refactor subscribe button and comment-add for visitor-interact UX (#1100)
authorRigel Kent <par@rigelk.eu>
Tue, 25 Sep 2018 13:42:58 +0000 (15:42 +0200)
committerGitHub <noreply@github.com>
Tue, 25 Sep 2018 13:42:58 +0000 (15:42 +0200)
* refactor subscribe button for visitor-subscribe UX
* refactor comment-add for visitor-interact UX

17 files changed:
client/src/app/+video-channels/video-channels.component.html
client/src/app/search/search.component.html
client/src/app/shared/shared.module.ts
client/src/app/shared/user-subscription/index.ts
client/src/app/shared/user-subscription/remote-subscribe.component.html [new file with mode: 0644]
client/src/app/shared/user-subscription/remote-subscribe.component.scss [new file with mode: 0644]
client/src/app/shared/user-subscription/remote-subscribe.component.ts [new file with mode: 0644]
client/src/app/shared/user-subscription/subscribe-button.component.html
client/src/app/shared/user-subscription/subscribe-button.component.scss
client/src/app/shared/user-subscription/subscribe-button.component.ts
client/src/app/videos/+video-watch/comment/video-comment-add.component.html
client/src/app/videos/+video-watch/comment/video-comment-add.component.scss
client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
client/src/app/videos/+video-watch/comment/video-comments.component.html
client/src/app/videos/+video-watch/video-watch.component.html
client/src/sass/application.scss
client/src/sass/include/_bootstrap.scss

index 8d6f81e1b8bff4644a25bd6bd27365c5a5d575bf..c65b5713d76b25943993eec491543c9e87cd1adc 100644 (file)
@@ -9,7 +9,7 @@
           <div class="actor-display-name">{{ videoChannel.displayName }}</div>
           <div class="actor-name">{{ videoChannel.nameWithHost }}</div>
 
-          <my-subscribe-button #subscribeButton *ngIf="isUserLoggedIn()" [videoChannel]="videoChannel"></my-subscribe-button>
+          <my-subscribe-button #subscribeButton [videoChannel]="videoChannel"></my-subscribe-button>
         </div>
         <div i18n class="actor-followers">{{ videoChannel.followersCount }} subscribers</div>
 
index c4072b29106cba3acf2b7f34fe597d8f0abea8a9..216905cf0a6320305f568d25d53fd166e267e40b 100644 (file)
@@ -41,7 +41,7 @@
         <div i18n class="video-channel-followers">{{ result.followersCount }} subscribers</div>
       </div>
 
-      <my-subscribe-button *ngIf="isUserLoggedIn()" [videoChannel]="result"></my-subscribe-button>
+      <my-subscribe-button [videoChannel]="result"></my-subscribe-button>
     </div>
 
     <div *ngIf="isVideo(result)" class="entry video">
index 1e71feb869b5b5acb232795d46db162bf75f7d13..076f1d2750a594cc3832b9bceb2d7b5ca3633bb3 100644 (file)
@@ -53,7 +53,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c
 import { VideoImportService } from '@app/shared/video-import/video-import.service'
 import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
 import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
-import { SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
+import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription'
 import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component'
 import { OverviewService } from '@app/shared/overview'
 
@@ -93,6 +93,7 @@ import { OverviewService } from '@app/shared/overview'
     ReactiveFileComponent,
     PeertubeCheckboxComponent,
     SubscribeButtonComponent,
+    RemoteSubscribeComponent,
     InstanceFeaturesTableComponent
   ],
 
@@ -127,6 +128,7 @@ import { OverviewService } from '@app/shared/overview'
     ReactiveFileComponent,
     PeertubeCheckboxComponent,
     SubscribeButtonComponent,
+    RemoteSubscribeComponent,
     InstanceFeaturesTableComponent,
 
     NumberFormatterPipe,
index faddae66a0b28f256f008408c80f74528a68a7d3..e76940f7b58fe6a5f8c382f6677ace4dfbe44120 100644 (file)
@@ -1,2 +1,3 @@
 export * from './user-subscription.service'
 export * from './subscribe-button.component'
+export * from './remote-subscribe.component'
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.html b/client/src/app/shared/user-subscription/remote-subscribe.component.html
new file mode 100644 (file)
index 0000000..3762142
--- /dev/null
@@ -0,0 +1,25 @@
+<form novalidate [formGroup]="form" 
+      (ngSubmit)="formValidated()">
+  <div class="form-group">
+    <input type="email"
+      formControlName="text"
+      class="form-control"
+      (keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()"
+      placeholder="jane_doe@example.com">
+  </div>
+  <button type="submit"
+    [disabled]="!form.valid"
+    class="btn btn-sm btn-remote-follow"
+    i18n>
+    <span *ngIf="!interact">Remote subscribe</span>
+    <span *ngIf="interact">Remote interact</span>
+  </button>
+  <my-help *ngIf="!interact && showHelp"
+           helpType="custom"
+           i18n-customHtml customHtml="You can subscribe to the channel via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there.">
+  </my-help>
+  <my-help *ngIf="showHelp && interact"
+           helpType="custom"
+           i18n-customHtml customHtml="You can interact with this via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there.">
+  </my-help>
+</form>
\ No newline at end of file
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.scss b/client/src/app/shared/user-subscription/remote-subscribe.component.scss
new file mode 100644 (file)
index 0000000..e492710
--- /dev/null
@@ -0,0 +1,5 @@
+@import '_mixins';
+
+.btn-remote-follow {
+  @include orange-button;
+}
\ No newline at end of file
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.ts b/client/src/app/shared/user-subscription/remote-subscribe.component.ts
new file mode 100644 (file)
index 0000000..7a81108
--- /dev/null
@@ -0,0 +1,43 @@
+import { Component, Input, OnInit } from '@angular/core'
+import { FormReactive } from '@app/shared/forms/form-reactive'
+import {
+  FormValidatorService,
+  UserValidatorsService
+} from '@app/shared/forms/form-validators'
+
+@Component({
+  selector: 'my-remote-subscribe',
+  templateUrl: './remote-subscribe.component.html',
+  styleUrls: ['./remote-subscribe.component.scss']
+})
+export class RemoteSubscribeComponent extends FormReactive implements OnInit {
+  @Input() account: string
+  @Input() interact = false
+  @Input() showHelp = false
+
+  constructor (
+    protected formValidatorService: FormValidatorService,
+    private userValidatorsService: UserValidatorsService
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.buildForm({
+      text: this.userValidatorsService.USER_EMAIL
+    })
+  }
+
+  onValidKey () {
+    this.onValueChanged()
+    if (!this.form.valid) return
+
+    this.formValidated()
+  }
+
+  formValidated () {
+    const address = this.form.value['text']
+    const [ , hostname ] = address.split('@')
+    window.open(`https://${hostname}/authorize_interaction?acct=${this.account}`)
+  }
+}
index 34c024c178428fac05f5100be12950aa70dd63c7..b62fb7dfa86d8f8eddd50a6ade11508f5649bcf1 100644 (file)
@@ -1,15 +1,45 @@
-<span i18n *ngIf="subscribed === false" class="subscribe-button" [ngClass]="size" role="button" (click)="subscribe()">
-  <span>Subscribe</span>
-  <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
-    {{ videoChannel.followersCount | myNumberFormatter }}
-  </span>
-</span>
-
-<span *ngIf="subscribed === true" class="unsubscribe-button" [ngClass]="size" role="button" (click)="unsubscribe()">
-  <span class="subscribed" i18n>Subscribed</span>
-  <span class="unsubscribe" i18n>Unsubscribe</span>
-
-  <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
-    {{ videoChannel.followersCount | myNumberFormatter }}
-  </span>
-</span>
+<div class="btn-group-subscribe btn-group"
+    [ngClass]="{'subscribe-button': subscribed !== true, 'unsubscribe-button': subscribed === true}">
+  <button *ngIf="subscribed === false && isUserLoggedIn()" type="button"
+          class="btn btn-sm" role="button"
+          (click)="subscribe()" i18n>
+    <span>
+      Subscribe
+    </span>
+    <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
+      {{ videoChannel.followersCount | myNumberFormatter }}
+    </span>
+  </button>
+  <button *ngIf="subscribed === true" type="button"
+          class="btn btn-sm" role="button"
+          (click)="unsubscribe()" i18n>Unsubscribe</button>
+
+  <div class="btn-group" ngbDropdown autoClose="outside" 
+       placement="bottom-right" role="group"
+       aria-label="Multiple ways to subscribe to the current channel">
+    <button class="btn btn-sm dropdown-toggle-split" ngbDropdownToggle>
+      <span *ngIf="!isUserLoggedIn()">
+        Subscribe
+      </span>
+      <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
+        {{ videoChannel.followersCount | myNumberFormatter }}
+      </span>
+    </button>
+    <div class="dropdown-menu" ngbDropdownMenu>
+
+      <h6 class="dropdown-header" i18n>Using an ActivityPub-compatible account</h6>
+      <button class="dropdown-item" (click)="subscribe()"
+              *ngIf="subscribed === false">
+        <span *ngIf="!isUserLoggedIn()" i18n>Subscribe with an account on {{ videoChannel.host }}</span>
+        <span *ngIf="isUserLoggedIn()" i18n>Subscribe with your local account</span>
+      </button>
+      <button class="dropdown-item" i18n>Subscribe with a remote account:</button>
+      <my-remote-subscribe showHelp="true" account="{{ uriAccount }}"></my-remote-subscribe>
+      <div class="dropdown-divider"></div>
+
+      <h6 class="dropdown-header" i18n>Using a syndication feed</h6>
+      <button (click)="rssOpen()" class="dropdown-item" i18n>Subscribe via RSS</button>
+
+    </div>
+  </div>
+</div>
\ No newline at end of file
index b78d2f59cd53b27d482341f9172c7fe8a5b7a709..7a8a8ee08cdae945140a4d6790a4b43502fc6658 100644 (file)
@@ -1,51 +1,46 @@
 @import '_variables';
 @import '_mixins';
 
-.subscribe-button {
+.btn-group-subscribe {
   @include peertube-button;
-  @include orange-button;
-}
+  @include disable-default-a-behaviour;
+  float: right;
+  padding: 0;
 
-.unsubscribe-button {
-  @include peertube-button;
-  @include grey-button
-}
+  &.btn-group > .btn:not(.dropdown-toggle) {
+    padding-right: 5px;
+    font-size: 15px;
+  }
+  &.btn-group > .btn-group:not(:first-child) > .btn {
+    padding-left: 2px;
+  }
 
-.subscribe-button,
-.unsubscribe-button {
-  display: inline-block;
+  &.subscribe-button {
+    .btn {
+      @include orange-button;
+      font-weight: 600;
+    }
 
-  &.small {
-    min-width: 75px;
-    height: 20px;
-    line-height: 20px;
-    font-size: 13px;
+    span.followers-count {
+      padding-left:5px;
+    }
   }
-
-  &.normal {
-    min-width: 120px;
-    height: 30px;
-    line-height: 30px;
-    font-size: 16px;
+  &.unsubscribe-button {
+    .btn {
+      @include grey-button;
+      font-weight: 600;
+    }
   }
-}
 
-.unsubscribe-button {
-  .subscribed {
-    display: inline;
+  .dropdown-header {
+    padding-left: 1rem;
   }
 
-  .unsubscribe {
-    display: none;
+  /deep/ form {
+    padding: 0.25rem 1rem;
   }
 
-  &:hover {
-    .subscribed {
-      display: none;
-    }
-
-    .unsubscribe {
-      display: inline;
-    }
+  input {
+    @include peertube-input-text(100%);
   }
-}
\ No newline at end of file
+}
index b5873a2eec4d507d6a451d578741b117f298e3f0..e3c758942703c91bd5c316e570949d065edfc3de 100644 (file)
@@ -1,4 +1,6 @@
 import { Component, Input, OnInit } from '@angular/core'
+import { Router } from '@angular/router'
+import { AuthService } from '@app/core'
 import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
 import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
 import { NotificationsService } from 'angular2-notifications'
@@ -17,6 +19,8 @@ export class SubscribeButtonComponent implements OnInit {
   subscribed: boolean
 
   constructor (
+    private authService: AuthService,
+    private router: Router,
     private notificationsService: NotificationsService,
     private userSubscriptionService: UserSubscriptionService,
     private i18n: I18n
@@ -26,16 +30,30 @@ export class SubscribeButtonComponent implements OnInit {
     return this.videoChannel.name + '@' + this.videoChannel.host
   }
 
+  get uriAccount () {
+    return this.videoChannel.ownerAccount.name + '@' + this.videoChannel.host
+  }
+
   ngOnInit () {
-    this.userSubscriptionService.isSubscriptionExists(this.uri)
-      .subscribe(
-        res => this.subscribed = res[this.uri],
+    if (this.isUserLoggedIn()) {
+      this.userSubscriptionService.isSubscriptionExists(this.uri)
+        .subscribe(
+          res => this.subscribed = res[this.uri],
 
-        err => this.notificationsService.error(this.i18n('Error'), err.message)
-      )
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+    }
   }
 
   subscribe () {
+    if (this.isUserLoggedIn()) {
+      this.localSubscribe()
+    } else {
+      this.gotoLogin()
+    }
+  }
+
+  localSubscribe () {
     this.userSubscriptionService.addSubscription(this.uri)
       .subscribe(
         () => {
@@ -52,6 +70,12 @@ export class SubscribeButtonComponent implements OnInit {
   }
 
   unsubscribe () {
+    if (this.isUserLoggedIn()) {
+      this.localUnsubscribe()
+    }
+  }
+
+  localUnsubscribe () {
     this.userSubscriptionService.deleteSubscription(this.uri)
         .subscribe(
           () => {
@@ -66,4 +90,16 @@ export class SubscribeButtonComponent implements OnInit {
           err => this.notificationsService.error(this.i18n('Error'), err.message)
         )
   }
+
+  isUserLoggedIn () {
+    return this.authService.isLoggedIn()
+  }
+
+  gotoLogin () {
+    this.router.navigate([ '/login' ])
+  }
+
+  rssOpen () {
+    window.open('')
+  }
 }
index f65a88d201e3e2512f6ca56e4af1f087268000cb..9441edfb11257ce02727ac699649402ca73eeb2a 100644 (file)
@@ -1,9 +1,11 @@
 <form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
   <div class="avatar-and-textarea">
-    <img [src]="user.accountAvatarUrl" alt="Avatar" />
+    <img [src]="getAvatarUrl()" alt="Avatar" />
 
     <div class="form-group">
       <textarea i18n-placeholder placeholder="Add comment..." autosize
+                [readonly]="(user === null) ? true : false"
+                (click)="openVisitorModal($event)"
                 formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
                 (keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea>
 
     </button>
   </div>
 </form>
+
+<ng-template #visitorModal let-modal>
+  <div class="modal-header">
+    <h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4>
+    <button type="button" class="close" aria-label="Close" (click)="hideVisitorModal()"></button>
+  </div>
+  <div class="modal-body" i18n>
+    <span i18n>
+      If you have an account on this instance, you can login:
+    </span>
+    <span class="btn btn-sm mx-3" role="button" (click)="gotoLogin()" i18n>login to comment</span>
+    <span i18n>
+      Otherwise you can comment using an account on an ActivityPub-compatible instance:
+    </span>
+    <my-remote-subscribe [interact]="true" account="{{ uri }}"></my-remote-subscribe>
+  </div>
+  <div class="modal-footer inputs">
+    <span i18n class="action-button action-button-cancel" role="button" (click)="hideVisitorModal()">
+      Cancel
+    </span>
+  </div>
+</ng-template>
index bb809296a237b1ac75ef79390b3c907964c065c1..e998cba5e7fe9100b72b3c9962942153f197213d 100644 (file)
@@ -36,7 +36,7 @@ form {
 
   button {
     @include peertube-button;
-    @include orange-button
+    @include orange-button;
   }
 }
 
@@ -44,4 +44,16 @@ form {
   textarea, .submit-comment button {
     font-size: 14px !important;
   }
+}
+
+.modal-body {
+  .btn {
+    @include peertube-button;
+    @include orange-button;
+  }
+
+  span {
+    float: left;
+    margin-bottom: 20px;
+  }
 }
\ No newline at end of file
index 9998685e8fda24ebea4496b38a2aea7dc99b231c..91af113d243a30d0bea9fb53b20bca2411efa173 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
+import { Router } from '@angular/router'
 import { NotificationsService } from 'angular2-notifications'
 import { Observable } from 'rxjs'
 import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
@@ -10,6 +11,8 @@ import { VideoCommentService } from './video-comment.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
 import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { AuthService } from '@app/core/auth'
 
 @Component({
   selector: 'my-video-comment-add',
@@ -25,15 +28,20 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
 
   @Output() commentCreated = new EventEmitter<VideoCommentCreate>()
 
+  @ViewChild('visitorModal') visitorModal: NgbModal
   @ViewChild('textarea') private textareaElement: ElementRef
 
   private addingComment = false
+  private uri: string
 
   constructor (
     protected formValidatorService: FormValidatorService,
     private videoCommentValidatorsService: VideoCommentValidatorsService,
     private notificationsService: NotificationsService,
     private videoCommentService: VideoCommentService,
+    private authService: AuthService,
+    private modalService: NgbModal,
+    private router: Router,
     private i18n: I18n
   ) {
     super()
@@ -44,19 +52,23 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
       text: this.videoCommentValidatorsService.VIDEO_COMMENT_TEXT
     })
 
-    if (this.focusOnInit === true) {
-      this.textareaElement.nativeElement.focus()
-    }
+    this.uri = this.router.url
 
-    if (this.parentComment) {
-      const mentions = this.parentComments
-        .filter(c => c.account.id !== this.user.account.id) // Don't add mention of ourselves
-        .map(c => '@' + c.by)
+    if (this.user) {
+      if (this.focusOnInit === true) {
+        this.textareaElement.nativeElement.focus()
+      }
+
+      if (this.parentComment) {
+        const mentions = this.parentComments
+          .filter(c => c.account.id !== this.user.account.id) // Don't add mention of ourselves
+          .map(c => '@' + c.by)
 
-      const mentionsSet = new Set(mentions)
-      const mentionsText = Array.from(mentionsSet).join(' ') + ' '
+        const mentionsSet = new Set(mentions)
+        const mentionsText = Array.from(mentionsSet).join(' ') + ' '
 
-      this.form.patchValue({ text: mentionsText })
+        this.form.patchValue({ text: mentionsText })
+      }
     }
   }
 
@@ -67,6 +79,20 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
     this.formValidated()
   }
 
+  openVisitorModal (event) {
+    if (this.user === null) { // we only open it for visitors
+      // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error
+      event.srcElement.blur()
+      event.preventDefault()
+
+      this.modalService.open(this.visitorModal)
+    }
+  }
+
+  hideVisitorModal () {
+    this.modalService.dismissAll()
+  }
+
   formValidated () {
     // If we validate very quickly the comment form, we might comment twice
     if (this.addingComment) return
@@ -101,6 +127,17 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
     return this.form.value['text']
   }
 
+  getAvatarUrl () {
+    if (this.user) return this.user.accountAvatarUrl
+    return window.location.origin + '/client/assets/images/default-avatar.png'
+  }
+
+  gotoLogin () {
+    this.hideVisitorModal()
+    this.authService.redirectUrl = this.router.url
+    this.router.navigate([ '/login' ])
+  }
+
   private addCommentReply (commentCreate: VideoCommentCreate) {
     return this.videoCommentService
       .addCommentReply(this.video.id, this.parentComment.id, commentCreate)
index bf6706ed386053c6301ad688c9a243787a1d3939..9919ac615ef4ec5d57b4fe17d2913d6fc320bda8 100644 (file)
@@ -10,7 +10,6 @@
 
   <ng-template [ngIf]="video.commentsEnabled === true">
     <my-video-comment-add
-      *ngIf="isUserLoggedIn()"
       [video]="video"
       [user]="user"
       (commentCreated)="onCommentThreadCreated($event)"
index 630345a35832d02fed8814696e8c3cbd48b33746..770785d023da9f19d98ed6f9bc5a0bc4063c3d76 100644 (file)
                 <img [src]="video.videoChannelAvatarUrl" alt="Video channel avatar" />
               </a>
 
-              <my-subscribe-button #subscribeButton *ngIf="isUserLoggedIn()" [videoChannel]="video.channel" size="small"></my-subscribe-button>
+              <my-subscribe-button #subscribeButton [videoChannel]="video.channel" size="small"></my-subscribe-button>
             </div>
 
             <div class="video-info-by">
                 <span i18n>By {{ video.byAccount }}</span>
                 <img [src]="video.accountAvatarUrl" alt="Account avatar" />
               </a>
-
-              <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.name}}@{{video.account.host}}</strong> and subscribe there."></my-help>
             </div>
           </div>
 
index 760be3822cdcd705ef3e40cf10e05eb30ccd5387..0029c22ef6bcc04dff1afc4c2d5bc9c4872fc167 100644 (file)
@@ -199,9 +199,18 @@ label {
 
   .dropdown-item {
     padding: 3px 15px;
+
+    &:active {
+      color: #000 !important;
+    }
+  }
+
+  button {
+    @include disable-default-a-behaviour;
   }
 
   a {
+    @include disable-default-a-behaviour;
     color: #000 !important;
   }
 }
index 4abc887ad40d6065b4d6eaa3421f5e621c9c5c84..7bce85c3752237f9d33bff6e311800ff160c9128 100644 (file)
@@ -46,7 +46,7 @@ $nav-pills-link-active-color: #000;
 @import '~bootstrap/scss/buttons';
 //@import '~bootstrap/scss/transitions';
 @import '~bootstrap/scss/dropdown';
-//@import '~bootstrap/scss/button-group';
+@import '~bootstrap/scss/button-group';
 @import '~bootstrap/scss/input-group';
 //@import '~bootstrap/scss/custom-forms';
 @import '~bootstrap/scss/nav';