diff options
Diffstat (limited to 'client/src/app/+signup/+register/steps')
11 files changed, 454 insertions, 0 deletions
diff --git a/client/src/app/+signup/+register/steps/index.ts b/client/src/app/+signup/+register/steps/index.ts new file mode 100644 index 000000000..b5eae7468 --- /dev/null +++ b/client/src/app/+signup/+register/steps/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './register-step-about.component' | ||
2 | export * from './register-step-channel.component' | ||
3 | export * from './register-step-terms.component' | ||
4 | export * from './register-step-user.component' | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.html b/client/src/app/+signup/+register/steps/register-step-about.component.html new file mode 100644 index 000000000..f93de8ce9 --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-about.component.html | |||
@@ -0,0 +1,39 @@ | |||
1 | <div class="why"> | ||
2 | <h3 i18n>Why creating an account?</h3> | ||
3 | |||
4 | <p i18n> | ||
5 | As you probably noticed: creating an account is not necessary to watch video son {{ instanceName }}. | ||
6 | <br /> | ||
7 | However, creating an account on {{ instanceName }} will allow you to: | ||
8 | </p> | ||
9 | |||
10 | <ul> | ||
11 | <li i18n><strong>Comment</strong> videos</li> | ||
12 | <li i18n><strong>Subscribe</strong> to channels to be notified of new videos</li> | ||
13 | <li i18n>Have access to your <strong>watch history</strong></li> | ||
14 | <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> | ||
15 | </ul> | ||
16 | </div> | ||
17 | |||
18 | <div> | ||
19 | <h4 i18n>You're using Mastodon, ActivityPub or a RSS feed aggregator?</h4> | ||
20 | |||
21 | <p i18n> | ||
22 | You can already follow {{ instanceName }} using your favorite tool. | ||
23 | </p> | ||
24 | </div> | ||
25 | |||
26 | <div class="callout callout-orange callout-light"> | ||
27 | <div class="mascot-container" style="min-width: 140px"> | ||
28 | <img class="mascot" width="140px" height="160px" src="/client/assets/images/mascot/happy.svg" alt="mascot"/> | ||
29 | </div> | ||
30 | |||
31 | <div class="callout-content"> | ||
32 | <h4 i18>This website is a GAFAM alternative</h4> | ||
33 | |||
34 | <p i18n> | ||
35 | {{ 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. | ||
36 | <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 | ||
37 | </p> | ||
38 | </div> | ||
39 | </div> | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.scss b/client/src/app/+signup/+register/steps/register-step-about.component.scss new file mode 100644 index 000000000..ab6d6dd4d --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-about.component.scss | |||
@@ -0,0 +1,53 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | h3 { | ||
5 | font-weight: $font-bold; | ||
6 | font-size: 24px; | ||
7 | } | ||
8 | |||
9 | h4 { | ||
10 | font-size: 18px; | ||
11 | font-weight: $font-bold; | ||
12 | } | ||
13 | |||
14 | .why { | ||
15 | margin-bottom: 30px; | ||
16 | } | ||
17 | |||
18 | .callout { | ||
19 | margin: 75px auto 25px; | ||
20 | border-width: 2px; | ||
21 | display: flex; | ||
22 | |||
23 | .mascot-container { | ||
24 | position: relative; | ||
25 | |||
26 | .mascot { | ||
27 | position: absolute; | ||
28 | top: -65px; | ||
29 | } | ||
30 | } | ||
31 | |||
32 | .callout-content { | ||
33 | margin-left: 30px; | ||
34 | |||
35 | p { | ||
36 | margin: 0; | ||
37 | } | ||
38 | } | ||
39 | } | ||
40 | |||
41 | @media screen and (max-width: $small-view) { | ||
42 | .callout { | ||
43 | margin-top: 20px; | ||
44 | |||
45 | .mascot-container { | ||
46 | display: none; | ||
47 | } | ||
48 | |||
49 | .callout-content { | ||
50 | margin-left: 0; | ||
51 | } | ||
52 | } | ||
53 | } | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.ts b/client/src/app/+signup/+register/steps/register-step-about.component.ts new file mode 100644 index 000000000..9a0941016 --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-about.component.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { ServerService } from '@app/core' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-register-step-about', | ||
6 | templateUrl: './register-step-about.component.html', | ||
7 | styleUrls: [ './register-step-about.component.scss' ] | ||
8 | }) | ||
9 | export class RegisterStepAboutComponent { | ||
10 | @Input() videoUploadDisabled: boolean | ||
11 | |||
12 | constructor (private serverService: ServerService) { | ||
13 | |||
14 | } | ||
15 | |||
16 | get instanceName () { | ||
17 | return this.serverService.getHTMLConfig().instance.name | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.html b/client/src/app/+signup/+register/steps/register-step-channel.component.html new file mode 100644 index 000000000..c79256c68 --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-channel.component.html | |||
@@ -0,0 +1,55 @@ | |||
1 | <div class="mb-5"> | ||
2 | <p i18n> | ||
3 | You want to <strong>publish videos</strong> on {{ instanceName }}? Then you need to create your first <strong>channel</strong>. | ||
4 | </p> | ||
5 | |||
6 | <p i18n> | ||
7 | You might want to <strong>create a channel by theme:</strong> for example, you can create a channel named "SweetMelodies" | ||
8 | to publish your piano concerts and another one "Ecology" in which you publish your videos talking about ecology. | ||
9 | </p> | ||
10 | |||
11 | <p i18n *ngIf="videoQuota !== -1"> | ||
12 | {{ instanceName }} administrators allow you to publish up to <strong>{{ videoQuota | bytes: 0 }} of videos</strong> on their website. | ||
13 | </p> | ||
14 | </div> | ||
15 | |||
16 | <form role="form" [formGroup]="form"> | ||
17 | |||
18 | <div class="row"> | ||
19 | |||
20 | <div class="col-md-12 col-xl-6 form-group"> | ||
21 | <label for="displayName" i18n>Channel display name</label> | ||
22 | |||
23 | <div i18n class="form-group-description">This is the name that will be publicly visible by other users.</div> | ||
24 | |||
25 | <div class="input-group"> | ||
26 | <input | ||
27 | type="text" id="displayName" i18n-placeholder placeholder="Example: Sweet Melodies" | ||
28 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
29 | > | ||
30 | </div> | ||
31 | |||
32 | <div *ngIf="formErrors.displayName" class="form-error">{{ formErrors.displayName }}</div> | ||
33 | </div> | ||
34 | |||
35 | <div class="col-md-12 col-xl-6 form-group"> | ||
36 | <label for="name" i18n>Channel identifier</label> | ||
37 | |||
38 | <div i18n class="form-group-description">This is the name that will be displayed in your profile URL.</div> | ||
39 | |||
40 | <div class="input-group"> | ||
41 | <input | ||
42 | type="text" id="name" i18n-placeholder placeholder="Example: sweetmelodies24" | ||
43 | formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" | ||
44 | > | ||
45 | <div class="input-group-text">@{{ instanceHost }}</div> | ||
46 | </div> | ||
47 | |||
48 | <div *ngIf="formErrors.name" class="form-error">{{ formErrors.name }}</div> | ||
49 | |||
50 | <div *ngIf="isSameThanUsername()" class="form-error" i18n> | ||
51 | Channel identifier cannot be the same as your account name. You can click on the first step to update your account name. | ||
52 | </div> | ||
53 | </div> | ||
54 | </div> | ||
55 | </form> | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.ts b/client/src/app/+signup/+register/steps/register-step-channel.component.ts new file mode 100644 index 000000000..c10b568ba --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-channel.component.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | import { concat, of } from 'rxjs' | ||
2 | import { pairwise } from 'rxjs/operators' | ||
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
4 | import { FormGroup } from '@angular/forms' | ||
5 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' | ||
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
7 | import { UserSignupService } from '@app/shared/shared-users' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-register-step-channel', | ||
11 | templateUrl: './register-step-channel.component.html', | ||
12 | styleUrls: [ './step.component.scss' ] | ||
13 | }) | ||
14 | export class RegisterStepChannelComponent extends FormReactive implements OnInit { | ||
15 | @Input() username: string | ||
16 | @Input() instanceName: string | ||
17 | @Input() videoQuota: number | ||
18 | |||
19 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
20 | |||
21 | constructor ( | ||
22 | protected formValidatorService: FormValidatorService, | ||
23 | private userSignupService: UserSignupService | ||
24 | ) { | ||
25 | super() | ||
26 | } | ||
27 | |||
28 | get instanceHost () { | ||
29 | return window.location.host | ||
30 | } | ||
31 | |||
32 | ngOnInit () { | ||
33 | this.buildForm({ | ||
34 | displayName: VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, | ||
35 | name: VIDEO_CHANNEL_NAME_VALIDATOR | ||
36 | }) | ||
37 | |||
38 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
39 | |||
40 | concat( | ||
41 | of(''), | ||
42 | this.form.get('displayName').valueChanges | ||
43 | ).pipe(pairwise()) | ||
44 | .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) | ||
45 | } | ||
46 | |||
47 | isSameThanUsername () { | ||
48 | return this.username && this.username === this.form.value['name'] | ||
49 | } | ||
50 | |||
51 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | ||
52 | const name = this.form.value['name'] || '' | ||
53 | |||
54 | const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name) | ||
55 | this.form.patchValue({ name: newName }) | ||
56 | } | ||
57 | } | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.html b/client/src/app/+signup/+register/steps/register-step-terms.component.html new file mode 100644 index 000000000..f54ca77e2 --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.html | |||
@@ -0,0 +1,16 @@ | |||
1 | <form role="form" [formGroup]="form"> | ||
2 | <div class="form-group"> | ||
3 | <my-peertube-checkbox inputName="terms" formControlName="terms"> | ||
4 | <ng-template ptTemplate="label"> | ||
5 | <ng-container i18n> | ||
6 | I am at least {{ minimumAge }} years old and agree | ||
7 | to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> | ||
8 | <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> | ||
9 | of this instance | ||
10 | </ng-container> | ||
11 | </ng-template> | ||
12 | </my-peertube-checkbox> | ||
13 | |||
14 | <div *ngIf="formErrors.terms" class="form-error">{{ formErrors.terms }}</div> | ||
15 | </div> | ||
16 | </form> | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.ts b/client/src/app/+signup/+register/steps/register-step-terms.component.ts new file mode 100644 index 000000000..87d16696e --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import { FormGroup } from '@angular/forms' | ||
3 | import { | ||
4 | USER_TERMS_VALIDATOR | ||
5 | } from '@app/shared/form-validators/user-validators' | ||
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-register-step-terms', | ||
10 | templateUrl: './register-step-terms.component.html', | ||
11 | styleUrls: [ './step.component.scss' ] | ||
12 | }) | ||
13 | export class RegisterStepTermsComponent extends FormReactive implements OnInit { | ||
14 | @Input() hasCodeOfConduct = false | ||
15 | @Input() minimumAge = 16 | ||
16 | |||
17 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
18 | @Output() termsClick = new EventEmitter<void>() | ||
19 | @Output() codeOfConductClick = new EventEmitter<void>() | ||
20 | |||
21 | constructor ( | ||
22 | protected formValidatorService: FormValidatorService | ||
23 | ) { | ||
24 | super() | ||
25 | } | ||
26 | |||
27 | get instanceHost () { | ||
28 | return window.location.host | ||
29 | } | ||
30 | |||
31 | ngOnInit () { | ||
32 | this.buildForm({ | ||
33 | terms: USER_TERMS_VALIDATOR | ||
34 | }) | ||
35 | |||
36 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
37 | } | ||
38 | |||
39 | onTermsClick (event: Event) { | ||
40 | event.preventDefault() | ||
41 | this.termsClick.emit() | ||
42 | } | ||
43 | |||
44 | onCodeOfConductClick (event: Event) { | ||
45 | event.preventDefault() | ||
46 | this.codeOfConductClick.emit() | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.html b/client/src/app/+signup/+register/steps/register-step-user.component.html new file mode 100644 index 000000000..bffcf0346 --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-user.component.html | |||
@@ -0,0 +1,73 @@ | |||
1 | <div class="alert pt-alert-primary" i18n *ngIf="videoUploadDisabled"> | ||
2 | Video uploads are disabled on this instance, hence your account won't be able to upload videos. | ||
3 | </div> | ||
4 | |||
5 | <form role="form" [formGroup]="form"> | ||
6 | <div class="row"> | ||
7 | |||
8 | <div class="col-md-12 col-xl-6 form-group"> | ||
9 | <label for="displayName" i18n>Public name</label> | ||
10 | |||
11 | <div class="form-group-description" i18n> | ||
12 | This is the name that will be publicly visible by other users. | ||
13 | </div> | ||
14 | |||
15 | <div class="input-group"> | ||
16 | <input | ||
17 | type="text" id="displayName" i18n-placeholder placeholder="Example: John Doe" | ||
18 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
19 | > | ||
20 | </div> | ||
21 | |||
22 | <div *ngIf="formErrors.displayName" class="form-error">{{ formErrors.displayName }}</div> | ||
23 | </div> | ||
24 | |||
25 | <div class="col-md-12 col-xl-6 form-group"> | ||
26 | <label for="username" i18n>Username</label> | ||
27 | |||
28 | <div class="form-group-description" i18n> | ||
29 | This is the name that will be displayed in your profile URL. | ||
30 | </div> | ||
31 | |||
32 | <div class="input-group"> | ||
33 | <input | ||
34 | type="text" id="username" i18n-placeholder placeholder="Example: john_doe58" | ||
35 | formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" | ||
36 | > | ||
37 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
38 | </div> | ||
39 | |||
40 | <div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div> | ||
41 | </div> | ||
42 | </div> | ||
43 | |||
44 | <div class="row"> | ||
45 | <div class="col-md-12 col-xl-6 form-group"> | ||
46 | <label for="email" i18n>Email</label> | ||
47 | |||
48 | <div *ngIf="requiresEmailVerification" class="form-group-description" i18n> | ||
49 | This email address will be used to validate your account. | ||
50 | </div> | ||
51 | |||
52 | <input | ||
53 | type="text" id="email" i18n-placeholder placeholder="Example: john@example.com" | ||
54 | formControlName="email" class="form-control" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
55 | > | ||
56 | |||
57 | <div *ngIf="formErrors.email" class="form-error">{{ formErrors.email }}</div> | ||
58 | </div> | ||
59 | |||
60 | <div class="col-md-12 col-xl-6 form-group"> | ||
61 | <label for="password" i18n>Password</label> | ||
62 | |||
63 | <div class="form-group-description">{{ getMinPasswordLengthMessage() }}</div> | ||
64 | |||
65 | <my-input-text | ||
66 | formControlName="password" inputId="password" | ||
67 | [ngClass]="{ 'input-error': formErrors['password'] }" autocomplete="new-password" | ||
68 | ></my-input-text> | ||
69 | |||
70 | <div *ngIf="formErrors.password" class="form-error">{{ formErrors.password }}</div> | ||
71 | </div> | ||
72 | </div> | ||
73 | </form> | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.ts b/client/src/app/+signup/+register/steps/register-step-user.component.ts new file mode 100644 index 000000000..b89e38a28 --- /dev/null +++ b/client/src/app/+signup/+register/steps/register-step-user.component.ts | |||
@@ -0,0 +1,63 @@ | |||
1 | import { concat, of } from 'rxjs' | ||
2 | import { pairwise } from 'rxjs/operators' | ||
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
4 | import { FormGroup } from '@angular/forms' | ||
5 | import { | ||
6 | USER_DISPLAY_NAME_REQUIRED_VALIDATOR, | ||
7 | USER_EMAIL_VALIDATOR, | ||
8 | USER_PASSWORD_VALIDATOR, | ||
9 | USER_USERNAME_VALIDATOR | ||
10 | } from '@app/shared/form-validators/user-validators' | ||
11 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
12 | import { UserSignupService } from '@app/shared/shared-users' | ||
13 | |||
14 | @Component({ | ||
15 | selector: 'my-register-step-user', | ||
16 | templateUrl: './register-step-user.component.html', | ||
17 | styleUrls: [ './step.component.scss' ] | ||
18 | }) | ||
19 | export class RegisterStepUserComponent extends FormReactive implements OnInit { | ||
20 | @Input() videoUploadDisabled = false | ||
21 | @Input() requiresEmailVerification = false | ||
22 | |||
23 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
24 | |||
25 | constructor ( | ||
26 | protected formValidatorService: FormValidatorService, | ||
27 | private userSignupService: UserSignupService | ||
28 | ) { | ||
29 | super() | ||
30 | } | ||
31 | |||
32 | get instanceHost () { | ||
33 | return window.location.host | ||
34 | } | ||
35 | |||
36 | ngOnInit () { | ||
37 | this.buildForm({ | ||
38 | displayName: USER_DISPLAY_NAME_REQUIRED_VALIDATOR, | ||
39 | username: USER_USERNAME_VALIDATOR, | ||
40 | password: USER_PASSWORD_VALIDATOR, | ||
41 | email: USER_EMAIL_VALIDATOR | ||
42 | }) | ||
43 | |||
44 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
45 | |||
46 | concat( | ||
47 | of(''), | ||
48 | this.form.get('displayName').valueChanges | ||
49 | ).pipe(pairwise()) | ||
50 | .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) | ||
51 | } | ||
52 | |||
53 | getMinPasswordLengthMessage () { | ||
54 | return USER_PASSWORD_VALIDATOR.MESSAGES.minlength | ||
55 | } | ||
56 | |||
57 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | ||
58 | const username = this.form.value['username'] || '' | ||
59 | |||
60 | const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username) | ||
61 | this.form.patchValue({ username: newUsername }) | ||
62 | } | ||
63 | } | ||
diff --git a/client/src/app/+signup/+register/steps/step.component.scss b/client/src/app/+signup/+register/steps/step.component.scss new file mode 100644 index 000000000..35cfdae91 --- /dev/null +++ b/client/src/app/+signup/+register/steps/step.component.scss | |||
@@ -0,0 +1,27 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | input:not([type=submit]) { | ||
5 | @include peertube-input-text(100%); | ||
6 | display: block; | ||
7 | |||
8 | &#username, | ||
9 | &#name { | ||
10 | width: auto !important; | ||
11 | flex-grow: 1; | ||
12 | } | ||
13 | } | ||
14 | |||
15 | input[type=submit], | ||
16 | button { | ||
17 | @include peertube-button; | ||
18 | } | ||
19 | |||
20 | label { | ||
21 | font-size: 18px; | ||
22 | margin-bottom: 5px; | ||
23 | } | ||
24 | |||
25 | .row { | ||
26 | margin-bottom: 30px; | ||
27 | } | ||