improvements to login and sign-up pages (#3357)

* New login form ui
* Move InstanceAboutAccordion to shared components
* Update closed registration alert text
* Add alert for opened registration and move them bellow login form
* Adjust flex block on signup and login views
* Replace toggle accordion with expand on links in signup and login + scrollTo
* Improve display of login alerts
* Fix missing Component suffix
* Define min-width instance-information block sign-up and login for mobile screens
* Add ability to select specific panels in instanceAboutAccorddion
* Add instance title and short-description to common instanceAboutAccordion
* Clarify title alert in login page
* Add step terms for signup

Co-authored-by: kimsible <kimsible@users.noreply.github.com>
Co-authored-by: Rigel Kent <sendmemail@rigelk.eu>
pull/3423/head
Kimsible 2020-12-07 16:34:07 +01:00 committed by GitHub
parent 10f26f4203
commit 40360c17d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 542 additions and 253 deletions

View File

@ -8,73 +8,81 @@
</div> </div>
<ng-container *ngIf="!externalAuthError && !isAuthenticatedWithExternalAuth"> <ng-container *ngIf="!externalAuthError && !isAuthenticatedWithExternalAuth">
<div class="looking-for-account alert alert-info" *ngIf="signupAllowed === false" role="alert">
<h6 class="alert-heading" i18n>
If you are looking for an account…
</h6>
<div i18n>
Currently this instance doesn't allow for user registration, but you can find an instance
that gives you the possibility to sign up for an account and upload your videos there.
<br />
Find yours among multiple instances at <a class="alert-link" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
</div>
</div>
<div *ngIf="error" class="alert alert-danger">{{ error }} <div *ngIf="error" class="alert alert-danger">{{ error }}
<span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span> <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
</div> </div>
<div class="login-form-and-externals"> <div class="wrapper">
<div class="login-form-and-externals">
<form role="form" (ngSubmit)="login()" [formGroup]="form"> <form role="form" (ngSubmit)="login()" [formGroup]="form">
<div class="form-group"> <div class="form-group">
<div> <div>
<label i18n for="username">User</label> <label i18n for="username">User</label>
<input <input
type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1" type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #usernameInput formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #usernameInput
> >
<a i18n *ngIf="signupAllowed === true" routerLink="/signup" class="create-an-account"> </div>
or create an account
</a> <div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }}
</div>
</div> </div>
<div *ngIf="formErrors.username" class="form-error"> <div class="form-group">
{{ formErrors.username }} <label i18n for="password">Password</label>
</div>
</div>
<div class="form-group">
<label i18n for="password">Password</label>
<div>
<my-input-toggle-hidden formControlName="password" id="password" <my-input-toggle-hidden formControlName="password" id="password"
i18n-placeholder placeholder="Password" i18n-placeholder placeholder="Password"
[ngClass]="{ 'input-error': formErrors['password'] }" [ngClass]="{ 'input-error': formErrors['password'] }"
autocomplete="current-password"></my-input-toggle-hidden> autocomplete="current-password" tabindex="2"></my-input-toggle-hidden>
<a i18n-title class="forgot-password-button" (click)="openForgotPasswordModal()" title="Click here to reset your password">I forgot my password</a> <div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }}
</div>
</div> </div>
<div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }} <input type="submit" i18n-value value="Login" [disabled]="!form.valid">
<div class="additionnal-links">
<a class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
<div *ngIf="signupAllowed" class="signup-link">
<span>·</span>
<a i18n routerLink="/signup" class="create-an-account">Create an account</a>
</div>
</div> </div>
</div>
<input type="submit" i18n-value value="Login" [disabled]="!form.valid"> <div class="looking-for-account alert alert-info" role="alert">
</form> <h6 class="alert-heading" i18n>
Logging into an account lets you publish content
</h6>
<div class="external-login-blocks" *ngIf="getExternalLogins().length !== 0"> <div *ngIf="signupAllowed" i18n>
<div class="block-title" i18n>Or sign in with</div> This instance allows registration. However, be careful to check the <a class="terms-anchor" (click)="onTermsClick($event, instanceInformation)" href='#'>Terms</a><a class="terms-link" target="_blank" routerLink="/about/instance" fragment="terms">Terms</a> before creating an account.
You may also search for another instance to match your exact needs at: <br /><a class="alert-link" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
</div>
<div> <div *ngIf="!signupAllowed" i18n>
<a class="external-login-block" *ngFor="let auth of getExternalLogins()" [href]="getAuthHref(auth)" role="button"> Currently this instance doesn't allow for user registration, you may check the <a (click)="onTermsClick($event, instanceInformation)" href='#'>Terms</a> for more details or find an instance that gives you the possibility to sign up for an account and upload your videos there.
{{ auth.authDisplayName }} Find yours among multiple instances at: <br /> <a class="alert-link" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
</a> </div>
</div>
</form>
<div class="external-login-blocks" *ngIf="getExternalLogins().length !== 0">
<div class="block-title" i18n>Or sign in with</div>
<div>
<a class="external-login-block" *ngFor="let auth of getExternalLogins()" [href]="getAuthHref(auth)" role="button">
{{ auth.authDisplayName }}
</a>
</div>
</div> </div>
</div> </div>
</div>
<div #instanceInformation class="instance-information">
<my-instance-about-accordion (init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels"></my-instance-about-accordion>
</div>
</div>
</ng-container> </ng-container>
</div> </div>

View File

@ -1,5 +1,8 @@
@import '_variables'; @import '_variables';
@import '_mixins'; @import '_mixins';
@import './_bootstrap-variables';
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
label { label {
display: block; display: block;
@ -57,39 +60,138 @@ input[type=submit] {
} }
} }
.login-form-and-externals { .wrapper {
display: flex; display: flex;
justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 15px;
form { & > div {
margin: 0 50px 20px 0; flex: 1 1;
} }
.external-login-blocks { .login-form-and-externals {
min-width: 200px; display: flex;
flex-wrap: wrap;
font-size: 15px;
max-width: 450px;
margin-bottom: 40px;
margin-left: 10px;
margin-right: 10px;
.block-title { form {
font-weight: $font-semibold; margin: 0;
&, input {
width: 100%;
}
.additionnal-links {
display: block;
text-align: center;
margin-top: 20px;
margin-bottom: 20px;
.forgot-password-button,
.create-an-account {
padding: 4px;
display: inline-block;
color: var(--mainColor);
&:hover, &:active {
color: var(--mainHoverColor);
}
}
}
} }
.external-login-block { .external-login-blocks {
@include disable-default-a-behaviour; min-width: 200px;
cursor: pointer; .block-title {
border: 1px solid #d1d7e0; font-weight: $font-semibold;
border-radius: 5px;
color: pvar(--mainForegroundColor);
margin: 10px 10px 0 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 35px;
min-width: 100px;
&:hover {
background-color: rgba(209, 215, 224, 0.5)
} }
.external-login-block {
@include disable-default-a-behaviour;
cursor: pointer;
border: 1px solid #d1d7e0;
border-radius: 5px;
color: pvar(--mainForegroundColor);
margin: 10px 10px 0 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 35px;
min-width: 100px;
&:hover {
background-color: rgba(209, 215, 224, 0.5)
}
}
}
.signup-link {
display: inline-block;
}
}
.instance-information {
max-width: 600px;
min-width: 350px;
margin-bottom: 40px;
margin-left: 10px;
margin-right: 10px;
}
.terms-anchor {
display: inline;
}
.terms-link {
display: none;
}
}
@mixin columnReverseDisplay {
flex-direction: column-reverse;
.login-form-and-externals,
.instance-information {
width: 100%;
margin-left: 0;
margin-right: 0;
max-width: 450px;
min-width: unset;
align-self: center;
}
.instance-information {
::ng-deep .accordion {
display: none;
}
}
.terms-anchor {
display: none;
}
.terms-link {
display: inline;
}
}
@media screen and (max-width: breakpoint(md)) {
.wrapper {
@include columnReverseDisplay();
}
}
@media screen and (max-width: breakpoint(md) + $menu-width) {
:host-context(.main-col:not(.expanded)) {
.wrapper {
@include columnReverseDisplay();
} }
} }
} }

View File

@ -3,9 +3,10 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { AuthService, Notifier, RedirectService, UserService } from '@app/core' import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service' import { HooksService } from '@app/core/plugins/hooks.service'
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
@Component({ @Component({
@ -18,6 +19,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
@ViewChild('usernameInput', { static: false }) usernameInput: ElementRef @ViewChild('usernameInput', { static: false }) usernameInput: ElementRef
@ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef
accordion: NgbAccordion
error: string = null error: string = null
forgotPasswordEmail = '' forgotPasswordEmail = ''
@ -25,6 +27,14 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
externalAuthError = false externalAuthError = false
externalLogins: string[] = [] externalLogins: string[] = []
instanceInformationPanels = {
terms: true,
administrators: false,
features: false,
moderation: false,
codeOfConduct: false
}
private openedForgotPasswordModal: NgbModalRef private openedForgotPasswordModal: NgbModalRef
private serverConfig: ServerConfig private serverConfig: ServerConfig
@ -45,6 +55,15 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
return this.serverConfig.signup.allowed === true return this.serverConfig.signup.allowed === true
} }
onTermsClick (event: Event, instanceInformation: HTMLElement) {
event.preventDefault()
if (this.accordion) {
this.accordion.expand('terms')
instanceInformation.scrollIntoView({ behavior: 'smooth' })
}
}
isEmailDisabled () { isEmailDisabled () {
return this.serverConfig.email.enabled === false return this.serverConfig.email.enabled === false
} }
@ -122,6 +141,10 @@ The link will expire within 1 hour.`
this.openedForgotPasswordModal.close() this.openedForgotPasswordModal.close()
} }
onInstanceAboutAccordionInit (instanceAboutAccordion: InstanceAboutAccordionComponent) {
this.accordion = instanceAboutAccordion.accordion
}
private loadExternalAuthToken (username: string, token: string) { private loadExternalAuthToken (username: string, token: string) {
this.isAuthenticatedWithExternalAuth = true this.isAuthenticatedWithExternalAuth = true

View File

@ -1,6 +1,7 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { SharedFormModule } from '@app/shared/shared-forms' import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons' import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedInstanceModule } from '@app/shared/shared-instance'
import { SharedMainModule } from '@app/shared/shared-main' import { SharedMainModule } from '@app/shared/shared-main'
import { LoginRoutingModule } from './login-routing.module' import { LoginRoutingModule } from './login-routing.module'
import { LoginComponent } from './login.component' import { LoginComponent } from './login.component'
@ -11,7 +12,9 @@ import { LoginComponent } from './login.component'
SharedMainModule, SharedMainModule,
SharedFormModule, SharedFormModule,
SharedGlobalIconModule SharedGlobalIconModule,
SharedInstanceModule
], ],
declarations: [ declarations: [

View File

@ -2,7 +2,7 @@
<header *ngIf="steps.length > 2"> <header *ngIf="steps.length > 2">
<ng-container *ngFor="let step of steps; let i = index; let isLast = last;"> <ng-container *ngFor="let step of steps; let i = index; let isLast = last;">
<div <div
class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step) }" [attr.aria-current]="selectedIndex === i" class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step), 'c-hand': isAccessible(i) }" [attr.aria-current]="selectedIndex === i"
(click)="onClick(i)" (click)="onClick(i)"
> >
<div class="step-index"> <div class="step-index">

View File

@ -4,6 +4,12 @@
$grey-color: #9CA3AB; $grey-color: #9CA3AB;
$index-block-height: 32px; $index-block-height: 32px;
.container {
padding-left: 0;
padding-right: 0;
max-width: unset !important;
}
header { header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -17,6 +23,10 @@ header {
align-items: center; align-items: center;
width: $index-block-height; width: $index-block-height;
&:not(.c-hand) {
cursor: default;
}
.step-index { .step-index {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -16,4 +16,11 @@ export class CustomStepperComponent extends CdkStepper {
isCompleted (step: CdkStep) { isCompleted (step: CdkStep) {
return step.stepControl && step.stepControl.dirty && step.stepControl.valid return step.stepControl && step.stepControl.dirty && step.stepControl.valid
} }
isAccessible (index: number) {
const stepsCompletedMap = this.steps.map(step => this.isCompleted(step))
return index === 0
? true
: stepsCompletedMap[ index - 1 ]
}
} }

View File

@ -0,0 +1,18 @@
<form role="form" [formGroup]="form">
<div class="form-group form-group-terms">
<my-peertube-checkbox inputName="terms" formControlName="terms">
<ng-template ptTemplate="label">
<ng-container i18n>
I am at least 16 years old and agree
to the <a class="terms-anchor" (click)="onTermsClick($event)" href='#'>Terms</a>
<ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
of this instance
</ng-container>
</ng-template>
</my-peertube-checkbox>
<div *ngIf="formErrors.terms" class="form-error">
{{ formErrors.terms }}
</div>
</div>
</form>

View File

@ -0,0 +1,47 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { FormGroup } from '@angular/forms'
import {
USER_TERMS_VALIDATOR
} from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
@Component({
selector: 'my-register-step-terms',
templateUrl: './register-step-terms.component.html',
styleUrls: [ './register.component.scss' ]
})
export class RegisterStepTermsComponent extends FormReactive implements OnInit {
@Input() hasCodeOfConduct = false
@Output() formBuilt = new EventEmitter<FormGroup>()
@Output() termsClick = new EventEmitter<void>()
@Output() codeOfConductClick = new EventEmitter<void>()
constructor (
protected formValidatorService: FormValidatorService
) {
super()
}
get instanceHost () {
return window.location.host
}
ngOnInit () {
this.buildForm({
terms: USER_TERMS_VALIDATOR
})
setTimeout(() => this.formBuilt.emit(this.form))
}
onTermsClick (event: Event) {
event.preventDefault()
this.termsClick.emit()
}
onCodeOfConductClick (event: Event) {
event.preventDefault()
this.codeOfConductClick.emit()
}
}

View File

@ -63,20 +63,4 @@
</div> </div>
</div> </div>
<div class="form-group form-group-terms">
<my-peertube-checkbox inputName="terms" formControlName="terms">
<ng-template ptTemplate="label">
<ng-container i18n>
I am at least 16 years old and agree
to the <a (click)="onTermsClick($event)" href='#'>Terms</a>
<ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
of this instance
</ng-container>
</ng-template>
</my-peertube-checkbox>
<div *ngIf="formErrors.terms" class="form-error">
{{ formErrors.terms }}
</div>
</div>
</form> </form>

View File

@ -7,7 +7,6 @@ import {
USER_DISPLAY_NAME_REQUIRED_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
USER_EMAIL_VALIDATOR, USER_EMAIL_VALIDATOR,
USER_PASSWORD_VALIDATOR, USER_PASSWORD_VALIDATOR,
USER_TERMS_VALIDATOR,
USER_USERNAME_VALIDATOR USER_USERNAME_VALIDATOR
} from '@app/shared/form-validators/user-validators' } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
@ -18,12 +17,9 @@ import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
styleUrls: [ './register.component.scss' ] styleUrls: [ './register.component.scss' ]
}) })
export class RegisterStepUserComponent extends FormReactive implements OnInit { export class RegisterStepUserComponent extends FormReactive implements OnInit {
@Input() hasCodeOfConduct = false
@Input() videoUploadDisabled = false @Input() videoUploadDisabled = false
@Output() formBuilt = new EventEmitter<FormGroup>() @Output() formBuilt = new EventEmitter<FormGroup>()
@Output() termsClick = new EventEmitter<void>()
@Output() codeOfConductClick = new EventEmitter<void>()
constructor ( constructor (
protected formValidatorService: FormValidatorService, protected formValidatorService: FormValidatorService,
@ -41,8 +37,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
displayName: USER_DISPLAY_NAME_REQUIRED_VALIDATOR, displayName: USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
username: USER_USERNAME_VALIDATOR, username: USER_USERNAME_VALIDATOR,
password: USER_PASSWORD_VALIDATOR, password: USER_PASSWORD_VALIDATOR,
email: USER_EMAIL_VALIDATOR, email: USER_EMAIL_VALIDATOR
terms: USER_TERMS_VALIDATOR
}) })
setTimeout(() => this.formBuilt.emit(this.form)) setTimeout(() => this.formBuilt.emit(this.form))
@ -54,16 +49,6 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
.subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
} }
onTermsClick (event: Event) {
event.preventDefault()
this.termsClick.emit()
}
onCodeOfConductClick (event: Event) {
event.preventDefault()
this.codeOfConductClick.emit()
}
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
const username = this.form.value['username'] || '' const username = this.form.value['username'] || ''

View File

@ -10,19 +10,26 @@
<div class="wrapper" [hidden]="signupDone"> <div class="wrapper" [hidden]="signupDone">
<div class="register-form"> <div class="register-form">
<my-custom-stepper linear *ngIf="!signupDone"> <my-custom-stepper linear *ngIf="!signupDone">
<cdk-step [stepControl]="formStepUser" i18n-label label="User"> <cdk-step [stepControl]="formStepTerms" i18n-label="Stepper label for the registration page describing terms of service" label="Terms">
<my-register-step-user <div class="instance-information">
[hasCodeOfConduct]="!!aboutHtml.codeOfConduct" <my-instance-about-accordion (init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels"></my-instance-about-accordion>
[videoUploadDisabled]="videoUploadDisabled" </div>
(formBuilt)="onUserFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
>
</my-register-step-user>
<button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid" <my-register-step-terms
(click)="signup()">{{ videoUploadDisabled ? 'Signup' : 'Next' }}</button> [hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
(formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
></my-register-step-terms>
<button cdkStepperNext [disabled]="!formStepTerms || !formStepTerms.valid">{{ defaultNextStepButtonLabel }}</button>
</cdk-step> </cdk-step>
<cdk-step [stepControl]="formStepChannel" i18n-label label="Channel" *ngIf="!videoUploadDisabled"> <cdk-step [stepControl]="formStepUser" i18n-label="Stepper label for the registration page asking user informations" label="User">
<my-register-step-user (formBuilt)="onUserFormBuilt($event)" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-user>
<button cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid" (click)="videoUploadDisabled && signup()">{{ stepUserButtonLabel }}</button>
</cdk-step>
<cdk-step [stepControl]="formStepChannel" i18n-label="Stepper label for the registration page asking information about the default channel" label="Channel" *ngIf="!videoUploadDisabled">
<my-register-step-channel (formBuilt)="onChannelFormBuilt($event)" [username]="getUsername()"></my-register-step-channel> <my-register-step-channel (formBuilt)="onChannelFormBuilt($event)" [username]="getUsername()"></my-register-step-channel>
<button i18n cdkStepperNext (click)="signup()" <button i18n cdkStepperNext (click)="signup()"
@ -43,58 +50,6 @@
</cdk-step> </cdk-step>
</my-custom-stepper> </my-custom-stepper>
</div> </div>
<div class="instance-information">
<ngb-accordion [closeOthers]="true" #accordion="ngbAccordion">
<ngb-panel id="instance-features" i18n-title title="Features found on this instance">
<ng-template ngbPanelContent>
<my-instance-features-table></my-instance-features-table>
</ng-template>
</ngb-panel>
<ng-container *ngIf="about">
<ngb-panel
*ngIf="aboutHtml.administrator || about.instance.maintenanceLifetime || about.instance.businessModel"
id="admin-sustainability" i18n-title title="Administrators & Sustainability"
>
<ng-template ngbPanelContent>
<div class="block">
<strong i18n>Who are we?</strong>
<div [innerHTML]="aboutHtml.administrator"></div>
</div>
<div class="block">
<strong i18n>How long do we plan to maintain this instance?</strong>
<div [innerHTML]="about.instance.maintenanceLifetime"></div>
</div>
<div class="block">
<strong i18n>How will we finance this instance?</strong>
<div [innerHTML]="about.instance.businessModel"></div>
</div>
</ng-template>
</ngb-panel>
<ngb-panel *ngIf="aboutHtml.moderationInformation" id="moderation-information" i18n-title title="Moderation information">
<ng-template ngbPanelContent>
<div class="block" [innerHTML]="aboutHtml.moderationInformation"></div>
</ng-template>
</ngb-panel>
<ngb-panel *ngIf="aboutHtml.codeOfConduct" id="code-of-conduct" i18n-title title="Code of conduct">
<ng-template ngbPanelContent>
<div class="block" [innerHTML]="aboutHtml.codeOfConduct"></div>
</ng-template>
</ngb-panel>
<ngb-panel *ngIf="aboutHtml.terms" id="terms" i18n-title title="Terms">
<ng-template ngbPanelContent>
<div class="block" [innerHTML]="aboutHtml.terms"></div>
</ng-template>
</ngb-panel>
</ng-container>
</ngb-accordion>
</div>
</div> </div>
</div> </div>

View File

@ -1,9 +1,5 @@
@import '_variables'; @import '_variables';
@import '_mixins'; @import '_mixins';
@import "./_bootstrap-variables";
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
.alert { .alert {
font-size: 15px; font-size: 15px;
@ -12,44 +8,20 @@
.wrapper { .wrapper {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
flex-wrap: wrap;
& > div { .register-form {
margin-bottom: 40px; max-width: 600px;
align-self: center;
}
&.register-form { .register-form,
width: 450px; .instance-information {
width: 100%;
}
@media screen and (max-width: $mobile-view) { .instance-information {
width: 100%; margin-bottom: 15px;
}
}
&.instance-information {
width: 600px;
margin-bottom: 40px;
.block {
font-size: 15px;
margin-bottom: 15px;
padding: 0 $btn-padding-x;
}
@media screen and (max-width: 1500px) {
width: 450px;
}
@media screen and (max-width: $mobile-view) {
width: 100%;
}
ngb-accordion ::ng-deep {
.btn {
font-weight: $font-semibold !important;
color: pvar(--mainForegroundColor) !important;
}
}
}
} }
} }
@ -58,15 +30,19 @@
} }
.input-group { .input-group {
@include peertube-input-group(400px); @include peertube-input-group(100%);
} }
.input-group-append { .input-group-append {
height: 30px; height: 30px;
} }
.form-group-terms {
width: 100% !important;
}
input:not([type=submit]) { input:not([type=submit]) {
@include peertube-input-text(400px); @include peertube-input-text(100%);
display: block; display: block;
&#username, &#username,
@ -76,19 +52,10 @@ input:not([type=submit]) {
} }
} }
@media screen and (max-width: $mobile-view) {
.form-group-terms,
.input-group,
input:not([type=submit]) {
width: 100%;
}
}
input[type=submit], input[type=submit],
button { button {
@include peertube-button; @include peertube-button;
@include orange-button; @include orange-button;
} }
.name-information { .name-information {

View File

@ -1,12 +1,12 @@
import { Component, OnInit, ViewChild } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { FormGroup } from '@angular/forms' import { FormGroup } from '@angular/forms'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { AuthService, Notifier, UserService } from '@app/core' import { AuthService, UserService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service' import { HooksService } from '@app/core/plugins/hooks.service'
import { InstanceService } from '@app/shared/shared-instance'
import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
import { UserRegister } from '@shared/models' import { UserRegister } from '@shared/models'
import { About, ServerConfig } from '@shared/models/server' import { ServerConfig } from '@shared/models/server'
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
@Component({ @Component({
selector: 'my-register', selector: 'my-register',
@ -14,35 +14,39 @@ import { About, ServerConfig } from '@shared/models/server'
styleUrls: [ './register.component.scss' ] styleUrls: [ './register.component.scss' ]
}) })
export class RegisterComponent implements OnInit { export class RegisterComponent implements OnInit {
@ViewChild('accordion', { static: true }) accordion: NgbAccordion accordion: NgbAccordion
info: string = null info: string = null
error: string = null error: string = null
success: string = null success: string = null
signupDone = false signupDone = false
about: About
aboutHtml = {
description: '',
terms: '',
codeOfConduct: '',
moderationInformation: '',
administrator: ''
}
videoUploadDisabled: boolean videoUploadDisabled: boolean
formStepTerms: FormGroup
formStepUser: FormGroup formStepUser: FormGroup
formStepChannel: FormGroup formStepChannel: FormGroup
aboutHtml = {
codeOfConduct: ''
}
instanceInformationPanels = {
codeOfConduct: true,
terms: true,
administrators: false,
features: false,
moderation: false
}
defaultNextStepButtonLabel = $localize`Next`
stepUserButtonLabel = this.defaultNextStepButtonLabel
private serverConfig: ServerConfig private serverConfig: ServerConfig
constructor ( constructor (
private route: ActivatedRoute, private route: ActivatedRoute,
private authService: AuthService, private authService: AuthService,
private notifier: Notifier,
private userService: UserService, private userService: UserService,
private instanceService: InstanceService,
private hooks: HooksService private hooks: HooksService
) { ) {
} }
@ -55,19 +59,12 @@ export class RegisterComponent implements OnInit {
this.serverConfig = this.route.snapshot.data.serverConfig this.serverConfig = this.route.snapshot.data.serverConfig
this.videoUploadDisabled = this.serverConfig.user.videoQuota === 0 this.videoUploadDisabled = this.serverConfig.user.videoQuota === 0
this.stepUserButtonLabel = this.videoUploadDisabled
this.instanceService.getAbout() ? $localize`Signup`
.subscribe( : this.defaultNextStepButtonLabel
async about => {
this.about = about
this.aboutHtml = await this.instanceService.buildHtml(about)
},
err => this.notifier.error(err.message)
)
this.hooks.runAction('action:signup.register.init', 'signup') this.hooks.runAction('action:signup.register.init', 'signup')
} }
hasSameChannelAndAccountNames () { hasSameChannelAndAccountNames () {
@ -86,6 +83,10 @@ export class RegisterComponent implements OnInit {
return this.formStepChannel.value['name'] return this.formStepChannel.value['name']
} }
onTermsFormBuilt (form: FormGroup) {
this.formStepTerms = form
}
onUserFormBuilt (form: FormGroup) { onUserFormBuilt (form: FormGroup) {
this.formStepUser = form this.formStepUser = form
} }
@ -102,6 +103,11 @@ export class RegisterComponent implements OnInit {
if (this.accordion) this.accordion.toggle('code-of-conduct') if (this.accordion) this.accordion.toggle('code-of-conduct')
} }
onInstanceAboutAccordionInit (instanceAboutAccordion: InstanceAboutAccordionComponent) {
this.accordion = instanceAboutAccordion.accordion
this.aboutHtml = instanceAboutAccordion.aboutHtml
}
async signup () { async signup () {
this.error = null this.error = null

View File

@ -2,10 +2,10 @@ import { CdkStepperModule } from '@angular/cdk/stepper'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module'
import { SharedInstanceModule } from '@app/shared/shared-instance' import { SharedInstanceModule } from '@app/shared/shared-instance'
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
import { CustomStepperComponent } from './custom-stepper.component' import { CustomStepperComponent } from './custom-stepper.component'
import { RegisterRoutingModule } from './register-routing.module' import { RegisterRoutingModule } from './register-routing.module'
import { RegisterStepChannelComponent } from './register-step-channel.component' import { RegisterStepChannelComponent } from './register-step-channel.component'
import { RegisterStepTermsComponent } from './register-step-terms.component'
import { RegisterStepUserComponent } from './register-step-user.component' import { RegisterStepUserComponent } from './register-step-user.component'
import { RegisterComponent } from './register.component' import { RegisterComponent } from './register.component'
@ -14,7 +14,6 @@ import { RegisterComponent } from './register.component'
RegisterRoutingModule, RegisterRoutingModule,
CdkStepperModule, CdkStepperModule,
NgbAccordionModule,
SignupSharedModule, SignupSharedModule,
@ -25,6 +24,7 @@ import { RegisterComponent } from './register.component'
RegisterComponent, RegisterComponent,
CustomStepperComponent, CustomStepperComponent,
RegisterStepChannelComponent, RegisterStepChannelComponent,
RegisterStepTermsComponent,
RegisterStepUserComponent RegisterStepUserComponent
], ],

View File

@ -115,9 +115,7 @@ export const USER_DESCRIPTION_VALIDATOR: BuildFormValidator = {
} }
export const USER_TERMS_VALIDATOR: BuildFormValidator = { export const USER_TERMS_VALIDATOR: BuildFormValidator = {
VALIDATORS: [ VALIDATORS: [ Validators.requiredTrue ],
Validators.requiredTrue
],
MESSAGES: { MESSAGES: {
'required': $localize`You must agree with the instance terms in order to register on it.` 'required': $localize`You must agree with the instance terms in order to register on it.`
} }

View File

@ -1,4 +1,5 @@
export * from './feature-boolean.component' export * from './feature-boolean.component'
export * from './instance-about-accordion.component'
export * from './instance-features-table.component' export * from './instance-features-table.component'
export * from './instance-follow.service' export * from './instance-follow.service'
export * from './instance-statistics.component' export * from './instance-statistics.component'

View File

@ -0,0 +1,53 @@
<h2 class="instance-name">{{ about?.instance.name }}</h2>
<div class="instance-short-description">{{ about?.instance.shortDescription }}</div>
<ngb-accordion #accordion="ngbAccordion" [closeOthers]="true">
<ngb-panel *ngIf="panels.features" id="instance-features" i18n-title title="Features found on this instance">
<ng-template ngbPanelContent>
<my-instance-features-table></my-instance-features-table>
</ng-template>
</ngb-panel>
<ng-container *ngIf="about">
<ngb-panel
*ngIf="getAdministratorsPanel()"
id="admin-sustainability" i18n-title title="Administrators & Sustainability"
>
<ng-template ngbPanelContent>
<div class="block">
<strong i18n>Who are we?</strong>
<div [innerHTML]="aboutHtml.administrator"></div>
</div>
<div class="block">
<strong i18n>How long do we plan to maintain this instance?</strong>
<div [innerHTML]="about.instance.maintenanceLifetime"></div>
</div>
<div class="block">
<strong i18n>How will we finance this instance?</strong>
<div [innerHTML]="about.instance.businessModel"></div>
</div>
</ng-template>
</ngb-panel>
<ngb-panel *ngIf="termsPanel" id="terms" i18n-title title="Terms">
<ng-template ngbPanelContent>
<div class="block" [innerHTML]="aboutHtml.terms"></div>
</ng-template>
</ngb-panel>
<ngb-panel *ngIf="moderationPanel" id="moderation-information" i18n-title title="Moderation information">
<ng-template ngbPanelContent>
<div class="block" [innerHTML]="aboutHtml.moderationInformation"></div>
</ng-template>
</ngb-panel>
<ngb-panel *ngIf="codeOfConductPanel" id="code-of-conduct" i18n-title title="Code of conduct">
<ng-template ngbPanelContent>
<div class="block" [innerHTML]="aboutHtml.codeOfConduct"></div>
</ng-template>
</ngb-panel>
</ng-container>
</ngb-accordion>

View File

@ -0,0 +1,46 @@
@import '_variables';
@import '_mixins';
@import "./_bootstrap-variables";
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
.instance-name {
line-height: 1.7rem;
}
.instance-short-description {
@include ellipsis-multiline(1rem, 3);
margin-top: 20px;
margin-bottom: 20px;
}
.block {
font-size: 15px;
margin-bottom: 15px;
padding: 0 $btn-padding-x;
}
ngb-accordion ::ng-deep {
.card {
border-color: var(--mainBackgroundColor);
.card-header {
background-color: unset;
padding: 0;
& + .collapse.show {
background-color: var(--submenuColor);
}
}
}
.btn {
@include peertube-button;
@include grey-button;
border-radius: unset;
width: 100%;
}
}

View File

@ -0,0 +1,71 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
import { InstanceService } from './instance.service'
import { Notifier } from '@app/core'
import { About } from '@shared/models/server'
@Component({
selector: 'my-instance-about-accordion',
templateUrl: './instance-about-accordion.component.html',
styleUrls: ['./instance-about-accordion.component.scss']
})
export class InstanceAboutAccordionComponent implements OnInit {
@ViewChild('accordion', { static: true }) accordion: NgbAccordion
@Output() init: EventEmitter<InstanceAboutAccordionComponent> = new EventEmitter<InstanceAboutAccordionComponent>()
@Input() panels = {
features: true,
administrators: true,
moderation: true,
codeOfConduct: true,
terms: true
}
about: About
aboutHtml = {
description: '',
terms: '',
codeOfConduct: '',
moderationInformation: '',
administrator: ''
}
constructor (
private instanceService: InstanceService,
private notifier: Notifier
) { }
ngOnInit (): void {
this.instanceService.getAbout()
.subscribe(
async about => {
this.about = about
this.aboutHtml = await this.instanceService.buildHtml(about)
this.init.emit(this)
},
err => this.notifier.error(err.message)
)
}
getAdministratorsPanel () {
if (!this.about) return false
if (!this.panels.administrators) return false
return !!(this.aboutHtml?.administrator || this.about?.instance.maintenanceLifetime || this.about?.instance.businessModel)
}
get moderationPanel () {
return this.panels.moderation && !!this.aboutHtml.moderationInformation
}
get codeOfConductPanel () {
return this.panels.codeOfConduct && !!this.aboutHtml.codeOfConduct
}
get termsPanel () {
return this.panels.terms && !!this.aboutHtml.terms
}
}

View File

@ -1,7 +1,9 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
import { SharedMainModule } from '../shared-main/shared-main.module' import { SharedMainModule } from '../shared-main/shared-main.module'
import { FeatureBooleanComponent } from './feature-boolean.component' import { FeatureBooleanComponent } from './feature-boolean.component'
import { InstanceAboutAccordionComponent } from './instance-about-accordion.component'
import { InstanceFeaturesTableComponent } from './instance-features-table.component' import { InstanceFeaturesTableComponent } from './instance-features-table.component'
import { InstanceFollowService } from './instance-follow.service' import { InstanceFollowService } from './instance-follow.service'
import { InstanceStatisticsComponent } from './instance-statistics.component' import { InstanceStatisticsComponent } from './instance-statistics.component'
@ -9,17 +11,20 @@ import { InstanceService } from './instance.service'
@NgModule({ @NgModule({
imports: [ imports: [
SharedMainModule SharedMainModule,
NgbAccordionModule
], ],
declarations: [ declarations: [
FeatureBooleanComponent, FeatureBooleanComponent,
InstanceAboutAccordionComponent,
InstanceFeaturesTableComponent, InstanceFeaturesTableComponent,
InstanceStatisticsComponent InstanceStatisticsComponent
], ],
exports: [ exports: [
FeatureBooleanComponent, FeatureBooleanComponent,
InstanceAboutAccordionComponent,
InstanceFeaturesTableComponent, InstanceFeaturesTableComponent,
InstanceStatisticsComponent InstanceStatisticsComponent
], ],