1
0
Fork 0

Redesign register steps

This commit is contained in:
Chocobozzz 2022-06-14 13:54:54 +02:00
parent 936ce6e563
commit 6f03f944c3
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
45 changed files with 1055 additions and 379 deletions

View File

@ -4,7 +4,7 @@
<div class="col-12 col-lg-4 col-xl-3"></div>
<div class="col-12 col-lg-8">
<div class="callout callout-info">
<div class="callout callout-orange">
<span i18n>
Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
</span>

View File

@ -1,24 +1,29 @@
<section class="container">
<section>
<header *ngIf="steps.length > 2">
<ng-container *ngFor="let step of steps; let i = index; let isLast = last;">
<div
class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step), 'c-hand': isAccessible(i) }" [attr.aria-current]="selectedIndex === i"
(click)="onClick(i)"
>
<div class="step-index">
<ng-container *ngIf="!isCompleted(step)"><span class="visually-hidden" i18n>Step</span> {{ i + 1 }}</ng-container>
<my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon>
<div class="header-steps">
<ng-container *ngFor="let step of steps; let i = index; let isLast = last;">
<div
class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step), 'c-hand': isAccessible(step) }" [attr.aria-current]="selectedIndex === i"
(click)="onClick(i)"
>
<div class="step-index">
<span class="visually-hidden" i18n>Step</span> {{ i + 1 }}
<div class="completed-icon" *ngIf="isCompleted(step)">
<my-global-icon iconName="tick"></my-global-icon>
</div>
</div>
<div class="step-label">{{ step.label }}</div>
</div>
<div class="step-label">{{ step.label }}</div>
</div>
<!-- Do no display if this is the last child -->
<div *ngIf="!isLast" class="connector"></div>
</ng-container>
<!-- Do no display if this is the last child -->
<div *ngIf="!isLast" class="connector"></div>
</ng-container>
</div>
</header>
<div [style.display]="selected ? 'block' : 'none'">
<div class="margin-content" [style.display]="selected ? 'block' : 'none'">
<ng-container [ngTemplateOutlet]="selected.content"></ng-container>
</div>

View File

@ -2,76 +2,113 @@
@use '_variables' as *;
@use '_mixins' as *;
$grey-color: #9CA3AB;
$index-block-height: 32px;
.container {
@include padding-left(0);
@include padding-right(0);
max-width: unset !important;
}
$index-block-height: 40px;
header {
margin-bottom: 40px;
padding-bottom: 60px;
width: 100%;
background-color: pvar(--mainColorVeryLight);
}
.header-steps {
max-width: 800px;
display: flex;
justify-content: space-between;
font-size: 15px;
margin-bottom: 30px;
margin: auto;
.step-info {
color: $grey-color;
// Useful on small screens
padding: 0 20px;
}
.step-index {
display: flex;
justify-content: center;
align-items: center;
width: $index-block-height;
height: $index-block-height;
border-radius: $index-block-height;
border: 1px solid pvar(--mainColor);
margin-bottom: 10px;
font-size: 24px;
position: relative;
.completed-icon {
width: 16px;
height: 16px;
border-radius: 16px;
background-color: pvar(--mainBackgroundColor);
position: absolute;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: $index-block-height;
border: 1px solid pvar(--mainColor);
&:not(.c-hand) {
cursor: default;
}
my-global-icon {
@include apply-svg-color(pvar(--mainColor));
.step-index {
display: flex;
justify-content: center;
align-items: center;
width: $index-block-height;
height: $index-block-height;
border-radius: 100px;
border: 2px solid $grey-color;
margin-bottom: 10px;
width: 12px;
height: 12px;
}
}
}
my-global-icon {
@include apply-svg-color(pvar(--mainBackgroundColor));
.step-label {
width: max-content;
font-size: 18px;
}
width: 22px;
height: 22px;
}
.step-info {
color: pvar(--mainColor);
display: flex;
flex-direction: column;
align-items: center;
width: $index-block-height;
opacity: 0.5;
cursor: default;
&.c-hand {
cursor: pointer;
}
&.active,
&.completed {
.step-index {
background-color: pvar(--mainColor);
color: pvar(--mainBackgroundColor);
}
.step-label {
width: max-content;
}
&.active,
&.completed {
.step-index {
border-color: pvar(--mainColor);
background-color: pvar(--mainColor);
color: pvar(--mainBackgroundColor);
}
.step-label {
color: pvar(--mainColor);
}
}
&.completed {
cursor: pointer;
color: pvar(--mainColor);
}
}
.connector {
flex: auto;
margin: math.div($index-block-height, 2) 10px 0 10px;
height: 2px;
background-color: $grey-color;
&.active {
opacity: 1;
}
}
.connector {
flex: auto;
margin: math.div($index-block-height, 2) 10px 0 10px;
height: 2px;
background-color: pvar(--mainColor);
opacity: 0.3;
}
@media screen and (min-width: $small-view) {
.margin-content {
max-width: 1000px;
margin:auto;
}
}
@media screen and (max-width: $small-view) {
.step-label {
width: auto;
text-align: center;
}
}

View File

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

View File

@ -1,52 +0,0 @@
<form role="form" [formGroup]="form">
<div class="channel-explanations">
<p i18n>
A channel is an entity in which you upload your videos. Creating several of them helps you to organize and separate your content.<br />
For example, you could decide to have a channel to publish your piano concerts, and another channel in which you publish your videos talking about ecology.
</p>
<p i18n>
Other users can decide to subscribe any channel they want, to be notified when you publish a new video.
</p>
</div>
<div class="form-group">
<label for="displayName" i18n>Channel display name</label>
<div class="input-group">
<input
type="text" id="displayName"
formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
>
</div>
<div *ngIf="formErrors.displayName" class="form-error">
{{ formErrors.displayName }}
</div>
</div>
<div class="form-group">
<label for="name" i18n>Channel name</label>
<div class="input-group">
<input
type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel"
formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }"
>
<div class="input-group-text">@{{ instanceHost }}</div>
</div>
<div class="name-information" i18n>
The channel name is a unique identifier of your channel on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it.
</div>
<div *ngIf="formErrors.name" class="form-error">
{{ formErrors.name }}
</div>
<div *ngIf="isSameThanUsername()" class="form-error" i18n>
Channel name cannot be the same as your account name. You can click on the first step to update your account name.
</div>
</div>
</form>

View File

@ -1,64 +0,0 @@
<form role="form" [formGroup]="form">
<div class="capability-information alert alert-info" i18n *ngIf="videoUploadDisabled">
Video uploads are disabled on this instance, hence your account won't be able to upload videos.
</div>
<div class="form-group">
<label for="displayName" i18n>Display name</label>
<div class="input-group">
<input
type="text" id="displayName" placeholder="John Doe"
formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
>
</div>
<div *ngIf="formErrors.displayName" class="form-error">
{{ formErrors.displayName }}
</div>
</div>
<div class="form-group">
<label for="username" i18n>Username</label>
<div class="input-group">
<input
type="text" id="username" i18n-placeholder="Username choice placeholder in the registration form" placeholder="e.g. jane_doe"
formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }"
>
<span class="input-group-text">@{{ instanceHost }}</span>
</div>
<div class="name-information" i18n>
The username is a unique identifier of your account on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it.
</div>
<div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }}
</div>
</div>
<div class="form-group">
<label for="email" i18n>Email</label>
<input
type="text" id="email" i18n-placeholder placeholder="Email"
formControlName="email" class="form-control" [ngClass]="{ 'input-error': formErrors['email'] }"
>
<div *ngIf="formErrors.email" class="form-error">
{{ formErrors.email }}
</div>
</div>
<div class="form-group">
<label for="password" i18n>Password</label>
<my-input-text formControlName="password" inputId="password"
i18n-placeholder placeholder="Password"
[ngClass]="{ 'input-error': formErrors['password'] }"
autocomplete="new-password"></my-input-text>
<div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }}
</div>
</div>
</form>

View File

@ -1,64 +1,121 @@
<div class="margin-content">
<div>
<div class="signup-disabled" *ngIf="signupDisabled">
<div class="alert alert-warning" i18n>Signup is not enabled on this instance.</div>
</div>
<ng-container *ngIf="!signupDisabled">
<div i18n class="title-page title-page-single">
<h1 i18n class="header-title">
<strong class="underline-orange">{{ instanceName }}</strong>
>
Create an account
</div>
</h1>
<my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success>
<div *ngIf="info" class="alert alert-info">{{ info }}</div>
<div class="register-content">
<my-custom-stepper linear>
<div class="wrapper" [hidden]="signupDone">
<div class="register-form">
<my-custom-stepper linear *ngIf="!signupDone">
<cdk-step [stepControl]="formStepTerms" i18n-label="Stepper label for the registration page describing terms of service" label="Terms">
<div class="instance-information">
<my-instance-about-accordion
(init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels"
pluginScope="signup" pluginHook="filter:signup.instance-about-plugin-panels.create.result"
></my-instance-about-accordion>
</div>
<cdk-step i18n-label label="About" [editable]="!signupSuccess">
<my-signup-step-title mascotImageName="about" i18n>
<strong>Create an account</strong>
<div>on {{ instanceName }}</div>
</my-signup-step-title>
<my-register-step-terms
[hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
[minimumAge]="minimumAge"
(formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
></my-register-step-terms>
<my-register-step-about [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about>
<div class="step-buttons">
<a i18n class="skip-step underline-orange" routerLink="/login">
<strong>I already have an account</strong>, I log in
</a>
<button i18n cdkStepperNext>I create an account</button>
</div>
</cdk-step>
<cdk-step [stepControl]="formStepTerms" i18n-label label="Terms" [editable]="!signupSuccess">
<my-signup-step-title mascotImageName="terms" i18n>
<strong>Terms</strong>
<div>of {{ instanceName }}</div>
</my-signup-step-title>
<my-instance-about-accordion
[displayInstanceName]="false"
(init)="onInstanceAboutAccordionInit($event)" [panels]="instanceInformationPanels"
pluginScope="signup" pluginHook="filter:signup.instance-about-plugin-panels.create.result"
></my-instance-about-accordion>
<my-register-step-terms
[hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
[minimumAge]="minimumAge"
(formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
></my-register-step-terms>
<div class="step-buttons">
<button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
<button cdkStepperNext [disabled]="!formStepTerms || !formStepTerms.valid">{{ defaultNextStepButtonLabel }}</button>
</cdk-step>
</div>
</cdk-step>
<cdk-step [stepControl]="formStepUser" i18n-label="Stepper label for the registration page asking user information" label="User">
<my-register-step-user (formBuilt)="onUserFormBuilt($event)" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-user>
<cdk-step [stepControl]="formStepUser" label="My account" [editable]="!signupSuccess">
<my-signup-step-title mascotImageName="account" i18n>
<strong>Setup</strong>
<div>your account</div>
</my-signup-step-title>
<my-register-step-user
(formBuilt)="onUserFormBuilt($event)"
[videoUploadDisabled]="videoUploadDisabled" [requiresEmailVerification]="requiresEmailVerification"
></my-register-step-user>
<div class="step-buttons">
<button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
<button cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid" (click)="videoUploadDisabled && signup()">{{ stepUserButtonLabel }}</button>
</cdk-step>
</div>
</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>
<cdk-step *ngIf="!videoUploadDisabled" [optional]="true" [stepControl]="formStepChannel" i18n-label label="My channel" [editable]="!signupSuccess">
<my-signup-step-title mascotImageName="channel" i18n>
<div>Create</div>
<strong>your first channel</strong>
</my-signup-step-title>
<my-register-step-channel
(formBuilt)="onChannelFormBuilt($event)"
[videoQuota]="videoQuota" [instanceName]="instanceName" [username]="getUsername()"
></my-register-step-channel>
<div class="step-buttons">
<button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
<div class="skip-step">
<span class="underline-orange" role="button" (click)="skipChannelCreation()">
<strong i18n>I don't want to create a channel</strong>
</span>
<div class="skip-step-description" i18n>You will be able to create a channel later</div>
</div>
<button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n>
Create my account
</button>
</cdk-step>
</div>
</cdk-step>
<cdk-step i18n-label label="Done" editable="false">
<div *ngIf="!signupDone && !error" class="done-loader">
<my-loader [loading]="true"></my-loader>
<cdk-step #lastStep i18n-label label="Done!" [editable]="false">
<div *ngIf="!signupSuccess && !signupError" class="done-loader">
<my-loader [loading]="true"></my-loader>
<div i18n>PeerTube is creating your account...</div>
</div>
<div i18n>PeerTube is creating your account...</div>
</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
</cdk-step>
</my-custom-stepper>
</div>
<div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div>
<my-signup-success *ngIf="signupSuccess" [requiresEmailVerification]="requiresEmailVerification"></my-signup-success>
<div *ngIf="signupError" class="steps-button">
<button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
</div>
</cdk-step>
</my-custom-stepper>
</div>
</ng-container>

View File

@ -2,7 +2,7 @@
@use '_mixins' as *;
.alert {
font-size: 15px;
font-size: 16px;
text-align: center;
}
@ -10,61 +10,75 @@
padding-top: 30vh;
}
.wrapper {
display: flex;
flex-direction: column;
.register-form {
max-width: 600px;
align-self: center;
}
.register-form,
.instance-information {
width: 100%;
}
.instance-information {
margin-bottom: 15px;
}
.header-title {
font-weight: normal;
font-size: 15px;
background-color: pvar(--mainColorVeryLight);
padding: 35px 25px 15px 25px;
margin: 0;
}
input:not([type=submit]) {
@include peertube-input-text(100%);
.register-content {
font-size: 16px;
}
my-instance-about-accordion {
display: block;
margin-bottom: 25px;
}
&#username,
&#name {
width: auto !important;
flex-grow: 1;
.step-buttons {
display: flex;
flex-wrap: wrap;
align-items: center;
.skip-step {
@include margin-right(30px);
display: inline-block;
}
.skip-step-description {
margin-top: 5px;
font-size: 14px;
}
.underline-orange {
color: pvar(--mainForegroundColor);
&:hover {
opacity: 0.8;
}
}
button,
.skip-step {
margin-top: 20px;
margin-bottom: 20px;
}
.skip-step,
button[cdkStepperNext] {
@include margin-left(auto);
}
.skip-step + button[cdkStepperNext] {
@include margin-left(0);
}
}
input[type=submit],
button {
@include peertube-button;
@include peertube-button-big;
&[cdkStepperNext] {
@include orange-button;
// Chrome does not support inline-end
float: right;
float: inline-end;
}
&[cdkStepperPrevious] {
@include grey-button;
// Chrome does not support inline-start
float: left;
float: inline-start;
}
}
.name-information {
margin-top: 10px;
}
.done-loader {
display: flex;
justify-content: center;
@ -73,13 +87,16 @@ button {
my-loader {
margin-bottom: 20px;
}
}
::ng-deep .loader div {
border-color: pvar(--mainColor) transparent transparent transparent;
}
@media screen and (max-width: $small-view) {
.step-buttons {
justify-content: space-between;
+ div {
font-size: 15px;
.skip-step,
button[cdkStepperNext] {
@include margin-left(0);
}
}
}

View File

@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core'
import { CdkStep } from '@angular/cdk/stepper'
import { Component, OnInit, ViewChild } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { ActivatedRoute } from '@angular/router'
import { AuthService } from '@app/core'
@ -15,13 +16,15 @@ import { ServerConfig } from '@shared/models/server'
styleUrls: [ './register.component.scss' ]
})
export class RegisterComponent implements OnInit {
@ViewChild('lastStep') lastStep: CdkStep
accordion: NgbAccordion
info: string = null
error: string = null
success: string = null
signupDone = false
signupError: string
signupSuccess = false
videoUploadDisabled: boolean
videoQuota: number
formStepTerms: FormGroup
formStepUser: FormGroup
@ -39,8 +42,8 @@ export class RegisterComponent implements OnInit {
moderation: false
}
defaultPreviousStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Back`
defaultNextStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Next`
defaultPreviousStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Go to the previous step`
defaultNextStepButtonLabel = $localize`:Button on the registration form to go to the previous step:Go to the next step`
stepUserButtonLabel = this.defaultNextStepButtonLabel
signupDisabled = false
@ -62,7 +65,11 @@ export class RegisterComponent implements OnInit {
return this.serverConfig.signup.minimumAge
}
ngOnInit (): void {
get instanceName () {
return this.serverConfig.instance.name
}
ngOnInit () {
this.serverConfig = this.route.snapshot.data.serverConfig
if (this.serverConfig.signup.allowed === false || this.serverConfig.signup.allowedForCurrentIP === false) {
@ -70,7 +77,9 @@ export class RegisterComponent implements OnInit {
return
}
this.videoUploadDisabled = this.serverConfig.user.videoQuota === 0
this.videoQuota = this.serverConfig.user.videoQuota
this.videoUploadDisabled = this.videoQuota === 0
this.stepUserButtonLabel = this.videoUploadDisabled
? $localize`:Button on the registration form to finalize the account and channel creation:Signup`
: this.defaultNextStepButtonLabel
@ -120,21 +129,31 @@ export class RegisterComponent implements OnInit {
this.aboutHtml = instanceAboutAccordion.aboutHtml
}
skipChannelCreation () {
this.formStepChannel.reset()
this.lastStep.select()
this.signup()
}
async signup () {
this.error = null
this.signupError = undefined
const body: UserRegister = await this.hooks.wrapObject(
Object.assign(this.formStepUser.value, { channel: this.videoUploadDisabled ? undefined : this.formStepChannel.value }),
{
...this.formStepUser.value,
channel: this.formStepChannel?.value?.name
? this.formStepChannel.value
: undefined
},
'signup',
'filter:api.signup.registration.create.params'
)
this.userSignupService.signup(body).subscribe({
next: () => {
this.signupDone = true
if (this.requiresEmailVerification) {
this.info = $localize`Now please check your emails to verify your account and complete signup.`
this.signupSuccess = true
return
}
@ -142,17 +161,17 @@ export class RegisterComponent implements OnInit {
this.authService.login(body.username, body.password)
.subscribe({
next: () => {
this.success = $localize`You are now logged in as ${body.username}!`
this.signupSuccess = true
},
error: err => {
this.error = err.message
this.signupError = err.message
}
})
},
error: err => {
this.error = err.message
this.signupError = err.message
}
})
}

View File

@ -2,15 +2,15 @@ import { CdkStepperModule } from '@angular/cdk/stepper'
import { NgModule } from '@angular/core'
import { SharedSignupModule } from '@app/+signup/shared/shared-signup.module'
import { SharedInstanceModule } from '@app/shared/shared-instance'
import { SharedMainModule } from '@app/shared/shared-main'
import { CustomStepperComponent } from './custom-stepper.component'
import { RegisterRoutingModule } from './register-routing.module'
import { RegisterStepChannelComponent } from './register-step-channel.component'
import { RegisterStepTermsComponent } from './register-step-terms.component'
import { RegisterStepUserComponent } from './register-step-user.component'
import { RegisterComponent } from './register.component'
import { RegisterStepAboutComponent, RegisterStepChannelComponent, RegisterStepTermsComponent, RegisterStepUserComponent } from './steps'
@NgModule({
imports: [
SharedMainModule,
RegisterRoutingModule,
CdkStepperModule,
@ -25,7 +25,8 @@ import { RegisterComponent } from './register.component'
CustomStepperComponent,
RegisterStepChannelComponent,
RegisterStepTermsComponent,
RegisterStepUserComponent
RegisterStepUserComponent,
RegisterStepAboutComponent
],
exports: [

View File

@ -0,0 +1,4 @@
export * from './register-step-about.component'
export * from './register-step-channel.component'
export * from './register-step-terms.component'
export * from './register-step-user.component'

View File

@ -0,0 +1,39 @@
<div class="why">
<h3 i18n>Why creating an account?</h3>
<p i18n>
As you probably noticed: creating an account is not necessary to watch video son {{ instanceName }}.
<br />
However, creating an account on {{ instanceName }} will allow you to:
</p>
<ul>
<li i18n><strong>Comment</strong> videos</li>
<li i18n><strong>Subscribe</strong> to channels to be notified of new videos</li>
<li i18n>Have access to your <strong>watch history</strong></li>
<li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li>
</ul>
</div>
<div>
<h4 i18n>You're using Mastodon, ActivityPub or a RSS feed aggregator?</h4>
<p i18n>
You can already follow {{ instanceName }} using your favorite tool.
</p>
</div>
<div class="callout callout-orange callout-light">
<div class="mascot-container" style="min-width: 140px">
<img class="mascot" width="140px" height="160px" src="/client/assets/images/mascot/happy.svg" alt="mascot"/>
</div>
<div class="callout-content">
<h4 i18>This website is a GAFAM alternative</h4>
<p i18n>
{{ instanceName }} has been created using <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">PeerTube</a>, a video creation platform developed by Framasoft.
<a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://framasoft.org">Framasoft</a> is a french non-profit organization that offers alternatives to Big Tech's digital tools
</p>
</div>
</div>

View File

@ -0,0 +1,53 @@
@use '_variables' as *;
@use '_mixins' as *;
h3 {
font-weight: $font-bold;
font-size: 24px;
}
h4 {
font-size: 18px;
font-weight: $font-bold;
}
.why {
margin-bottom: 30px;
}
.callout {
margin: 75px auto 25px;
border-width: 2px;
display: flex;
.mascot-container {
position: relative;
.mascot {
position: absolute;
top: -65px;
}
}
.callout-content {
margin-left: 30px;
p {
margin: 0;
}
}
}
@media screen and (max-width: $small-view) {
.callout {
margin-top: 20px;
.mascot-container {
display: none;
}
.callout-content {
margin-left: 0;
}
}
}

View File

@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core'
import { ServerService } from '@app/core'
@Component({
selector: 'my-register-step-about',
templateUrl: './register-step-about.component.html',
styleUrls: [ './register-step-about.component.scss' ]
})
export class RegisterStepAboutComponent {
@Input() videoUploadDisabled: boolean
constructor (private serverService: ServerService) {
}
get instanceName () {
return this.serverService.getHTMLConfig().instance.name
}
}

View File

@ -0,0 +1,55 @@
<div class="mb-5">
<p i18n>
You want to <strong>publish videos</strong> on {{ instanceName }}? Then you need to create your first <strong>channel</strong>.
</p>
<p i18n>
You might want to <strong>create a channel by theme:</strong> for example, you can create a channel named "SweetMelodies"
to publish your piano concerts and another one "Ecology" in which you publish your videos talking about ecology.
</p>
<p i18n *ngIf="videoQuota !== -1">
{{ instanceName }} administrators allow you to publish up to <strong>{{ videoQuota | bytes: 0 }} of videos</strong> on their website.
</p>
</div>
<form role="form" [formGroup]="form">
<div class="row">
<div class="col-md-12 col-xl-6 form-group">
<label for="displayName" i18n>Channel display name</label>
<div i18n class="form-group-description">This is the name that will be publicly visible by other users.</div>
<div class="input-group">
<input
type="text" id="displayName" i18n-placeholder placeholder="Example: Sweet Melodies"
formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
>
</div>
<div *ngIf="formErrors.displayName" class="form-error">{{ formErrors.displayName }}</div>
</div>
<div class="col-md-12 col-xl-6 form-group">
<label for="name" i18n>Channel identifier</label>
<div i18n class="form-group-description">This is the name that will be displayed in your profile URL.</div>
<div class="input-group">
<input
type="text" id="name" i18n-placeholder placeholder="Example: sweetmelodies24"
formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }"
>
<div class="input-group-text">@{{ instanceHost }}</div>
</div>
<div *ngIf="formErrors.name" class="form-error">{{ formErrors.name }}</div>
<div *ngIf="isSameThanUsername()" class="form-error" i18n>
Channel identifier cannot be the same as your account name. You can click on the first step to update your account name.
</div>
</div>
</div>
</form>

View File

@ -9,10 +9,13 @@ import { UserSignupService } from '@app/shared/shared-users'
@Component({
selector: 'my-register-step-channel',
templateUrl: './register-step-channel.component.html',
styleUrls: [ './register.component.scss' ]
styleUrls: [ './step.component.scss' ]
})
export class RegisterStepChannelComponent extends FormReactive implements OnInit {
@Input() username: string
@Input() instanceName: string
@Input() videoQuota: number
@Output() formBuilt = new EventEmitter<FormGroup>()
constructor (

View File

@ -4,15 +4,13 @@
<ng-template ptTemplate="label">
<ng-container i18n>
I am at least {{ minimumAge }} years old and agree
to the <a class="terms-anchor" (click)="onTermsClick($event)" href='#'>Terms</a>
to the <a class="link-orange" (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 *ngIf="formErrors.terms" class="form-error">{{ formErrors.terms }}</div>
</div>
</form>

View File

@ -8,7 +8,7 @@ import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
@Component({
selector: 'my-register-step-terms',
templateUrl: './register-step-terms.component.html',
styleUrls: [ './register.component.scss' ]
styleUrls: [ './step.component.scss' ]
})
export class RegisterStepTermsComponent extends FormReactive implements OnInit {
@Input() hasCodeOfConduct = false

View File

@ -0,0 +1,73 @@
<div class="alert pt-alert-primary" i18n *ngIf="videoUploadDisabled">
Video uploads are disabled on this instance, hence your account won't be able to upload videos.
</div>
<form role="form" [formGroup]="form">
<div class="row">
<div class="col-md-12 col-xl-6 form-group">
<label for="displayName" i18n>Public name</label>
<div class="form-group-description" i18n>
This is the name that will be publicly visible by other users.
</div>
<div class="input-group">
<input
type="text" id="displayName" i18n-placeholder placeholder="Example: John Doe"
formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
>
</div>
<div *ngIf="formErrors.displayName" class="form-error">{{ formErrors.displayName }}</div>
</div>
<div class="col-md-12 col-xl-6 form-group">
<label for="username" i18n>Username</label>
<div class="form-group-description" i18n>
This is the name that will be displayed in your profile URL.
</div>
<div class="input-group">
<input
type="text" id="username" i18n-placeholder placeholder="Example: john_doe58"
formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }"
>
<span class="input-group-text">@{{ instanceHost }}</span>
</div>
<div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-xl-6 form-group">
<label for="email" i18n>Email</label>
<div *ngIf="requiresEmailVerification" class="form-group-description" i18n>
This email address will be used to validate your account.
</div>
<input
type="text" id="email" i18n-placeholder placeholder="Example: john@example.com"
formControlName="email" class="form-control" [ngClass]="{ 'input-error': formErrors['email'] }"
>
<div *ngIf="formErrors.email" class="form-error">{{ formErrors.email }}</div>
</div>
<div class="col-md-12 col-xl-6 form-group">
<label for="password" i18n>Password</label>
<div class="form-group-description">{{ getMinPasswordLengthMessage() }}</div>
<my-input-text
formControlName="password" inputId="password"
[ngClass]="{ 'input-error': formErrors['password'] }" autocomplete="new-password"
></my-input-text>
<div *ngIf="formErrors.password" class="form-error">{{ formErrors.password }}</div>
</div>
</div>
</form>

View File

@ -14,10 +14,11 @@ import { UserSignupService } from '@app/shared/shared-users'
@Component({
selector: 'my-register-step-user',
templateUrl: './register-step-user.component.html',
styleUrls: [ './register.component.scss' ]
styleUrls: [ './step.component.scss' ]
})
export class RegisterStepUserComponent extends FormReactive implements OnInit {
@Input() videoUploadDisabled = false
@Input() requiresEmailVerification = false
@Output() formBuilt = new EventEmitter<FormGroup>()
@ -49,6 +50,10 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
.subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
}
getMinPasswordLengthMessage () {
return USER_PASSWORD_VALIDATOR.MESSAGES.minlength
}
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
const username = this.form.value['username'] || ''

View File

@ -0,0 +1,27 @@
@use '_variables' as *;
@use '_mixins' as *;
input:not([type=submit]) {
@include peertube-input-text(100%);
display: block;
&#username,
&#name {
width: auto !important;
flex-grow: 1;
}
}
input[type=submit],
button {
@include peertube-button;
}
label {
font-size: 18px;
margin-bottom: 5px;
}
.row {
margin-bottom: 30px;
}

View File

@ -3,7 +3,7 @@
Verify account email confirmation
</div>
<my-signup-success i18n *ngIf="!isPendingEmail && success" message="Your email has been verified and you may now login.">
<my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false">
</my-signup-success>
<div i18n class="alert alert-success" *ngIf="isPendingEmail && success">

View File

@ -3,6 +3,8 @@ import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
import { SharedUsersModule } from '@app/shared/shared-users'
import { SignupMascotComponent } from './signup-mascot.component'
import { SignupStepTitleComponent } from './signup-step-title.component'
import { SignupSuccessComponent } from './signup-success.component'
@NgModule({
@ -14,7 +16,9 @@ import { SignupSuccessComponent } from './signup-success.component'
],
declarations: [
SignupSuccessComponent
SignupSuccessComponent,
SignupStepTitleComponent,
SignupMascotComponent
],
exports: [
@ -22,7 +26,9 @@ import { SignupSuccessComponent } from './signup-success.component'
SharedFormModule,
SharedGlobalIconModule,
SignupSuccessComponent
SignupSuccessComponent,
SignupStepTitleComponent,
SignupMascotComponent
],
providers: [

View File

@ -0,0 +1,11 @@
@use '_variables' as *;
@use '_mixins' as *;
.root {
display: inline-block;
width: 270px;
}
div ::ng-deep svg {
color: pvar(--mainColor);
}

View File

@ -0,0 +1,29 @@
import { Component, Input } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser'
const images = {
about: require('!!raw-loader?!../../../assets/images/mascot/register/about.svg').default,
terms: require('!!raw-loader?!../../../assets/images/mascot/register/terms.svg').default,
success: require('!!raw-loader?!../../../assets/images/mascot/register/success.svg').default,
channel: require('!!raw-loader?!../../../assets/images/mascot/register/channel.svg').default,
account: require('!!raw-loader?!../../../assets/images/mascot/register/account.svg').default
}
export type MascotImageName = keyof typeof images
@Component({
selector: 'my-signup-mascot',
styleUrls: [ './signup-mascot.component.scss' ],
template: `<div class="root" [innerHTML]="html"></div>`
})
export class SignupMascotComponent {
@Input() imageName: MascotImageName
constructor (private sanitize: DomSanitizer) {
}
get html () {
return this.sanitize.bypassSecurityTrustHtml(images[this.imageName])
}
}

View File

@ -0,0 +1,9 @@
<div class="step-content-title">
<my-signup-mascot [imageName]="mascotImageName"></my-signup-mascot>
<h2>
<ng-content></ng-content>
</h2>
<div class="step-content-title-separator"></div>
</div>

View File

@ -0,0 +1,23 @@
@use '_variables' as *;
@use '_mixins' as *;
.step-content-title {
text-align: center;
margin: auto;
margin-bottom: 45px;
h2 {
font-size: 32px;
font-weight: normal;
max-width: 300px;
margin: 15px auto 0;
}
}
.step-content-title-separator {
height: 6px;
width: 60px;
border-radius: 4px;
background-color: pvar(--mainColor);
margin: 5px auto 0;
}

View File

@ -0,0 +1,12 @@
import { Component, Input } from '@angular/core'
import { MascotImageName } from './signup-mascot.component'
@Component({
selector: 'my-signup-step-title',
templateUrl: './signup-step-title.component.html',
styleUrls: [ './signup-step-title.component.scss' ]
})
export class SignupStepTitleComponent {
@Input() mascotImageName: MascotImageName
}

View File

@ -1,20 +1,22 @@
<!-- Thanks: Amit Singh Sansoya from https://codepen.io/amit3200/pen/zWMJOO -->
<my-signup-step-title mascotImageName="success" i18n>
<strong>Welcome</strong>
<div>on {{ instanceName }}</div>
</my-signup-step-title>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2">
<circle class="path circle" fill="none" stroke="#73AF55" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"/>
<polyline class="path check" fill="none" stroke="#73AF55" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "/>
</svg>
<div class="alert pt-alert-primary">
<p i18n>Your account has been created!</p>
<p i18n class="bottom-message">Welcome to PeerTube!</p>
<div *ngIf="message" class="alert alert-success">
<p>{{ message }}</p>
<p i18n>
If you need help to use PeerTube, you can have a look at the <a href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
<p i18n *ngIf="requiresEmailVerification">
<strong>Check your emails</strong> to validate your account and complete your inscription.
</p>
<p i18n>
To help moderators and other users to know <strong>who you are</strong>, don't forget to <a routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>.
</p>
<ng-container *ngIf="!requiresEmailVerification">
<p i18n>
If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
</p>
<p i18n>
To help moderators and other users to know <strong>who you are</strong>, don't forget to <a class="link-orange" routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>.
</p>
</ng-container>
</div>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 957 B

View File

@ -1,54 +1,6 @@
svg {
width: 100px;
display: block;
margin: 40px auto 0;
}
.path {
stroke-dasharray: 1000;
stroke-dashoffset: 0;
&.circle {
animation: dash .9s ease-in-out;
}
&.line {
stroke-dashoffset: 1000;
animation: dash .9s .35s ease-in-out forwards;
}
&.check {
stroke-dashoffset: -100;
animation: dash-check .9s .35s ease-in-out forwards;
}
}
.bottom-message {
text-align: center;
margin: 20px 0 60px;
font-size: 1.25em;
color: #73AF55;
}
.alert {
font-size: 15px;
font-size: 18px;
max-width: 900px;
text-align: center;
}
@keyframes dash {
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
}
@keyframes dash-check {
0% {
stroke-dashoffset: -100;
}
100% {
stroke-dashoffset: 900;
}
margin: auto;
}

View File

@ -1,4 +1,5 @@
import { Component, Input } from '@angular/core'
import { ServerService } from '@app/core'
@Component({
selector: 'my-signup-success',
@ -6,5 +7,13 @@ import { Component, Input } from '@angular/core'
styleUrls: [ './signup-success.component.scss' ]
})
export class SignupSuccessComponent {
@Input() message: string
@Input() requiresEmailVerification: boolean
constructor (private serverService: ServerService) {
}
get instanceName () {
return this.serverService.getHTMLConfig().instance.name
}
}

View File

@ -61,7 +61,7 @@ export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = {
}
}
export const USER_PASSWORD_VALIDATOR: BuildFormValidator = {
export const USER_PASSWORD_VALIDATOR = {
VALIDATORS: [
Validators.required,
Validators.minLength(6),

View File

@ -1,6 +1,6 @@
<h2 class="instance-name">{{ about?.instance.name }}</h2>
<h2 *ngIf="displayInstanceName" class="instance-name">{{ about?.instance.name }}</h2>
<div class="instance-short-description">{{ about?.instance.shortDescription }}</div>
<div *ngIf="displayInstanceShortDescription" 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">
@ -32,7 +32,7 @@
</ng-template>
</ngb-panel>
<ngb-panel *ngIf="termsPanel" id="terms" i18n-title title="Terms">
<ngb-panel *ngIf="termsPanel" id="terms" [title]="getTermsTitle()">
<ng-template ngbPanelContent>
<div class="block" [innerHTML]="aboutHtml.terms"></div>
</ng-template>

View File

@ -8,8 +8,7 @@
.instance-short-description {
@include ellipsis-multiline(1rem, 3);
margin-top: 20px;
margin-bottom: 20px;
margin: 25px 0;
}
.block {

View File

@ -15,6 +15,9 @@ export class InstanceAboutAccordionComponent implements OnInit {
@Output() init: EventEmitter<InstanceAboutAccordionComponent> = new EventEmitter<InstanceAboutAccordionComponent>()
@Input() displayInstanceName = true
@Input() displayInstanceShortDescription = true
@Input() pluginScope: PluginClientScope
@Input() pluginHook: ClientFilterHookName
@ -66,6 +69,10 @@ export class InstanceAboutAccordionComponent implements OnInit {
return !!(this.aboutHtml?.administrator || this.about?.instance.maintenanceLifetime || this.about?.instance.businessModel)
}
getTermsTitle () {
return $localize`Terms of ${this.about.instance.name}`
}
get moderationPanel () {
return this.panels.moderation && !!this.aboutHtml.moderationInformation
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -26,6 +26,7 @@ body {
--mainColor: #{$main-color};
--mainColorLighter: #{$main-color-lighter};
--mainColorLightest: #{$main-color-lightest};
--mainColorVeryLight: #{$main-color-very-light};
--mainHoverColor: #{$main-hover-color};
--mainBackgroundHoverColor: #{$main-background-hover-color};

View File

@ -3,6 +3,24 @@
@use '_badges' as *;
@use '_icons' as *;
.link-orange {
color: pvar(--mainForegroundColor);
font-weight: $font-bold;
border-bottom: 3px solid pvar(--mainColor);
&:hover {
color: pvar(--mainForegroundColor);
opacity: 0.8;
}
}
.underline-orange {
display: inline-block;
border-bottom: 3px solid pvar(--mainColor);
}
// ---------------------------------------------------------------------------
.peertube-button {
@include peertube-button;
}
@ -70,6 +88,11 @@
margin-top: 10px;
}
label + .form-group-description {
margin-bottom: 10px;
margin-top: 0;
}
// ---------------------------------------------------------------------------
@ -192,9 +215,12 @@
border-left-width: .25rem;
}
&.callout-info {
&.callout-orange {
border-color: pvar(--mainColorLightest);
border-left-color: pvar(--mainColor);
&:not(.callout-light) {
border-left-color: pvar(--mainColor);
}
}
}
@ -210,3 +236,16 @@
top: #{-($header-height + $sub-menu-height + 20px)};
}
}
// ---------------------------------------------------------------------------
.alert {
p:last-child {
margin-bottom: 0;
}
&.pt-alert-primary {
background-color: pvar(--mainColorVeryLight);
border: 2px solid pvar(--mainColorLightest);
}
}

View File

@ -46,5 +46,5 @@ $dropdown-border-radius: 3px;
$dropdown-link-active-color: pvar(--mainForegroundColor);
$dropdown-link-active-bg: pvar(--mainBackgroundHoverColor);
$accordion-button-active-bg: pvar(--mainColorLightest);
$accordion-button-active-bg: pvar(--mainColorVeryLight);
$accordion-button-active-color: pvar(--mainForegroundColor);

View File

@ -264,6 +264,18 @@
}
}
@mixin peertube-button-big {
height: auto;
padding: 10px 25px;
font-size: 18px;
line-height: 1.2;
border: 0;
font-weight: $font-semibold;
// Because of primeng that redefines border-radius of all input[type="..."]
border-radius: 3px !important;
}
@mixin peertube-button-link {
@include disable-default-a-behaviour;
@include peertube-button;

View File

@ -1,4 +1,5 @@
@use 'sass:math';
@use 'sass:color';
@use '~bootstrap/scss/functions' as *;
$small-view: 800px;
@ -14,11 +15,12 @@ $grey-background-color: #E5E5E5;
$grey-background-hover-color: #EFEFEF;
$grey-foreground-color: #585858;
$grey-foreground-hover-color: #303030;
$grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%);
$grey-button-outline-color: color.scale($grey-foreground-color, $alpha: -95%);
$main-color: hsl(24, 90%, 50%);
$main-color-lighter: lighten($main-color, 10%);
$main-color-lightest: lighten($main-color, 40%);
$main-color-very-light: #fff5eb;
$main-hover-color: lighten($main-color, 5%);
$main-background-hover-color: #e9ecef;
@ -109,6 +111,7 @@ $variables: (
--mainColor: var(--mainColor),
--mainColorLighter: var(--mainColorLighter),
--mainColorLightest: var(--mainColorLightest),
--mainColorVeryLight: var(--mainColorVeryLight),
--mainHoverColor: var(--mainHoverColor),
--mainBackgroundHoverColor: var(--mainBackgroundHoverColor),