mirror of
https://framagit.org/framasoft/framadate/funky-framadate-front.git
synced 2023-08-25 13:53:14 +02:00
Merge branch 'creation-form'
This commit is contained in:
commit
b483cc520e
1
CHANGELOG.md
Normal file
1
CHANGELOG.md
Normal file
@ -0,0 +1 @@
|
||||
# Changelog
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -6515,6 +6515,11 @@
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"crypto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
|
||||
},
|
||||
"crypto-browserify": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"build:prod": "ng build --prod",
|
||||
"build:prod:stats": "ng build --prod --stats-json",
|
||||
"build:prod:gitlabpage": "ng build --prod --baseHref=/framadate/funky-framadate-front/",
|
||||
"build:prod:demobliss": "ng build --prod --baseHref=https://framadate-api.cipherbliss.com",
|
||||
"build:prod:demobliss": "ng build --prod --baseHref=https://framadate-api.cipherbliss.com --stats-json ",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:ci": "jest --ci",
|
||||
@ -47,11 +47,13 @@
|
||||
"bulma": "^0.9.0",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"crypto": "^1.0.1",
|
||||
"fork-awesome": "^1.1.7",
|
||||
"ng2-charts": "^2.3.0",
|
||||
"ngx-clipboard": "^13.0.0",
|
||||
"ngx-markdown": "^9.0.0",
|
||||
"ngx-webstorage": "^5.0.0",
|
||||
"node-forge": "^0.10.0",
|
||||
"primeng": "^9.0.6",
|
||||
"quill": "^1.3.7",
|
||||
"rxjs": "^6.5.5",
|
||||
|
@ -1,14 +1,9 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {routes} from "./routes-framadate";
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { routes } from './routes-framadate';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
// enableTracing: true, // <-- debugging purposes only
|
||||
}),
|
||||
],
|
||||
imports: [RouterModule.forRoot(routes, { useHash: true })],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
@ -9,7 +9,7 @@
|
||||
<app-header [appTitle]="appTitle" [appLogo]="appLogo"></app-header>
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
<div *ngIf="devModeEnabled">
|
||||
<div class="padded" *ngIf="devModeEnabled">
|
||||
<br />
|
||||
<mat-slide-toggle (change)="sidenav.toggle()" label="dev menu"> </mat-slide-toggle> menu
|
||||
développeur
|
||||
|
@ -6,7 +6,6 @@ import { environment } from '../environments/environment';
|
||||
import { Theme } from './core/enums/theme.enum';
|
||||
import { LanguageService } from './core/services/language.service';
|
||||
import { ThemeService } from './core/services/theme.service';
|
||||
import { MockingService } from './core/services/mocking.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -23,7 +23,10 @@ import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { CoreModule } from './core/core.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
|
||||
import { CguComponent } from './features/shared/components/ui/static-pages/cgu/cgu.component';
|
||||
import { LegalComponent } from './features/shared/components/ui/static-pages/legal/legal.component';
|
||||
import { PrivacyComponent } from './features/shared/components/ui/static-pages/privacy/privacy.component';
|
||||
import { CipheringComponent } from './features/shared/components/ui/static-pages/ciphering/ciphering.component';
|
||||
registerLocaleData(localeEn, 'en-EN');
|
||||
registerLocaleData(localeFr, 'fr-FR');
|
||||
|
||||
@ -38,7 +41,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
declarations: [AppComponent, CguComponent, LegalComponent, PrivacyComponent, CipheringComponent],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
|
@ -1,10 +1,20 @@
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="column">
|
||||
<blockquote class="notification is-info is-light content is-size-3 has-text-weight-light">
|
||||
<h1 class="title">
|
||||
{{ 'home.title' | translate }}
|
||||
{{ env.appTitle }}
|
||||
</h1>
|
||||
<i class="fa fa-poll"></i>
|
||||
{{
|
||||
'SENTENCES.framadate-is-an-online-service-for-planning-an-appointment-or-making-a-decision-quickly-and-easily'
|
||||
| translate
|
||||
}}
|
||||
</blockquote>
|
||||
<img src="assets/img/undraw_group_selfie_ijc6.svg" alt="image WIP" />
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h2 class="subtitle">
|
||||
@ -19,40 +29,59 @@
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<a role="button" class="button is-fullwidth is-primary" routerLink="administration">
|
||||
<a role="button" class="button is-fullwidth is-primary is-size-3" routerLink="administration">
|
||||
<i class="fa fa-plus-circle"></i>
|
||||
{{ 'home.create_button' | translate }}
|
||||
</a>
|
||||
<img src="assets/img/kind/date.jpeg" alt="sondage date" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<a role="button" class="button is-fullwidth is-primary" routerLink="user/polls">
|
||||
<a role="button" class="button is-fullwidth is-info is-size-3" routerLink="user/polls">
|
||||
<i class="fa fa-search"></i>
|
||||
{{ 'home.search_button' | translate }}
|
||||
</a>
|
||||
<img src="assets/img/kind/classic.jpeg" alt="sondage date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<img src="assets/img/undraw_group_selfie_ijc6.svg" alt="image WIP" />
|
||||
<p>
|
||||
{{
|
||||
'SENTENCES.framadate-is-an-online-service-for-planning-an-appointment-or-making-a-decision-quickly-and-easily'
|
||||
| translate
|
||||
}}
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h2 class="title is-2">
|
||||
<i class="fa fa-format-paint"></i>
|
||||
{{ 'SENTENCES.here-is-how-it-works' | translate }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ 'SENTENCES.send-the-poll-link-to-your-friends-or-colleagues' | translate }}
|
||||
|
||||
{{ 'SENTENCES.what-you-can-do' | translate }}
|
||||
</p>
|
||||
<h2 class="title is-2">
|
||||
<i class="fa fa-format-paint"></i>
|
||||
{{ 'SENTENCES.view-an-example' | translate }}
|
||||
</h2>
|
||||
<p>
|
||||
<a href="/poll/orange-ou-citron/consultation" class="btn btn-info">
|
||||
<i class="fa fa-biking"></i> Orange ou citron?
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
{{ 'SENTENCES.what-is-framadate' | translate }}
|
||||
{{ 'SENTENCES.view-an-example' | translate }}
|
||||
<i class="fa fa-gavel"></i>
|
||||
|
||||
{{ 'SENTENCES.framadate-is-licensed-under-the' | translate }}
|
||||
<span class="licence">
|
||||
<span class="licence has-text-weight-medium">
|
||||
<i class="fa fa-gnu"></i>
|
||||
GNU Affero v3 Licence
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h2 class="title is-2">
|
||||
<i class="fa fa-seeding"></i>
|
||||
{{ 'SENTENCES.grow-your-own' | translate }}
|
||||
</h2>
|
||||
<p>
|
||||
{{
|
||||
'SENTENCES.if-you-want-to-install-the-software-for-your-own-use-and-thus-increase-your-independence-we-can-help'
|
||||
| translate
|
||||
@ -66,4 +95,79 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- statistiques-->
|
||||
<p class="title">Statistiques</p>
|
||||
<app-wip-todo></app-wip-todo>
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-4 is-vertical is-parent">
|
||||
<div class="tile is-child box">
|
||||
<div class="title">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
62 346
|
||||
</div>
|
||||
<p>sondages</p>
|
||||
</div>
|
||||
<div class="tile is-child box">
|
||||
<div class="title">
|
||||
<i class="fa fa-tachometer"></i>
|
||||
223 124
|
||||
</div>
|
||||
<p>votes</p>
|
||||
</div>
|
||||
|
||||
<div class="tile is-child box">
|
||||
<div class="title">
|
||||
<i class="fa fa-comment-o"></i>
|
||||
41 875
|
||||
</div>
|
||||
<p>commentaires</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<div class="tile is-child box">
|
||||
<div class="title">
|
||||
<i class="fa fa-calendar-check-o"></i>
|
||||
44 985
|
||||
</div>
|
||||
<p>sondages de type date</p>
|
||||
</div>
|
||||
|
||||
<div class="tile is-child box">
|
||||
<div class="title">
|
||||
<i class="fa fa-file-epub"></i>
|
||||
22 985
|
||||
</div>
|
||||
<p>sondages de type classique</p>
|
||||
</div>
|
||||
<div class="tile is-child box">
|
||||
<div class="title">
|
||||
<i class="fa fa-check-circle-o"></i>
|
||||
123
|
||||
</div>
|
||||
<p>consensus parfaits</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="title">Nos Mentions légales</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/legal">
|
||||
mentions légales,
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/cgu">
|
||||
CGU, CPU,
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/privacy">
|
||||
politique de confidentialité.
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -3,26 +3,34 @@ import { DateService } from '../services/date.service';
|
||||
|
||||
export class PollConfiguration {
|
||||
constructor(
|
||||
public isAboutDate: boolean = false,
|
||||
public isProtectedByPassword: boolean = false,
|
||||
public allowComments: boolean = true,
|
||||
public areResultsPublic: boolean = true,
|
||||
public dateCreated: Date = new Date(Date.now()),
|
||||
public password: string = '',
|
||||
public isAboutDate: boolean = false,
|
||||
public isAllowingtoChangeOwnAnswers: boolean = true,
|
||||
public isMaybeAnswerAvailable: boolean = false,
|
||||
public isProtectedByPassword: boolean = false,
|
||||
public isOwnerNotifiedByEmailOnNewVote: boolean = false,
|
||||
public isOwnerNotifiedByEmailOnNewComment: boolean = false,
|
||||
public isMaybeAnswerAvailable: boolean = false,
|
||||
public areResultsPublic: boolean = true,
|
||||
public isAllowingtoChangeOwnAnswers: boolean = true,
|
||||
public whoCanChangeAnswers: string = 'everybody',
|
||||
public dateCreated: Date = new Date(Date.now()),
|
||||
public expiresDaysDelay: number = environment.poll.defaultConfig.expiracyInDays,
|
||||
public isZeroKnoledge: boolean = true,
|
||||
public hasSeveralHours: boolean = false,
|
||||
public hasMaxCountOfAnswers: boolean = false,
|
||||
public whoCanChangeAnswers: string = environment.poll.defaultConfig.whoCanChangeAnswers, // everybody, self, nobody (= just admin)
|
||||
public visibility: string = environment.poll.defaultConfig.visibility, // visible to anyone with the link:
|
||||
public voteChoices: string = environment.poll.defaultConfig.voteChoices, // possible answers to a vote choice: only "yes", "yes, maybe, no": number = environment.poll.defaultConfig.maxCountOfAnswers,
|
||||
public maxCountOfAnswers: number = environment.poll.defaultConfig.maxCountOfAnswers,
|
||||
public expiresDaysDelay: number = environment.poll.defaultConfig.expiresDaysDelay,
|
||||
public expiracyAfterLastModificationInDays: number = environment.poll.defaultConfig
|
||||
.expiracyAfterLastModificationInDays,
|
||||
public expires: Date = DateService.addDaysToDate(
|
||||
environment.poll.defaultConfig.expiracyInDays,
|
||||
// date after creation day when people will not be able to vote anymore
|
||||
public expiracyDate: Date = DateService.addDaysToDate(
|
||||
environment.poll.defaultConfig.expiresDaysDelay,
|
||||
new Date(Date.now())
|
||||
)
|
||||
) {}
|
||||
|
||||
public static isArchived(configuration: PollConfiguration): boolean {
|
||||
return configuration.expires ? DateService.isDateInPast(configuration.expires) : undefined;
|
||||
return configuration.expiracyDate ? DateService.isDateInPast(configuration.expiracyDate) : undefined;
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,18 @@ import { User } from './user.model';
|
||||
export class Poll {
|
||||
constructor(
|
||||
public owner: User = new User(),
|
||||
public slug: string = 'default-slug',
|
||||
public title: string = 'default title',
|
||||
public slug: string = '',
|
||||
public title: string = '',
|
||||
public description?: string,
|
||||
public creatorPseudo?: string,
|
||||
public creatorEmail?: string,
|
||||
public allowSeveralHours?: boolean,
|
||||
public archiveNumberOfDays?: number,
|
||||
public configuration: PollConfiguration = new PollConfiguration(),
|
||||
public comments: Comment[] = [],
|
||||
public choices: Choice[] = [],
|
||||
public dateChoices: Choice[] = [],
|
||||
public timeChoices: Choice[] = []
|
||||
public dateChoices: Choice[] = [], // sets of days as strings, config to set identical time for days in a special days poll
|
||||
public timeChoices: Choice[] = [] // ranges of time expressed as strings
|
||||
) {}
|
||||
|
||||
public getAdministrationUrl(): string {
|
||||
@ -33,22 +37,22 @@ export class Poll {
|
||||
new User(item.owner.pseudo, item.owner.email, undefined),
|
||||
item.slug,
|
||||
item.title,
|
||||
item.description,
|
||||
item.configuration,
|
||||
item.comments
|
||||
.map(
|
||||
(c: Pick<Comment, 'author' | 'content' | 'dateCreated'>) =>
|
||||
new Comment(c.author, c.content, new Date(c.dateCreated))
|
||||
)
|
||||
.sort(Comment.sortChronologically),
|
||||
item.choices.map((c: Pick<Choice, 'label' | 'imageUrl' | 'participants' | 'counts'>) => {
|
||||
const choice = new Choice(c.label, c.imageUrl, new Map(c.participants));
|
||||
choice.participants.forEach((value, key) => {
|
||||
choice.participants.set(key, new Set(value));
|
||||
});
|
||||
choice.updateCounts();
|
||||
return choice;
|
||||
})
|
||||
item.description
|
||||
// item.configuration,
|
||||
// item.comments
|
||||
// .map(
|
||||
// (c: Pick<Comment, 'author' | 'content' | 'dateCreated'>) =>
|
||||
// new Comment(c.author, c.content, new Date(c.dateCreated))
|
||||
// )
|
||||
// .sort(Comment.sortChronologically),
|
||||
// item.choices.map((c: Pick<Choice, 'label' | 'imageUrl' | 'participants' | 'counts'>) => {
|
||||
// const choice = new Choice(c.label, c.imageUrl, new Map(c.participants));
|
||||
// choice.participants.forEach((value, key) => {
|
||||
// choice.participants.set(key, new Set(value));
|
||||
// });
|
||||
// choice.updateCounts();
|
||||
// return choice;
|
||||
// })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export class ApiService {
|
||||
|
||||
public async createPoll(poll: Poll): Promise<Subscription> {
|
||||
// this.loader.setStatus(true);
|
||||
console.log('config', poll);
|
||||
console.log('createPoll config', poll);
|
||||
// const baseHref = this.useDevLocalServer ? 'http://localhost:8000' : apiBaseHref;
|
||||
// return this.http
|
||||
// .post(`${baseHref}${currentApiRoutes['api_new_poll']}`, poll, ApiService.makeHeaders())
|
||||
|
@ -64,7 +64,7 @@ export class PollService implements Resolve<Poll> {
|
||||
console.log('getAllAvailablePolls res', res);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('getAllAvailablePolls e', e);
|
||||
console.error('getAllAvailablePolls e', e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +86,7 @@ export class PollService implements Resolve<Poll> {
|
||||
* @param config
|
||||
*/
|
||||
makeSlug(config: Poll): string {
|
||||
console.log('config', config);
|
||||
let str = '';
|
||||
str =
|
||||
config.configuration.dateCreated.getFullYear() +
|
||||
@ -228,7 +229,6 @@ export class PollService implements Resolve<Poll> {
|
||||
adminKey: '', // key to change config of the poll
|
||||
owner_modifier_token: '', // key to change a vote stack
|
||||
canModifyAnswers: newpoll.configuration.isAllowingtoChangeOwnAnswers, // bool for the frontend selector
|
||||
whoModifiesAnswers: newpoll.configuration.whoCanChangeAnswers, // everybody, self, nobody (: just admin)
|
||||
whoCanChangeAnswers: newpoll.configuration.whoCanChangeAnswers, // everybody, self, nobody (: just admin)
|
||||
dateList: newpoll.dateChoices, // sets of days as strings, config to set identical time for days in a special days poll
|
||||
timeList: newpoll.timeChoices, // ranges of time expressed as strings
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
@ -10,16 +10,38 @@ import { StepperComponent } from './stepper/stepper.component';
|
||||
import { NamingComponent } from './naming/naming.component';
|
||||
import { FormComponent } from './form/form.component';
|
||||
import { DateValueAccessorModule } from 'angular-date-value-accessor';
|
||||
import { SuccessComponent } from './success/success.component';
|
||||
import { DateSelectComponent } from './form/date-select/date-select.component';
|
||||
import { TextSelectComponent } from './form/text-select/text-select.component';
|
||||
import { KindSelectComponent } from './form/kind-select/kind-select.component';
|
||||
import { BaseConfigComponent } from './form/base-config/base-config.component';
|
||||
import { AdvancedConfigComponent } from './form/advanced-config/advanced-config.component';
|
||||
import { CalendarModule } from 'primeng';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdministrationComponent, StepperComponent, NamingComponent, FormComponent],
|
||||
declarations: [
|
||||
AdministrationComponent,
|
||||
StepperComponent,
|
||||
NamingComponent,
|
||||
FormComponent,
|
||||
SuccessComponent,
|
||||
DateSelectComponent,
|
||||
TextSelectComponent,
|
||||
KindSelectComponent,
|
||||
BaseConfigComponent,
|
||||
AdvancedConfigComponent,
|
||||
],
|
||||
imports: [
|
||||
CalendarModule,
|
||||
AdministrationRoutingModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
SharedModule,
|
||||
FormsModule,
|
||||
TranslateModule.forChild({ extend: true }),
|
||||
DateValueAccessorModule,
|
||||
DragDropModule,
|
||||
],
|
||||
})
|
||||
export class AdministrationModule {}
|
||||
|
@ -0,0 +1,165 @@
|
||||
<form [formGroup]="form">
|
||||
<fieldset class="complete well">
|
||||
<h2>{{ 'creation.advanced' | translate }}</h2>
|
||||
<label for="descr">Description (optionnel)</label>
|
||||
<textarea
|
||||
#description
|
||||
matInput
|
||||
id="descr"
|
||||
placeholder="Description"
|
||||
formControlName="description"
|
||||
required
|
||||
></textarea>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="description.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="description.value = ''"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
|
||||
<br />
|
||||
|
||||
<label for="slug">
|
||||
Url personnalisée pour les participants
|
||||
<i class="fa fa-close"></i>
|
||||
</label>
|
||||
<br />
|
||||
<span
|
||||
>{{ urlPrefix }}
|
||||
<strong>
|
||||
{{ form.controls.slug.value }}
|
||||
</strong>
|
||||
</span>
|
||||
<app-copy-text [textToCopy]="form.controls.slug.value"></app-copy-text>
|
||||
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="form.controls.slug.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="slug.value = ''"
|
||||
></button>
|
||||
<input #slug matInput id="slug" placeholder="Url" formControlName="slug" required />
|
||||
<br />
|
||||
<div appearance="outline" class="is-not-flex">
|
||||
<mat-label
|
||||
>Nombre de jours avant expiration de la possibilité de voter, après le sondage reste
|
||||
consultable</mat-label
|
||||
>
|
||||
<input
|
||||
#expiresDaysDelay
|
||||
id="expiresDaysDelay"
|
||||
matInput
|
||||
type="number"
|
||||
placeholder="Nombre de jours avant fin des votes"
|
||||
formControlName="expiracyNumberOfDays"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="expiresDaysDelay.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="expiresDaysDelay.value = ''"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div appearance="outline" class="is-not-flex">
|
||||
<mat-label
|
||||
>Nombre de jours avant archivage, après quoi le sondage n'est plus visible par le public</mat-label
|
||||
>
|
||||
<input
|
||||
#archive
|
||||
id="archive"
|
||||
matInput
|
||||
type="number"
|
||||
placeholder="Nombre de jours avant archivage"
|
||||
formControlName="archiveNumberOfDays"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="archive.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="archive.value = ''"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.areResultsPublic">
|
||||
Les participants pourront consulter les résultats
|
||||
</mat-checkbox>
|
||||
<h3 class="title is-3">
|
||||
<i class="fa fa-lock"></i>
|
||||
Accès sécurisé
|
||||
</h3>
|
||||
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.isProtectedByPassword">
|
||||
Le sondage sera protégé par un mot de passe
|
||||
</mat-checkbox>
|
||||
|
||||
<input
|
||||
*ngIf="form.value.isProtectedByPassword"
|
||||
#password
|
||||
id="password"
|
||||
matInput
|
||||
type="password"
|
||||
placeholder="password"
|
||||
formControlName="configuration.password"
|
||||
required
|
||||
/>
|
||||
|
||||
<h3 class="title is-3">
|
||||
<i class="fa fa-enveloppe"></i>
|
||||
Notifications
|
||||
</h3>
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.isOwnerNotifiedByEmailOnNewVote">
|
||||
Vous recevrez un mail à chaque nouvelle participation
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.isOwnerNotifiedByEmailOnNewComment">
|
||||
Vous recevrez un mail à chaque nouveau commentaire
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.isMaybeAnswerAvailable">
|
||||
La réponse « peut-être » sera disponible
|
||||
</mat-checkbox>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h2>
|
||||
Fonctionnalités pas encore disponibles:
|
||||
</h2>
|
||||
<app-wip-todo></app-wip-todo>
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.isProtectedByPassword">
|
||||
Spécifier un lien unique de vote à des participants définis
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.allowComments">
|
||||
Autoriser les commentaires
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.hasMaxCountOfAnswers">
|
||||
Nombre de réponses limitées à ce nombre
|
||||
</mat-checkbox>
|
||||
<input
|
||||
*ngIf="form.value.configuration.hasMaxCountOfAnswers"
|
||||
#maxCountOfAnswers
|
||||
id="maxCountOfAnswers"
|
||||
matInput
|
||||
type="number"
|
||||
placeholder="Nombre de réponses max"
|
||||
formControlName="configuration.maxCountOfAnswers"
|
||||
required
|
||||
/>
|
||||
|
||||
<mat-checkbox class="is-not-flex" formControlName="configuration.isZeroKnoledge">
|
||||
Les informations du sondage seront chiffrés en base de données
|
||||
</mat-checkbox>
|
||||
</fieldset>
|
||||
</form>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdvancedConfigComponent } from './advanced-config.component';
|
||||
|
||||
describe('AdvancedConfigComponent', () => {
|
||||
let component: AdvancedConfigComponent;
|
||||
let fixture: ComponentFixture<AdvancedConfigComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AdvancedConfigComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdvancedConfigComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Poll } from '../../../../core/models/poll.model';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-advanced-config',
|
||||
templateUrl: './advanced-config.component.html',
|
||||
styleUrls: ['./advanced-config.component.scss'],
|
||||
})
|
||||
export class AdvancedConfigComponent implements OnInit {
|
||||
public urlPrefix: string = window.location.origin + '/participation/';
|
||||
@Input()
|
||||
public poll?: Poll;
|
||||
@Input()
|
||||
public form: FormGroup;
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<form [formGroup]="form">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p>
|
||||
{{ 'creation.choose_title' | translate }}
|
||||
</p>
|
||||
<h2 class="title is-2">slug: {{ form.value.slug }}</h2>
|
||||
<br />
|
||||
<label class="hidden" for="title">Titre</label>
|
||||
<input
|
||||
matInput
|
||||
#title
|
||||
[placeholder]="'creation.choose_title_placeholder' | translate"
|
||||
formControlName="title"
|
||||
id="title"
|
||||
autofocus="autofocus"
|
||||
(change)="updateSlug()"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="form.value.title"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="form.patchValue({ title: '' })"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column">
|
||||
<label for="creatorPseudo">
|
||||
<span>
|
||||
{{ 'creation.email' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
#creatorEmail
|
||||
matInput
|
||||
placeholder="{{ 'creation.email_placeholder' | translate }}"
|
||||
formControlName="creatorEmail"
|
||||
id="creatorEmail"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
|
||||
<label class="" for="creatorEmail">
|
||||
<span>
|
||||
{{ 'creation.name' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
#creatorPseudo
|
||||
matInput
|
||||
placeholder="pseudo"
|
||||
formControlName="creatorPseudo"
|
||||
id="creatorPseudo"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<img src="assets/img/undraw_Moving_twwf.svg" alt="image WIP" />
|
||||
<div>
|
||||
<h2>
|
||||
{{ 'choices.title' | translate }}
|
||||
</h2>
|
||||
{{ 'dates.add' | translate }}
|
||||
<p>
|
||||
<i>
|
||||
{{ 'choices.helper' | translate }}
|
||||
</i>
|
||||
</p>
|
||||
{{ 'choices.answer_preset_1' | translate }}
|
||||
{{ 'choices.add' | translate }}
|
||||
{{ 'choices.continue' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button class="btn btn--warning" (click)="askInitFormDefault()">
|
||||
<i class="fa fa-refresh"></i>
|
||||
Tout réinitialiser
|
||||
</button>
|
||||
<br />
|
||||
<button class="btn is-success" (click)="createPoll()">
|
||||
<i class="fa fa-save"></i>
|
||||
Enregistrer le sondage
|
||||
</button>
|
||||
<br />
|
||||
<button class="btn is-default" (click)="automaticSlug()">
|
||||
<i class="fa fa-refresh"></i>
|
||||
Slug automatique
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BaseConfigComponent } from './base-config.component';
|
||||
|
||||
describe('BaseConfigComponent', () => {
|
||||
let component: BaseConfigComponent;
|
||||
let fixture: ComponentFixture<BaseConfigComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BaseConfigComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BaseConfigComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,69 @@
|
||||
import { ChangeDetectorRef, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { ToastService } from '../../../../core/services/toast.service';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { UuidService } from '../../../../core/services/uuid.service';
|
||||
import { PollService } from '../../../../core/services/poll.service';
|
||||
import { ApiService } from '../../../../core/services/api.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Poll } from '../../../../core/models/poll.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-config',
|
||||
templateUrl: './base-config.component.html',
|
||||
styleUrls: ['./base-config.component.scss'],
|
||||
})
|
||||
export class BaseConfigComponent {
|
||||
@Input()
|
||||
public poll?: Poll;
|
||||
@Input()
|
||||
public form: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private cd: ChangeDetectorRef,
|
||||
private uuidService: UuidService,
|
||||
private toastService: ToastService,
|
||||
private pollService: PollService,
|
||||
private apiService: ApiService,
|
||||
private router: Router,
|
||||
@Inject(DOCUMENT) private document: Document
|
||||
) {}
|
||||
|
||||
askInitFormDefault(): void {
|
||||
// this.initFormDefault(false);
|
||||
this.toastService.display('formulaire réinitialisé');
|
||||
}
|
||||
|
||||
public createPoll(): void {
|
||||
console.log('this.form', this.form);
|
||||
const newpoll = this.pollService.newPollFromForm(this.form);
|
||||
console.log('newpoll', newpoll);
|
||||
const router = this.router;
|
||||
|
||||
if (this.form.valid) {
|
||||
console.log('Le sondage est correctement rempli, prêt à enregistrer.');
|
||||
const newpoll = this.pollService.newPollFromForm(this.form);
|
||||
// TODO : save the poll
|
||||
|
||||
this.apiService.createPoll(newpoll).then((resp) => {
|
||||
console.log('resp', resp);
|
||||
router.navigate(['success']);
|
||||
});
|
||||
} else {
|
||||
this.toastService.display('invalid form');
|
||||
}
|
||||
}
|
||||
|
||||
public updateSlug(): void {
|
||||
const newValueFormatted = 'TODO';
|
||||
this.form.patchValue({ slug: newValueFormatted });
|
||||
}
|
||||
|
||||
/**
|
||||
* set the poll slug from other data of the poll
|
||||
*/
|
||||
automaticSlug() {
|
||||
this.form.patchValue({ slug: this.pollService.makeSlug(this.form.value) });
|
||||
}
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
<div class="date-selection">
|
||||
<form [formGroup]="form">
|
||||
<!-- interval-->
|
||||
<button
|
||||
(click)="showDateInterval = !showDateInterval"
|
||||
[ngClass]="{ active: showDateInterval }"
|
||||
class="btn btn--primary"
|
||||
id="toggle_interval_button"
|
||||
>
|
||||
<i class="fa fa-clock-o" aria-hidden="true"></i>
|
||||
<span>
|
||||
{{ 'dates.add_interval' | translate }}
|
||||
</span>
|
||||
</button>
|
||||
<fieldset *ngIf="showDateInterval" class="date-interval form-row is-boxed">
|
||||
<h2>{{ 'dates.add_interval' | translate }}</h2>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
{{ 'dates.interval_propose' | translate }}
|
||||
</div>
|
||||
<div class="column">
|
||||
<label for="start_interval" class="hidden">start</label>
|
||||
<input id="start_interval" (change)="countDays()" formControlName="startDateInterval" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
{{ 'dates.interval_span' | translate }}
|
||||
</div>
|
||||
<div class="column">
|
||||
<label for="end_interval" class="hidden">end</label>
|
||||
<input id="end_interval" formControlName="endDateInterval" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<button (click)="addIntervalOfDates()" class="btn btn-block btn--primary">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
{{ 'dates.interval_button' | translate }}
|
||||
{{ intervalDays }}
|
||||
{{ 'dates.interval_button_dates' | translate }}
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div class="dates-list">
|
||||
<button
|
||||
class="btn"
|
||||
[class.is-primary]="form.value.configuration.hasSeveralHours"
|
||||
(click)="
|
||||
form.patchValue({
|
||||
configuration: { hasSeveralHours: !form.value.configuration.hasSeveralHours }
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fa fa-clock-o"></i>
|
||||
|
||||
<span> horaires avancées</span>
|
||||
</button>
|
||||
<div class="is-info notification">
|
||||
<span *ngIf="form.value.configuration.hasSeveralHours">
|
||||
Chaque jour aura ses plages de temps personnalisées
|
||||
</span>
|
||||
<span *ngIf="!form.value.configuration.hasSeveralHours">
|
||||
Tous les jours auront les mêmes plages de temps
|
||||
</span>
|
||||
</div>
|
||||
<fieldset class="range-container is-boxed">
|
||||
<div class="actions columns">
|
||||
<div class="column"></div>
|
||||
<div class="column has-text-right">
|
||||
<button
|
||||
(click)="addTime()"
|
||||
*ngIf="!form.value.configuration.hasSeveralHours"
|
||||
class="btn btn--primary"
|
||||
id="add_time_button"
|
||||
>
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
<span>
|
||||
{{ 'dates.add_time' | translate }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
(click)="removeAllTimes()"
|
||||
*ngIf="form.value.configuration.hasSeveralHours"
|
||||
class="btn btn--warning"
|
||||
id="remove_time_button"
|
||||
>
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
<span>
|
||||
Aucune plage horaire
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
(click)="resetTimes()"
|
||||
*ngIf="form.value.configuration.hasSeveralHours"
|
||||
class="btn btn--warning"
|
||||
id="reset_time_button"
|
||||
>
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
<span>
|
||||
réinitialiser
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-time" *ngIf="!form.value.configuration.hasSeveralHours">
|
||||
<h2>
|
||||
<span class="count-dates-txt">
|
||||
{{ 'dates.count_time' | translate }}
|
||||
(identique pour chaque jour)
|
||||
</span>
|
||||
</h2>
|
||||
<div class="title">
|
||||
<span class="count-dates">
|
||||
{{ timeList.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="identical-dates" cdkDropListGroup>
|
||||
<div
|
||||
class="time-list"
|
||||
cdkDropList
|
||||
[cdkDropListData]="timeList"
|
||||
(cdkDropListDropped)="dropTimeItem($event)"
|
||||
>
|
||||
<div *ngFor="let time of timeList; index as time_id" class="time-choice" cdkDrag>
|
||||
<div class="columns">
|
||||
<div class="column movable">
|
||||
<label [for]="'timeChoices_' + time_id">
|
||||
<i class="fa fa-clock-o" aria-hidden="true"></i>
|
||||
</label>
|
||||
<input
|
||||
[(ngModel)]="time.literal"
|
||||
name="timeChoices_{{ time_id }}"
|
||||
type="text"
|
||||
id="timeChoices_{{ time_id }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button (click)="timeList.splice(time_id, 1)" class="btn btn-warning">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<hr />
|
||||
<div class="main-box is-boxed">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<!-- ajouter une date-->
|
||||
<button class="btn btn--primary" (click)="addChoice()">
|
||||
{{ 'dates.add' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="column">
|
||||
<span class="count-dates title">
|
||||
{{ dateList.length }}
|
||||
</span>
|
||||
<span>
|
||||
{{ 'dates.count_dates' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<!-- TODO lier au formulaire les valeurs des dates-->
|
||||
<h2>Dates</h2>
|
||||
<div *ngFor="let choice of dateList; index as id" class="date-choice">
|
||||
{{ id }})
|
||||
<input
|
||||
[(ngModel)]="choice.date_object"
|
||||
name="dateChoices_{{ id }}"
|
||||
id="dateChoices_{{ id }}"
|
||||
useValueAsDate
|
||||
type="date"
|
||||
/>
|
||||
<button (click)="dateList.splice(id, 1)" class="btn btn-warning">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
(click)="addTimeToDate(choice, id)"
|
||||
*ngIf="form.value.configuration.hasSeveralHours"
|
||||
class="btn btn--primary"
|
||||
>
|
||||
{{ 'dates.add_time' | translate }}
|
||||
</button>
|
||||
<div *ngIf="form.value.configuration.hasSeveralHours" class="several-times">
|
||||
plage horaire distincte
|
||||
<br />
|
||||
<div *ngFor="let timeItem of choice.timeList; index as idTime" class="time-choice">
|
||||
<input
|
||||
[(ngModel)]="timeItem.literal"
|
||||
name="dateTime_{{ id }}_Choices_{{ idTime }}"
|
||||
id="dateTime_{{ id }}_Choices_{{ idTime }}"
|
||||
type="text"
|
||||
/>
|
||||
<button (click)="choice.timeList.splice(idTime, 1)" class="btn btn-warning">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column calendar-column">
|
||||
<button class="btn" (click)="selectionKind = 'range'" [class.is-primary]="selectionKind == 'range'">
|
||||
plage de jours
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn"
|
||||
(click)="selectionKind = 'multiple'"
|
||||
[class.is-primary]="selectionKind == 'multiple'"
|
||||
>
|
||||
jours séparés
|
||||
</button>
|
||||
<button class="btn" (click)="setDefaultDatesForInterval()">
|
||||
réinitialiser</button
|
||||
><button class="btn" (click)="dateCalendarEnum = [today]">
|
||||
vider
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<br />
|
||||
<p-calendar
|
||||
[(ngModel)]="dateCalendarEnum"
|
||||
[locale]="'calendar_widget' | translate"
|
||||
[inline]="true"
|
||||
[monthNavigator]="true"
|
||||
[selectionMode]="selectionKind"
|
||||
></p-calendar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,43 @@
|
||||
:host {
|
||||
.time-choice {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: solid 1px #dedede;
|
||||
padding: 0.5em;
|
||||
//padding: 20px 10px;
|
||||
border-bottom: solid 1px #ccc;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
box-sizing: border-box;
|
||||
cursor: move;
|
||||
background: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
.btn i + span {
|
||||
margin-left: 1ch;
|
||||
}
|
||||
.btn + .btn {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
border: 2px solid #ccc;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
* {
|
||||
opacity: 0;
|
||||
}
|
||||
background: #dedede;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
.movable {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DateSelectComponent } from './date-select.component';
|
||||
|
||||
describe('DateSelectComponent', () => {
|
||||
let component: DateSelectComponent;
|
||||
let fixture: ComponentFixture<DateSelectComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DateSelectComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DateSelectComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,259 @@
|
||||
import { ChangeDetectorRef, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { UuidService } from '../../../../core/services/uuid.service';
|
||||
import { ToastService } from '../../../../core/services/toast.service';
|
||||
import { PollService } from '../../../../core/services/poll.service';
|
||||
import { DateUtilities } from '../../../old-stuff/config/DateUtilities';
|
||||
import { ApiService } from '../../../../core/services/api.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { DateChoice, moreTimeOfDay, otherDefaultDates, TimeSlices } from '../../../old-stuff/config/defaultConfigs';
|
||||
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-date-select',
|
||||
templateUrl: './date-select.component.html',
|
||||
styleUrls: ['./date-select.component.scss'],
|
||||
})
|
||||
export class DateSelectComponent implements OnInit {
|
||||
@Input()
|
||||
public form: FormGroup;
|
||||
|
||||
public showDateInterval = true;
|
||||
public allowSeveralHours = true;
|
||||
today = new Date();
|
||||
startDateInterval: string;
|
||||
endDateInterval: string;
|
||||
intervalDays: any;
|
||||
intervalDaysDefault = 7;
|
||||
dateList: DateChoice[] = otherDefaultDates; // sets of days as strings, config to set identical time for days in a special days poll
|
||||
timeList: TimeSlices[] = moreTimeOfDay; // ranges of time expressed as strings
|
||||
dateCalendarEnum: Date[] = [new Date('02/09/2021')];
|
||||
selectionKind = 'range';
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private cd: ChangeDetectorRef,
|
||||
private uuidService: UuidService,
|
||||
private toastService: ToastService,
|
||||
private pollService: PollService,
|
||||
public dateUtilities: DateUtilities,
|
||||
private apiService: ApiService,
|
||||
private router: Router,
|
||||
private translateService: TranslateService,
|
||||
@Inject(DOCUMENT) private document: any
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// this.setDefaultDatesForInterval();
|
||||
}
|
||||
|
||||
get choices(): FormArray {
|
||||
return this.form.get('choices') as FormArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* default interval of dates proposed is from today to 7 days more
|
||||
*/
|
||||
setDefaultDatesForInterval(): void {
|
||||
const dateCurrent = new Date();
|
||||
const dateJson = dateCurrent.toISOString();
|
||||
this.startDateInterval = dateJson.substring(0, 10);
|
||||
this.endDateInterval = this.dateUtilities
|
||||
.addDaysToDate(this.intervalDaysDefault, dateCurrent)
|
||||
.toISOString()
|
||||
.substring(0, 10);
|
||||
this.form.patchValue({
|
||||
startDateInterval: this.startDateInterval,
|
||||
endDateInterval: this.endDateInterval,
|
||||
});
|
||||
this.dateCalendarEnum = [dateCurrent, this.dateUtilities.addDaysToDate(this.intervalDaysDefault, dateCurrent)];
|
||||
this.countDays();
|
||||
}
|
||||
|
||||
countDays(): void {
|
||||
this.intervalDays = this.dateUtilities.countDays(
|
||||
this.dateUtilities.parseInputDateToDateObject(this.startDateInterval),
|
||||
this.dateUtilities.parseInputDateToDateObject(this.endDateInterval)
|
||||
);
|
||||
// this.cd.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* add all the dates between the start and end dates in the interval section
|
||||
*/
|
||||
addIntervalOfDates(): void {
|
||||
const newIntervalArray = this.dateUtilities.getDatesInRange(
|
||||
this.dateUtilities.parseInputDateToDateObject(this.startDateInterval),
|
||||
this.dateUtilities.parseInputDateToDateObject(this.endDateInterval),
|
||||
1
|
||||
);
|
||||
|
||||
const converted = [];
|
||||
newIntervalArray.forEach((element) => {
|
||||
converted.push({
|
||||
literal: element.literal,
|
||||
date_object: element.date_object,
|
||||
timeList: [],
|
||||
});
|
||||
});
|
||||
this.dateList = [...new Set(converted)];
|
||||
// add only dates that are not already present with a Set of unique items
|
||||
console.log('this.dateList', this.dateList);
|
||||
this.showDateInterval = false;
|
||||
|
||||
this.form.patchValue({ choices: this.dateList });
|
||||
// this.dateList.forEach(elem=>{
|
||||
// const newControlGroup = this.fb.group({
|
||||
// label: this.fb.control('', [Validators.required]),
|
||||
// imageUrl: ['', [Validators.required]],
|
||||
// });
|
||||
//
|
||||
// this.choices.push(newControlGroup);
|
||||
// })
|
||||
|
||||
this.toastService.display(`les dates ont été ajoutées aux réponses possibles.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* change time spans
|
||||
*/
|
||||
addTime() {
|
||||
this.timeList.push({
|
||||
literal: '',
|
||||
});
|
||||
}
|
||||
|
||||
removeAllTimes() {
|
||||
this.timeList = [];
|
||||
}
|
||||
|
||||
resetTimes() {
|
||||
this.timeList = otherDefaultDates;
|
||||
}
|
||||
|
||||
/**
|
||||
* add a time period to a specific date choice,
|
||||
* focus on the new input
|
||||
* @param config
|
||||
* @param id
|
||||
*/
|
||||
addTimeToDate(config: any, id: number) {
|
||||
this.timeList.push({
|
||||
literal: '',
|
||||
});
|
||||
const selector = '[ng-reflect-choice_label="dateTime_' + id + '_Choices_' + (this.timeList.length - 1) + '"]';
|
||||
// this.cd.detectChanges();
|
||||
const elem = this.document.querySelector(selector);
|
||||
if (elem) {
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
get dateChoices() {
|
||||
return this.form.get('dateChoices') as FormArray;
|
||||
}
|
||||
|
||||
addChoice(optionalLabel = ''): void {
|
||||
const newControlGroup = this.fb.group({
|
||||
label: this.fb.control('', [Validators.required]),
|
||||
imageUrl: ['', [Validators.required]],
|
||||
});
|
||||
|
||||
if (optionalLabel) {
|
||||
newControlGroup.patchValue({
|
||||
label: optionalLabel,
|
||||
imageUrl: 'mon url',
|
||||
});
|
||||
}
|
||||
this.dateChoices.push(newControlGroup);
|
||||
// this.cd.detectChanges();
|
||||
console.log('this.choices.length', this.choices.length);
|
||||
|
||||
this.focusOnChoice(this.choices.length - 1);
|
||||
}
|
||||
|
||||
focusOnChoice(index): void {
|
||||
const selector = '#choice_label_' + index;
|
||||
const elem = this.document.querySelector(selector);
|
||||
if (elem) {
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
deleteChoiceField(index: number): void {
|
||||
if (this.choices.length !== 1) {
|
||||
this.choices.removeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
reinitChoices(): void {
|
||||
this.choices.setValue([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle keyboard shortcuts
|
||||
* @param $event
|
||||
* @param choice_number
|
||||
*/
|
||||
keyOnChoice($event: KeyboardEvent, choice_number: number): void {
|
||||
$event.preventDefault();
|
||||
|
||||
console.log('this.choices.length', this.choices.length);
|
||||
console.log('choice_number', choice_number);
|
||||
const lastChoice = this.choices.length - 1 === choice_number;
|
||||
// TODO handle shortcuts
|
||||
// reset field with Ctrl + D
|
||||
// add a field with Ctrl + N
|
||||
// go to previous choice with arrow up
|
||||
// go to next choice with arrow down
|
||||
console.log('$event', $event);
|
||||
|
||||
if ($event.key == 'ArrowUp' && choice_number > 0) {
|
||||
this.focusOnChoice(choice_number - 1);
|
||||
}
|
||||
if ($event.key == 'ArrowDown') {
|
||||
// add a field if we are on the last choice
|
||||
if (lastChoice) {
|
||||
this.addChoice();
|
||||
this.toastService.display('choix ajouté par raccourci "flèche bas"');
|
||||
} else {
|
||||
this.focusOnChoice(choice_number + 1);
|
||||
}
|
||||
}
|
||||
if ($event.ctrlKey && $event.key == 'Backspace') {
|
||||
this.deleteChoiceField(choice_number);
|
||||
this.toastService.display('choix supprimé par raccourci "Ctrl + retour"');
|
||||
// this.cd.detectChanges();
|
||||
this.focusOnChoice(Math.min(choice_number - 1, 0));
|
||||
}
|
||||
if ($event.ctrlKey && $event.key == 'Enter') {
|
||||
// go to other fields
|
||||
const elem = this.document.querySelector('#creatorEmail');
|
||||
if (elem) {
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDemoValues(): void {
|
||||
this.addChoice('orange');
|
||||
this.addChoice('raisin');
|
||||
this.addChoice('abricot');
|
||||
}
|
||||
|
||||
dropTimeItem(event: any) {
|
||||
// moveItemInArray(this.timeList, event.previousIndex, event.currentIndex);
|
||||
if (event.previousContainer === event.container) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
} else {
|
||||
transferArrayItem(
|
||||
event.previousContainer.data,
|
||||
event.container.data,
|
||||
event.previousIndex,
|
||||
event.currentIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,421 +1,69 @@
|
||||
<div class="admin-form">
|
||||
<div class="admin-form padded">
|
||||
<form [formGroup]="form">
|
||||
<header class="columns">
|
||||
<div class="column">
|
||||
<h1>
|
||||
{{ 'creation.title' | translate }}
|
||||
</h1>
|
||||
|
||||
<img src="assets/img/undraw_Moving_twwf.svg" alt="image WIP" />
|
||||
<button class="btn btn--warning" (click)="askInitFormDefault()">
|
||||
<i class="fa fa-refresh"></i>
|
||||
Tout réinitialiser
|
||||
</button>
|
||||
<button class="btn is-success" (click)="createPoll()">
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
class="btn is-success"
|
||||
(click)="apiService.createPoll(poll)"
|
||||
[disabled]="!form.valid || !form.valid"
|
||||
>
|
||||
<i class="fa fa-save"></i>
|
||||
Enregistrer le sondage
|
||||
</button>
|
||||
<button class="btn is-default" (click)="automaticSlug()">
|
||||
<i class="fa fa-save"></i>
|
||||
Slug automatique
|
||||
</button>
|
||||
|
||||
<div class="well">
|
||||
{{ poll.slug }}
|
||||
</div>
|
||||
<div class="has-background-danger" *ngIf="!form.valid">
|
||||
</header>
|
||||
<main class="columns">
|
||||
<div class="column">
|
||||
<p class="subtitle">
|
||||
{{ 'creation.want' | translate }}
|
||||
</p>
|
||||
|
||||
<app-kind-select [form]="form"></app-kind-select>
|
||||
<app-base-config [form]="form"></app-base-config>
|
||||
<!-- <app-date-select-->
|
||||
<!-- *ngIf="form.value.configuration && form.value.configuration.isAboutDate"-->
|
||||
<!-- [form]="form"-->
|
||||
<!-- ></app-date-select>-->
|
||||
<!-- <app-text-select ng-if="!form.value.isAboutDate" [form]="form"></app-text-select>-->
|
||||
|
||||
<button
|
||||
class="btn"
|
||||
[class]="{ 'is-primary': advancedDisplayEnabled, 'is-info': !advancedDisplayEnabled }"
|
||||
(click)="advancedDisplayEnabled = !advancedDisplayEnabled"
|
||||
>
|
||||
<i class="fa fa-save"></i>
|
||||
{{ 'creation.advanced' | translate }}
|
||||
</button>
|
||||
<app-advanced-config [poll]="poll" [form]="form" *ngIf="advancedDisplayEnabled"></app-advanced-config>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="column" *ngIf="show_debug_data">
|
||||
<h2>Debug data</h2>
|
||||
<pre class="debug padded warning">
|
||||
form values :
|
||||
{{ form.value | json }}
|
||||
</pre
|
||||
>
|
||||
<pre class="debug padded warning">
|
||||
poll initial values :
|
||||
{{ poll | json }}
|
||||
</pre
|
||||
>
|
||||
</footer>
|
||||
<hr />
|
||||
<div class="validation">
|
||||
<div class="has-background-danger" *ngIf="!form.valid && form.touched">
|
||||
le formulaire est invalide
|
||||
<pre>
|
||||
|
||||
{{ form.errors | json }}
|
||||
</pre
|
||||
>
|
||||
</div>
|
||||
<div class="dates-list">
|
||||
<div class="title">
|
||||
<span class="count-dates">
|
||||
{{ timeList.length }}
|
||||
</span>
|
||||
<span class="count-dates-txt">
|
||||
{{ 'dates.count_time' | translate }}
|
||||
(pour chaque jour)
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button
|
||||
(click)="addTime()"
|
||||
*ngIf="'false' === allowSeveralHours"
|
||||
class="btn btn--primary"
|
||||
id="add_time_button"
|
||||
>
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
{{ 'dates.add_time' | translate }}
|
||||
</button>
|
||||
<button
|
||||
(click)="removeAllTimes()"
|
||||
*ngIf="'false' === allowSeveralHours"
|
||||
class="btn btn--warning"
|
||||
id="remove_time_button"
|
||||
>
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
Aucune plage horaire
|
||||
</button>
|
||||
<button
|
||||
(click)="resetTimes()"
|
||||
*ngIf="'false' === allowSeveralHours"
|
||||
class="btn btn--warning"
|
||||
id="reset_time_button"
|
||||
>
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="'false' === allowSeveralHours" class="identical-dates">
|
||||
<div cdkDropList class="example-list" (cdkDropListDropped)="drop($event)">
|
||||
<div *ngFor="let time of timeList; index as id" class="time-choice" cdkDrag>
|
||||
<label for="timeChoices_{{ id }}">
|
||||
<i class="fa fa-clock-o" aria-hidden="true"></i>
|
||||
</label>
|
||||
<input
|
||||
[(ngModel)]="time.literal"
|
||||
name="timeChoices_{{ id }}"
|
||||
type="text"
|
||||
id="timeChoices_{{ id }}"
|
||||
/>
|
||||
<button (click)="time.timeList.splice(id, 1)" class="btn btn-warning">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<span class="count-dates title">
|
||||
{{ dateList.length }}
|
||||
</span>
|
||||
<span>
|
||||
{{ 'dates.count_dates' | translate }}
|
||||
</span>
|
||||
<button class="btn btn--primary" (click)="addChoice()">
|
||||
{{ 'dates.add' | translate }}
|
||||
</button>
|
||||
<div *ngFor="let choice of dateList; index as id" class="date-choice">
|
||||
{{ id }})
|
||||
<input
|
||||
[(ngModel)]="choice.date_object"
|
||||
name="dateChoices_{{ id }}"
|
||||
id="dateChoices_{{ id }}"
|
||||
useValueAsDate
|
||||
type="date"
|
||||
/>
|
||||
<button (click)="dateList.splice(id, 1)" class="btn btn-warning">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button (click)="addTimeToDate(choice, id)" *ngIf="'true' === allowSeveralHours" class="btn btn--primary">
|
||||
{{ 'dates.add_time' | translate }}
|
||||
</button>
|
||||
<div *ngIf="'true' === allowSeveralHours" class="several-times">
|
||||
<div *ngFor="let timeItem of choice.timeList; index as idTime" class="time-choice">
|
||||
<input
|
||||
[(ngModel)]="timeItem.literal"
|
||||
name="dateTime_{{ id }}_Choices_{{ idTime }}"
|
||||
id="dateTime_{{ id }}_Choices_{{ idTime }}"
|
||||
type="text"
|
||||
/>
|
||||
<button (click)="choice.timeList.splice(idTime, 1)" class="btn btn-warning">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="form-field">
|
||||
<span class="pre-selector">
|
||||
{{ 'creation.want' | translate }}
|
||||
</span>
|
||||
<div class="kind-of-poll columns">
|
||||
<div class="column">
|
||||
<button
|
||||
class="btn-block btn"
|
||||
[ngClass]="{ 'is-primary': form.controls.isAboutDate.value }"
|
||||
(click)="form.controls.isAboutDate.setValue(true)"
|
||||
>
|
||||
<i class="fa fa-calendar"></i>
|
||||
{{ 'creation.kind.date' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
class="btn-block btn btn-default"
|
||||
[ngClass]="{ 'is-primary': !form.controls.isAboutDate.value }"
|
||||
(click)="form.controls.isAboutDate.setValue(false)"
|
||||
>
|
||||
<i class="fa fa-stats"></i>
|
||||
{{ 'creation.kind.classic' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ 'creation.choose_title' | translate }}
|
||||
</span>
|
||||
<label class="hidden" for="title">Titre</label>
|
||||
<input
|
||||
#title
|
||||
matInput
|
||||
[placeholder]="'creation.choose_title_placeholder' | translate"
|
||||
formControlName="title"
|
||||
id="title"
|
||||
autofocus="autofocus"
|
||||
(change)="updateSlug()"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="title.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="title.value = ''"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<fieldset class="date-kind">
|
||||
<!-- choix spécialement pour les dates-->
|
||||
</fieldset>
|
||||
|
||||
<button
|
||||
(click)="showDateInterval = !showDateInterval"
|
||||
[ngClass]="{ active: showDateInterval }"
|
||||
class="btn btn--primary"
|
||||
id="toggle_interval_button"
|
||||
>
|
||||
<i class="fa fa-clock-o" aria-hidden="true"></i>
|
||||
{{ 'dates.add_interval' | translate }}
|
||||
</button>
|
||||
<section *ngIf="showDateInterval" class="date-interval form-row">
|
||||
<h2>{{ 'dates.add_interval' | translate }}</h2>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
{{ 'dates.interval_propose' | translate }}
|
||||
</div>
|
||||
<div class="column">
|
||||
<label for="start_interval" class="hidden">start</label>
|
||||
<input id="start_interval" (change)="countDays()" formControlName="startDateInterval" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
{{ 'dates.interval_span' | translate }}
|
||||
</div>
|
||||
<div class="column">
|
||||
<label for="end_interval" class="hidden">end</label>
|
||||
<input id="end_interval" formControlName="endDateInterval" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<button (click)="addIntervalOfDates()" class="btn btn-block btn--primary">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
{{ 'dates.interval_button' | translate }}
|
||||
{{ intervalDays }}
|
||||
{{ 'dates.interval_button_dates' | translate }}
|
||||
</button>
|
||||
<hr />
|
||||
</section>
|
||||
|
||||
<div class="form-field">
|
||||
<h2>
|
||||
{{ 'choices.title' | translate }}
|
||||
</h2>
|
||||
{{ 'dates.add' | translate }}
|
||||
<p>
|
||||
<i>
|
||||
{{ 'choices.helper' | translate }}
|
||||
</i>
|
||||
</p>
|
||||
{{ 'choices.answer_preset_1' | translate }}
|
||||
{{ 'choices.add' | translate }}
|
||||
{{ 'choices.continue' | translate }}
|
||||
<span>
|
||||
<span class="columns">
|
||||
<span class="column">
|
||||
<button class="btn is-primary" (click)="addChoice()">
|
||||
<i class="fa fa-plus"></i>
|
||||
Ajouter un choix
|
||||
</button>
|
||||
</span>
|
||||
<span class="column pull-right">
|
||||
<button class="btn is-warning" (click)="reinitChoices()">
|
||||
<i class="fa fa-recycle"></i>
|
||||
Réinitialiser
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<p class="hint">
|
||||
{{ 'creation.choices_hint' | translate }}
|
||||
</p>
|
||||
<span *ngFor="let choice of choices.controls; let i = index">
|
||||
<div class="form-row" [formGroup]="choice">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<button class="btn btn-warning" (click)="deleteChoiceField(i)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
{{ i * 1 + 1 }})
|
||||
</div>
|
||||
<div class="column">
|
||||
<label [for]="'choice_label_' + i" class="hidden">label</label>
|
||||
<input
|
||||
formControlName="label"
|
||||
[id]="'choice_label_' + i"
|
||||
placeholder="Enter a choice description"
|
||||
(keyup)="keyOnChoice($event, i)"
|
||||
(keyup.backspace)="deleteChoiceField(i)"
|
||||
/>
|
||||
<br />
|
||||
<label [for]="'image_url_' + i" class="hidden">image Url</label>
|
||||
<input
|
||||
formControlName="imageUrl"
|
||||
[id]="'image_url_' + i"
|
||||
placeholder="URL de l' image"
|
||||
(keyup)="keyOnChoice($event, i)"
|
||||
(keyup.backspace)="deleteChoiceField(i)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="" for="creatorEmail">
|
||||
<span>
|
||||
{{ 'creation.name' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="hidden" for="creatorPseudo">
|
||||
<span>
|
||||
{{ 'creation.email' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<input #title matInput placeholder="pseudo" formControlName="creatorPseudo" id="creatorPseudo" required />
|
||||
<input
|
||||
#title
|
||||
matInput
|
||||
placeholder="mon-email@example.com"
|
||||
formControlName="creatorEmail"
|
||||
id="creatorEmail"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<button class="btn is-info" (click)="advancedDisplayEnabled = !advancedDisplayEnabled">
|
||||
<i class="fa fa-save"></i>
|
||||
{{ 'creation.advanced' | translate }}
|
||||
</button>
|
||||
<hr />
|
||||
|
||||
<fieldset class="complete well" *ngIf="advancedDisplayEnabled">
|
||||
<h2>{{ 'creation.advanced' | translate }}</h2>
|
||||
<label for="descr">Description (optionnel)</label>
|
||||
<textarea
|
||||
#description
|
||||
matInput
|
||||
id="descr"
|
||||
placeholder="Description"
|
||||
formControlName="description"
|
||||
required
|
||||
></textarea>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="description.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="description.value = ''"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
|
||||
<br />
|
||||
|
||||
<label for="slug"
|
||||
>Url pour les participants
|
||||
|
||||
<i class="fa fa-close"></i>
|
||||
</label>
|
||||
<br />
|
||||
<span
|
||||
>{{ urlPrefix }}
|
||||
<strong>
|
||||
{{ form.controls.slug.value }}
|
||||
</strong>
|
||||
</span>
|
||||
<app-copy-text [textToCopy]="form.controls.slug.value"></app-copy-text>
|
||||
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="form.controls.slug.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="slug.value = ''"
|
||||
></button>
|
||||
<input #slug matInput id="slug" placeholder="Url" formControlName="slug" required />
|
||||
<br />
|
||||
<div appearance="outline" class="is-not-flex">
|
||||
<mat-label>Nombre de jours avant expiration</mat-label>
|
||||
<input
|
||||
#expiracy
|
||||
id="expiracy"
|
||||
matInput
|
||||
type="number"
|
||||
placeholder="Nombre de jours avant expiration"
|
||||
formControlName="expiracyNumberOfDays"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="expiracy.value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="expiracy.value = ''"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<mat-checkbox class="is-not-flex" formControlName="areResultsPublic">
|
||||
Les participants pourront consulter les résultats
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="isAboutDate">
|
||||
Les choix possibles concerneront des dates
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="isProtectedByPassword">
|
||||
Le sondage sera protégé par un mot de passe
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="isOwnerNotifiedByEmailOnNewVote">
|
||||
Vous recevrez un mail à chaque nouvelle participation
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="isOwnerNotifiedByEmailOnNewComment">
|
||||
Vous recevrez un mail à chaque nouveau commentaire
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-not-flex" formControlName="isMaybeAnswerAvailable">
|
||||
La réponse « peut-être » sera disponible
|
||||
</mat-checkbox>
|
||||
</fieldset>
|
||||
<div class="columns">
|
||||
<div class="column"></div>
|
||||
<div class="column">
|
||||
<button class="btn is-success" (click)="createPoll()" [disabled]="!form.valid || !form.valid">
|
||||
<i class="fa fa-save"></i>
|
||||
Enregistrer le sondage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -83,4 +83,7 @@
|
||||
border-left: $success 3px solid;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.btn {
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { ChangeDetectorRef, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { Poll } from '../../../core/models/poll.model';
|
||||
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { UuidService } from '../../../core/services/uuid.service';
|
||||
import { ApiService } from '../../../core/services/api.service';
|
||||
import { ToastService } from '../../../core/services/toast.service';
|
||||
import { PollService } from '../../../core/services/poll.service';
|
||||
import { DateUtilities } from '../../old-stuff/config/DateUtilities';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { DateChoice, otherDefaultDates } from '../../old-stuff/config/defaultConfigs';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-form',
|
||||
@ -20,16 +18,8 @@ export class FormComponent implements OnInit {
|
||||
public poll?: Poll;
|
||||
public form: FormGroup;
|
||||
|
||||
public urlPrefix: string = window.location.origin + '/participation/';
|
||||
public advancedDisplayEnabled = false;
|
||||
public showDateInterval = true;
|
||||
public allowSeveralHours = true;
|
||||
startDateInterval: string;
|
||||
endDateInterval: string;
|
||||
intervalDays: any;
|
||||
intervalDaysDefault = 7;
|
||||
dateList: any = otherDefaultDates; // sets of days as strings, config to set identical time for days in a special days poll
|
||||
timeList: DateChoice[] = otherDefaultDates; // ranges of time expressed as strings
|
||||
public show_debug_data = false;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
@ -37,291 +27,106 @@ export class FormComponent implements OnInit {
|
||||
private uuidService: UuidService,
|
||||
private toastService: ToastService,
|
||||
private pollService: PollService,
|
||||
public dateUtilities: DateUtilities,
|
||||
private apiService: ApiService,
|
||||
public apiService: ApiService,
|
||||
private router: Router,
|
||||
@Inject(DOCUMENT) private document: any
|
||||
) {}
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
// moveItemInArray(this.choices, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
get choices(): FormArray {
|
||||
return this.form.get('choices') as FormArray;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initFormDefault();
|
||||
// TO remove after
|
||||
// this.createPoll();
|
||||
|
||||
const pollsAvailable = this.pollService.getAllAvailablePolls();
|
||||
console.log('pollsAvailable', pollsAvailable);
|
||||
}
|
||||
|
||||
public createPoll(): void {
|
||||
console.log('this.form', this.form);
|
||||
const newpoll = this.pollService.newPollFromForm(this.form);
|
||||
console.log('newpoll', newpoll);
|
||||
this.apiService.createPoll(newpoll);
|
||||
// if (this.form.valid) {
|
||||
// console.log('Le sondage est correctement rempli, prêt à enregistrer.');
|
||||
// const newpoll = this.pollService.newPollFromForm(this.form);
|
||||
// // TODO : save the poll
|
||||
// this.apiService.createPoll(newpoll);
|
||||
// } else {
|
||||
// this.toastService.display('invalid form');
|
||||
// }
|
||||
}
|
||||
|
||||
public updateSlug(): void {
|
||||
const newValueFormatted = 'TODO';
|
||||
this.form.patchValue({ slug: newValueFormatted });
|
||||
}
|
||||
|
||||
addChoice(optionalLabel = ''): void {
|
||||
const newControlGroup = this.fb.group({
|
||||
label: this.fb.control('', [Validators.required]),
|
||||
imageUrl: ['', [Validators.required]],
|
||||
});
|
||||
|
||||
if (optionalLabel) {
|
||||
newControlGroup.patchValue({
|
||||
label: optionalLabel,
|
||||
imageUrl: 'mon url',
|
||||
});
|
||||
}
|
||||
this.choices.push(newControlGroup);
|
||||
this.cd.detectChanges();
|
||||
console.log('this.choices.length', this.choices.length);
|
||||
|
||||
this.focusOnChoice(this.choices.length - 1);
|
||||
}
|
||||
|
||||
focusOnChoice(index): void {
|
||||
const selector = '#choice_label_' + index;
|
||||
const elem = this.document.querySelector(selector);
|
||||
if (elem) {
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
deleteChoiceField(index: number): void {
|
||||
if (this.choices.length !== 1) {
|
||||
this.choices.removeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
reinitChoices(): void {
|
||||
this.choices.setValue([]);
|
||||
}
|
||||
|
||||
initFormDefault(showDemoValues = true): void {
|
||||
const creationDate = new Date();
|
||||
|
||||
this.form = this.fb.group({
|
||||
title: ['', [Validators.required, Validators.minLength(12)]],
|
||||
creatorPseudo: ['', [Validators.required]],
|
||||
creatorEmail: ['', [Validators.required]],
|
||||
slug: [this.uuidService.getUUID(), [Validators.required]],
|
||||
description: ['', [Validators.required]],
|
||||
choices: new FormArray([]),
|
||||
whoModifiesAnswers: ['', [Validators.required]],
|
||||
whoCanChangeAnswers: ['', [Validators.required]],
|
||||
isAboutDate: [true, [Validators.required]],
|
||||
startDateInterval: ['', [Validators.required]],
|
||||
endDateInterval: ['', [Validators.required]],
|
||||
choices: this.fb.array([
|
||||
this.fb.group({
|
||||
label: ['', [Validators.required]],
|
||||
imageUrl: ['', [Validators.required]],
|
||||
}),
|
||||
]),
|
||||
dateChoices: this.fb.array([
|
||||
this.fb.group({
|
||||
label: ['', [Validators.required]],
|
||||
// if we have enabled detailed time choices per date choice, we have to make a time property for each date choice
|
||||
timeChoices: this.fb.array([
|
||||
this.fb.group({
|
||||
label: ['', [Validators.required]],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
timeChoices: this.fb.array([
|
||||
this.fb.group({
|
||||
label: ['', [Validators.required]],
|
||||
}),
|
||||
]),
|
||||
kind: ['', [Validators.required]],
|
||||
configuration: this.fb.group({
|
||||
areResultsPublic: [true, [Validators.required]],
|
||||
whoCanChangeAnswers: ['everybody', [Validators.required]],
|
||||
isProtectedByPassword: [false, [Validators.required]],
|
||||
isOwnerNotifiedByEmailOnNewVote: [false, [Validators.required]],
|
||||
isOwnerNotifiedByEmailOnNewComment: [false, [Validators.required]],
|
||||
isMaybeAnswerAvailable: [false, [Validators.required]],
|
||||
areResultsPublic: [true, [Validators.required]],
|
||||
expiracyNumberOfDays: [60, [Validators.required, Validators.min(0)]],
|
||||
|
||||
isAboutDate: [true, [Validators.required]],
|
||||
isZeroKnoledge: [false, [Validators.required]],
|
||||
expiresDaysDelay: [60, [Validators.required, Validators.min(1)]],
|
||||
maxCountOfAnswers: [150, [Validators.required, Validators.min(1)]],
|
||||
allowComments: [true, [Validators.required]],
|
||||
password: [this.uuidService.getUUID(), [Validators.required]],
|
||||
dateCreated: [creationDate, [Validators.required]],
|
||||
hasSeveralHours: [true, [Validators.required]],
|
||||
hasMaxCountOfAnswers: [true, [Validators.required, Validators.min(1)]],
|
||||
}),
|
||||
startDateInterval: ['', [Validators.required]],
|
||||
endDateInterval: ['', [Validators.required]],
|
||||
});
|
||||
console.log('this.form ', this.form);
|
||||
this.setDefaultDatesForInterval();
|
||||
|
||||
if (showDemoValues) {
|
||||
this.setDemoValues();
|
||||
this.toastService.display('default values filled for demo');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* default interval of dates proposed is from today to 7 days more
|
||||
*/
|
||||
setDefaultDatesForInterval(): void {
|
||||
const dateCurrent = new Date();
|
||||
const dateJson = dateCurrent.toISOString();
|
||||
this.startDateInterval = dateJson.substring(0, 10);
|
||||
this.endDateInterval = this.dateUtilities
|
||||
.addDaysToDate(this.intervalDaysDefault, dateCurrent)
|
||||
.toISOString()
|
||||
.substring(0, 10);
|
||||
this.form.patchValue({
|
||||
startDateInterval: this.startDateInterval,
|
||||
endDateInterval: this.endDateInterval,
|
||||
});
|
||||
this.countDays();
|
||||
}
|
||||
|
||||
/**
|
||||
* add example values to the form
|
||||
* add example values to the form, overrides defaults of PollConfiguration
|
||||
*/
|
||||
setDemoValues(): void {
|
||||
this.addChoice('orange');
|
||||
this.addChoice('raisin');
|
||||
this.addChoice('abricot');
|
||||
|
||||
this.form.patchValue({
|
||||
title: 'mon titre',
|
||||
title: '',
|
||||
description: 'répondez SVP <3 ! *-* ',
|
||||
slug: this.uuidService.getUUID(),
|
||||
creatorPseudo: 'Chuck Norris',
|
||||
creatorEmail: 'chucknorris@example.com',
|
||||
isAboutDate: true,
|
||||
whoModifiesAnswers: 'everybody',
|
||||
// hasSeveralHours: true,
|
||||
kind: 'date',
|
||||
// TODO aplatir les contrôles
|
||||
configuration: {
|
||||
whoCanChangeAnswers: 'everybody',
|
||||
isProtectedByPassword: false,
|
||||
isOwnerNotifiedByEmailOnNewVote: false,
|
||||
isOwnerNotifiedByEmailOnNewComment: false,
|
||||
isMaybeAnswerAvailable: false,
|
||||
areResultsPublic: true,
|
||||
expiracyNumberOfDays: 60,
|
||||
expiresDaysDelay: 60,
|
||||
},
|
||||
comments: [],
|
||||
choices: [],
|
||||
dateChoices: [],
|
||||
timeChoices: [],
|
||||
});
|
||||
this.automaticSlug();
|
||||
}
|
||||
|
||||
askInitFormDefault(): void {
|
||||
this.initFormDefault(false);
|
||||
this.toastService.display('formulaire réinitialisé');
|
||||
}
|
||||
|
||||
countDays(): void {
|
||||
this.intervalDays = this.dateUtilities.countDays(
|
||||
this.dateUtilities.parseInputDateToDateObject(this.startDateInterval),
|
||||
this.dateUtilities.parseInputDateToDateObject(this.endDateInterval)
|
||||
);
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* add all the dates between the start and end dates in the interval section
|
||||
*/
|
||||
addIntervalOfDates(): void {
|
||||
const newIntervalArray = this.dateUtilities.getDatesInRange(
|
||||
this.dateUtilities.parseInputDateToDateObject(this.startDateInterval),
|
||||
this.dateUtilities.parseInputDateToDateObject(this.endDateInterval),
|
||||
1
|
||||
);
|
||||
|
||||
const converted = [];
|
||||
newIntervalArray.forEach((element) => {
|
||||
converted.push({
|
||||
literal: element.literal,
|
||||
date_object: element.date_object,
|
||||
timeList: [],
|
||||
});
|
||||
});
|
||||
this.dateList = [...new Set(converted)];
|
||||
// add only dates that are not already present with a Set of unique items
|
||||
console.log('this.dateList', this.dateList);
|
||||
this.showDateInterval = false;
|
||||
|
||||
this.form.patchValue({ choices: this.dateList });
|
||||
// this.dateList.forEach(elem=>{
|
||||
// const newControlGroup = this.fb.group({
|
||||
// label: this.fb.control('', [Validators.required]),
|
||||
// imageUrl: ['', [Validators.required]],
|
||||
// });
|
||||
//
|
||||
// this.choices.push(newControlGroup);
|
||||
// })
|
||||
|
||||
this.toastService.display(`les dates ont été ajoutées aux réponses possibles.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle keyboard shortcuts
|
||||
* @param $event
|
||||
* @param choice_number
|
||||
*/
|
||||
keyOnChoice($event: KeyboardEvent, choice_number: number): void {
|
||||
$event.preventDefault();
|
||||
|
||||
console.log('this.choices.length', this.choices.length);
|
||||
console.log('choice_number', choice_number);
|
||||
const lastChoice = this.choices.length - 1 === choice_number;
|
||||
// reset field with Ctrl + D
|
||||
// add a field with Ctrl + N
|
||||
// go to previous choice with arrow up
|
||||
// go to next choice with arrow down
|
||||
console.log('$event', $event);
|
||||
|
||||
if ($event.key == 'ArrowUp' && choice_number > 0) {
|
||||
this.focusOnChoice(choice_number - 1);
|
||||
}
|
||||
if ($event.key == 'ArrowDown') {
|
||||
// add a field if we are on the last choice
|
||||
if (lastChoice) {
|
||||
this.addChoice();
|
||||
this.toastService.display('choix ajouté par raccourci "flèche bas"');
|
||||
} else {
|
||||
this.focusOnChoice(choice_number + 1);
|
||||
}
|
||||
}
|
||||
if ($event.ctrlKey && $event.key == 'Backspace') {
|
||||
this.deleteChoiceField(choice_number);
|
||||
this.toastService.display('choix supprimé par raccourci "Ctrl + retour"');
|
||||
this.cd.detectChanges();
|
||||
this.focusOnChoice(Math.min(choice_number - 1, 0));
|
||||
}
|
||||
if ($event.ctrlKey && $event.key == 'Enter') {
|
||||
// go to other fields
|
||||
const elem = this.document.querySelector('#creatorEmail');
|
||||
if (elem) {
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change time spans
|
||||
*/
|
||||
addTime() {
|
||||
this.timeList.push({
|
||||
literal: '',
|
||||
timeList: [],
|
||||
date_object: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
removeAllTimes() {
|
||||
this.timeList = [];
|
||||
}
|
||||
|
||||
resetTimes() {
|
||||
this.timeList = otherDefaultDates;
|
||||
}
|
||||
|
||||
/**
|
||||
* add a time period to a specific date choice,
|
||||
* focus on the new input
|
||||
* @param config
|
||||
* @param id
|
||||
*/
|
||||
addTimeToDate(config: any, id: number) {
|
||||
this.timeList.push({
|
||||
literal: '',
|
||||
timeList: [],
|
||||
date_object: new Date(),
|
||||
});
|
||||
const selector = '[ng-reflect-choice_label="dateTime_' + id + '_Choices_' + (this.timeList.length - 1) + '"]';
|
||||
this.cd.detectChanges();
|
||||
const elem = this.document.querySelector(selector);
|
||||
if (elem) {
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set the poll slug from other data of the poll
|
||||
*/
|
||||
automaticSlug() {
|
||||
this.poll.slug = this.pollService.makeSlug(this.poll);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
<div class="kind-select form-field">
|
||||
<form [formGroup]="form">
|
||||
<div class="kind-of-poll columns">
|
||||
<div class="column" *ngIf="template_questions_answers">
|
||||
<!-- version maquette questions réponses-->
|
||||
<select name="type" id="type" class="input" formControlName="kind">
|
||||
<option [value]="'date'">{{ 'creation.kind.date' | translate }}</option>
|
||||
<option [value]="'classic'">{{ 'creation.kind.classic' | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- version avec un choix de bouton-->
|
||||
<div class="columns" *ngIf="!template_questions_answers">
|
||||
<div class="column">
|
||||
<button
|
||||
class="btn-block btn"
|
||||
[ngClass]="{ 'is-primary': form.value.configuration.isAboutDate }"
|
||||
(click)="
|
||||
form.patchValue({
|
||||
configuration: {
|
||||
isAboutDate: true
|
||||
}
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fa fa-calendar"></i>
|
||||
{{ 'creation.kind.date' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="column">
|
||||
<button
|
||||
class="btn-block btn btn-default"
|
||||
[ngClass]="{ 'is-primary': !form.value.configuration.isAboutDate }"
|
||||
(click)="
|
||||
form.patchValue({
|
||||
configuration: {
|
||||
isAboutDate: false
|
||||
}
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="fa fa-stats"></i>
|
||||
{{ 'creation.kind.classic' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { KindSelectComponent } from './kind-select.component';
|
||||
|
||||
describe('KindSelectComponent', () => {
|
||||
let component: KindSelectComponent;
|
||||
let fixture: ComponentFixture<KindSelectComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [KindSelectComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(KindSelectComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Poll } from '../../../../core/models/poll.model';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-kind-select',
|
||||
templateUrl: './kind-select.component.html',
|
||||
styleUrls: ['./kind-select.component.scss'],
|
||||
})
|
||||
export class KindSelectComponent implements OnInit {
|
||||
public template_questions_answers = true;
|
||||
@Input()
|
||||
public poll?: Poll;
|
||||
@Input()
|
||||
public form: FormGroup;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<div class="form-field">
|
||||
<form [formGroup]="form">
|
||||
<span class="columns">
|
||||
<span class="column">
|
||||
<button class="btn is-primary" (click)="addChoice()">
|
||||
<i class="fa fa-plus"></i>
|
||||
Ajouter un choix
|
||||
</button>
|
||||
</span>
|
||||
<span class="column pull-right">
|
||||
<button class="btn is-warning" (click)="reinitChoices()">
|
||||
<i class="fa fa-recycle"></i>
|
||||
Réinitialiser
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="hint">
|
||||
{{ 'creation.choices_hint' | translate }}
|
||||
</span>
|
||||
<div *ngFor="let choice of choices; let i = index">
|
||||
<div class="form-row" [formGroup]="choice">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<button class="btn btn-warning" (click)="deleteChoiceField(i)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
{{ i * 1 + 1 }})
|
||||
</div>
|
||||
<div class="column">
|
||||
<label [for]="'choice_label_' + i" class="hidden">label</label>
|
||||
<input
|
||||
formControlName="label"
|
||||
[id]="'choice_label_' + i"
|
||||
placeholder="Enter a choice description"
|
||||
(keyup)="keyOnChoice($event, i)"
|
||||
(keyup.backspace)="deleteChoiceField(i)"
|
||||
/>
|
||||
<br />
|
||||
<label [for]="'image_url_' + i" class="hidden">image Url</label>
|
||||
<input
|
||||
formControlName="imageUrl"
|
||||
[id]="'image_url_' + i"
|
||||
placeholder="URL de l' image"
|
||||
(keyup)="keyOnChoice($event, i)"
|
||||
(keyup.backspace)="deleteChoiceField(i)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TextSelectComponent } from './text-select.component';
|
||||
|
||||
describe('TextSelectComponent', () => {
|
||||
let component: TextSelectComponent;
|
||||
let fixture: ComponentFixture<TextSelectComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TextSelectComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TextSelectComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-text-select',
|
||||
templateUrl: './text-select.component.html',
|
||||
styleUrls: ['./text-select.component.scss'],
|
||||
})
|
||||
export class TextSelectComponent implements OnInit {
|
||||
@Input()
|
||||
public form: FormGroup;
|
||||
public choices = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
reinitChoices(): void {}
|
||||
addChoice(): void {}
|
||||
deleteChoiceField(i): void {}
|
||||
keyOnChoice($event, i): void {}
|
||||
}
|
@ -72,22 +72,22 @@
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<mat-checkbox class="is-flex" formControlName="areResultsPublic">
|
||||
<mat-checkbox class="is-flex" formControlName="configuration.areResultsPublic">
|
||||
Les participants pourront consulter les résultats
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-flex" formControlName="isAboutDate">
|
||||
<mat-checkbox class="is-flex" formControlName="configuration.isAboutDate">
|
||||
Les choix possibles concerneront des dates
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-flex" formControlName="isProtectedByPassword">
|
||||
<mat-checkbox class="is-flex" formControlName="configuration.isProtectedByPassword">
|
||||
Le sondage sera protégé par un mot de passe
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-flex" formControlName="isOwnerNotifiedByEmailOnNewVote">
|
||||
<mat-checkbox class="is-flex" formControlName="configuration.isOwnerNotifiedByEmailOnNewVote">
|
||||
Vous recevrez un mail à chaque nouvelle participation
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-flex" formControlName="isOwnerNotifiedByEmailOnNewComment">
|
||||
<mat-checkbox class="is-flex" formControlName="configuration.isOwnerNotifiedByEmailOnNewComment">
|
||||
Vous recevrez un mail à chaque nouveau commentaire
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="is-flex" formControlName="isMaybeAnswerAvailable">
|
||||
<mat-checkbox class="is-flex" formControlName="configuration.isMaybeAnswerAvailable">
|
||||
La réponse « peut-être » sera disponible
|
||||
</mat-checkbox>
|
||||
|
||||
|
@ -50,7 +50,7 @@ export class StepperComponent implements OnInit {
|
||||
],
|
||||
areResultsPublic: [this.poll ? this.poll.configuration.areResultsPublic : true, [Validators.required]],
|
||||
expiracyNumberOfDays: [
|
||||
this.poll ? DateService.diffInDays(new Date(), this.poll.configuration.expires) : 60,
|
||||
this.poll ? DateService.diffInDays(new Date(), this.poll.configuration.expiracyDate) : 60,
|
||||
[Validators.required],
|
||||
],
|
||||
});
|
||||
|
@ -0,0 +1,16 @@
|
||||
<section class="hero is-medium is-success">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Création de sondage réussie
|
||||
</h1>
|
||||
<h2 class="subtitle">
|
||||
Bravo! partagez le lien de votre sondage. Un récapitulatif a été envoyé à votre adresse email.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<br />
|
||||
<section class="text-center">
|
||||
<img src="assets/img/undraw_group_selfie_ijc6.svg" alt="image succès" />
|
||||
</section>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SuccessComponent } from './success.component';
|
||||
|
||||
describe('SuccessComponent', () => {
|
||||
let component: SuccessComponent;
|
||||
let fixture: ComponentFixture<SuccessComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SuccessComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SuccessComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
12
src/app/features/administration/success/success.component.ts
Normal file
12
src/app/features/administration/success/success.component.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-success',
|
||||
templateUrl: './success.component.html',
|
||||
styleUrls: ['./success.component.scss'],
|
||||
})
|
||||
export class SuccessComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">{{ poll.title }}</p>
|
||||
<p class="card-header-icon">author : {{ poll.owner?.pseudo }}</p>
|
||||
<!-- <p class="card-header-icon">author : {{ poll.owner?.pseudo }}</p>-->
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
|
@ -62,7 +62,6 @@ export class PollConfig {
|
||||
adminKey = ''; // key to change config of the poll
|
||||
owner_modifier_token = ''; // key to change a vote stack
|
||||
canModifyAnswers = true; // bool for the frontend selector
|
||||
whoModifiesAnswers = 'everybody'; // everybody, self, nobody (= just admin)
|
||||
whoCanChangeAnswers = 'everybody'; // everybody, self, nobody (= just admin)
|
||||
dateList: any = otherDefaultDates; // sets of days as strings, config to set identical time for days in a special days poll
|
||||
timeList: DateChoice[] = otherDefaultDates; // ranges of time expressed as strings
|
||||
|
@ -73,7 +73,7 @@
|
||||
name="modificationScope"
|
||||
id="modificationScope"
|
||||
*ngIf="true == !!config.canModifyAnswers"
|
||||
[(ngModel)]="config.whoModifiesAnswers"
|
||||
[(ngModel)]="config.whoCanChangeAnswers"
|
||||
[disabled]="false == !!config.canModifyAnswers"
|
||||
>
|
||||
<option value="self">
|
||||
|
@ -84,7 +84,6 @@ export class ConfigService extends PollConfig {
|
||||
password: this.password,
|
||||
customUrl: this.customUrl,
|
||||
canModifyAnswers: this.canModifyAnswers,
|
||||
whoModifiesAnswers: this.whoModifiesAnswers,
|
||||
dateList: this.dateList,
|
||||
timeList: this.timeList,
|
||||
answers: this.answers,
|
||||
|
@ -0,0 +1,26 @@
|
||||
<div class="static-page content">
|
||||
<article>
|
||||
<section class="hero is-info">
|
||||
<div class="hero-body">
|
||||
<p class="title">
|
||||
Conditions Générales d'utilisation
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
Détail des CGU.
|
||||
<br />
|
||||
Ne nous prenez pas pour des chatons.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="hero is-info">
|
||||
<div class="hero-body">
|
||||
<p class="title">
|
||||
Conditions Particulières d'utilisation
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
Détail des CPU
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CguComponent } from './cgu.component';
|
||||
|
||||
describe('CguComponent', () => {
|
||||
let component: CguComponent;
|
||||
let fixture: ComponentFixture<CguComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CguComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CguComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cgu',
|
||||
templateUrl: './cgu.component.html',
|
||||
styleUrls: ['./cgu.component.scss'],
|
||||
})
|
||||
export class CguComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
@ -0,0 +1,307 @@
|
||||
<div class="ciphering padded">
|
||||
<h2 class="title">
|
||||
Essais de chiffrement pour un framadate zéro knoledge
|
||||
</h2>
|
||||
<section class="boxed-shadow">
|
||||
<h3 class="title">
|
||||
Chiffrement par matrice
|
||||
</h3>
|
||||
Voici un texte que l'on voudrait chiffrer: <br />
|
||||
<input type="text" [(ngModel)]="plainText" (ngModelChange)="encrypt(codingMatrice, plainText)" /><br />
|
||||
Vous pouvez le changer, la page se met à jour automatiquement. Le texte clair fait
|
||||
{{ plainText.length }} caractères.
|
||||
<br />
|
||||
Nous avons besoin d'une matrice de codage de 4 nombres.
|
||||
<br />
|
||||
<input type="text" [(ngModel)]="codingMatrice" />
|
||||
|
||||
<br />
|
||||
<button class="btn is-primary" (click)="encrypt(codingMatrice, plainText)">
|
||||
chiffrer avec la matrice
|
||||
</button>
|
||||
<br />
|
||||
Et voilà la résultat:
|
||||
<strong class="cipher-result">
|
||||
<app-copy-text [textToCopy]="cipheredTextCIL" [displayLabelButton]="false"></app-copy-text>
|
||||
{{ cipheredTextCIL }}
|
||||
</strong>
|
||||
|
||||
<h4 class="title">Comment on a fait?</h4>
|
||||
|
||||
Voici notre alphabet autorisé ( {{ alphab.length }} caractères):
|
||||
<pre>{{ alphab }}</pre>
|
||||
Tout caractère qui n'est pas présent dans notre alphabet sera converti en souligné "_".
|
||||
<br />
|
||||
Notre texte après vérification : <code>{{ filterCharacters(plainText) }}</code>
|
||||
<br />
|
||||
On sépare le texte en {{ separatedCouples.length }} couples de caractères pour le mettre dans une matrice. On
|
||||
prend la pilule rouge.
|
||||
<br />
|
||||
<span class="couple btn is-info" *ngFor="let c of separatedCouples"> [{{ c[0] }} , {{ c[1] }}] </span>
|
||||
<br />
|
||||
Pour chaque couple de caractères comme
|
||||
<span class="couple btn is-info">[{{ separatedCouples[0][0] }} , {{ separatedCouples[0][1] }}]</span>
|
||||
<br />
|
||||
on crée le couple d'indices de ces caractères.
|
||||
<code
|
||||
>[
|
||||
{{ dico.indexOf(separatedCouples[0][0]) }}
|
||||
,
|
||||
{{ dico.indexOf(separatedCouples[0][1]) }}
|
||||
]</code
|
||||
>
|
||||
<br />
|
||||
On
|
||||
<a href="https://fr.wikipedia.org/wiki/Produit_matriciel#Produit_matriciel_ordinaire">
|
||||
applique la matrice par produit
|
||||
</a>
|
||||
|
||||
<code>
|
||||
{{ convertStringToMatrix(codingMatrice) }}
|
||||
</code>
|
||||
au couple d'indices, ce qui donne:
|
||||
<br />
|
||||
<button class="btn is-info">
|
||||
{{
|
||||
applique(codingMatrice, [dico.indexOf(separatedCouples[0][0]), dico.indexOf(separatedCouples[0][1])])
|
||||
| json
|
||||
}}
|
||||
</button>
|
||||
<br />
|
||||
et on re convertit les indices en caractères selon notre dictionnaire.
|
||||
<button class="btn is-info">
|
||||
{{
|
||||
alphab[
|
||||
applique(codingMatrice, [
|
||||
dico.indexOf(separatedCouples[0][0]),
|
||||
dico.indexOf(separatedCouples[0][1])
|
||||
])[0]
|
||||
]
|
||||
}}
|
||||
{{
|
||||
alphab[
|
||||
applique(codingMatrice, [
|
||||
dico.indexOf(separatedCouples[0][0]),
|
||||
dico.indexOf(separatedCouples[0][1])
|
||||
])[1]
|
||||
]
|
||||
}}
|
||||
</button>
|
||||
<br />
|
||||
on accumule ces lettres converties dans la chaine msgout et on obtient tout notre texte chiffré.
|
||||
<code>
|
||||
{{ cipheredTextCIL }}
|
||||
</code>
|
||||
</section>
|
||||
<section class="boxed-shadow">
|
||||
<h3 class="title">1) Chiffrement simple</h3>
|
||||
<div class="notification is-info">
|
||||
Les ordinateurs font des opérations très rapidement sur des nombres, pour coder et décoder des textes, il
|
||||
suffit donc de les convertir en nombre et faire des opérations plus ou moins complexes avec.
|
||||
</div>
|
||||
Voici un texte que l'on voudrait chiffrer: <br />
|
||||
<input type="text" [(ngModel)]="plainText" (ngModelChange)="encrypt(codingMatrice, plainText)" />
|
||||
<br />
|
||||
Vous pouvez le changer, la page se met à jour automatiquement. Le texte clair fait
|
||||
{{ plainText.length }} caractères.
|
||||
|
||||
<br />
|
||||
Texte chiffré très simplement: on assigne un numéro à chaque caractère, séparé par un tiret en sortie.
|
||||
Javascript permet de faire ceci avec <code>monTexte.charCodeAt(1)</code> pour obtenir le code du caractère 1 de
|
||||
notre texte à chiffrer. Exemple:
|
||||
<ul>
|
||||
<li>
|
||||
Le premier caractère <code>{{ plainText[1] }}</code> de notre texte <code>{{ plainText }}</code> a pour
|
||||
numéro de caractère <i class="fa fa-arrow-right"></i> <code>{{ plainText.charCodeAt(1) }}</code
|
||||
>. <br />
|
||||
Voici ce que ça donne pour tout notre texte:
|
||||
</li>
|
||||
<li *ngFor="let char of plainText.split(''); index as i">
|
||||
Le caractère n° <code>{{ i }}</code> de notre texte est <code>{{ char }}</code> a pour numéro de
|
||||
caractère <i class="fa fa-arrow-right"></i> <code>{{ plainText.charCodeAt(i) }}</code
|
||||
>.
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
On peut donc convertir notre texte en cette suite de nombres:
|
||||
<strong class="cipher-result">
|
||||
<app-copy-text [textToCopy]="simpleCipheredText" [displayLabelButton]="false"></app-copy-text>
|
||||
<span>
|
||||
{{ simpleCipheredText }}
|
||||
</span>
|
||||
</strong>
|
||||
<br />
|
||||
<h3 class="title">2) Avec un peu de sel?</h3>
|
||||
<input type="text" [(ngModel)]="salt" />
|
||||
({{ salt.length }} caractères)
|
||||
<br />
|
||||
simple ciphered avec un sel. on ajoute le sel avant le texte puis on passe le tout dans la conversion comme
|
||||
ci-dessus. on obtient un message de {{ plainText.length }} + {{ salt.length }} caractères convertis en
|
||||
{{ plainText.length + salt.length }} nombres séparés par des tirets.
|
||||
<strong class="cipher-result">
|
||||
<app-copy-text [textToCopy]="simpleCipheredTextWithSalt" [displayLabelButton]="false"></app-copy-text>
|
||||
{{ simpleCipheredTextWithSalt }}
|
||||
</strong>
|
||||
<h3 class="title">3) On hashe le sel</h3>
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
Pour gagner en aléatoire on peut passer le sel dans une fonction de hashage cryptographique, telle que
|
||||
MD5 (Message Digest 5).
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/38/Md5_generalview.png/330px-Md5_generalview.png"
|
||||
alt="fonctionnement de MD5"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
Voici ce que le hashage du sel donne:
|
||||
<code>
|
||||
{{ md5(salt) }}
|
||||
</code>
|
||||
({{ md5(salt).length }}caractères)
|
||||
<br />
|
||||
Le nouveau texte chiffré avec le sel hashé est donc:
|
||||
<strong class="cipher-result">
|
||||
<app-copy-text
|
||||
[textToCopy]="simpleCipheredTextWithHashedSalt"
|
||||
[displayLabelButton]="false"
|
||||
></app-copy-text>
|
||||
{{ simpleCipheredTextWithHashedSalt }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="title">4) Légèrement épicé</h3>
|
||||
simple chiffrement avec un sel avant et un saupoudrage du sel sur chacun des caractères de la phrase. la valeur
|
||||
du caractère est ajoutée à la valeur du caractère du sel correspondant. une fois arrivé à la fin du sel on
|
||||
reprend au début pour saupoudrer toute la phrase à chiffrer. Ce qui donne
|
||||
{{ simpleCipheredTextWithSpreadSalt.split('-').length }} chiffres.
|
||||
<strong class="cipher-result">
|
||||
<app-copy-text [textToCopy]="simpleCipheredTextWithSpreadSalt" [displayLabelButton]="false"></app-copy-text>
|
||||
{{ simpleCipheredTextWithSpreadSalt }}
|
||||
</strong>
|
||||
<br />
|
||||
|
||||
<h3 class="title">5) On en remet une couche</h3>
|
||||
Les nombres que l'on a obtenu peuvent être transmis après une opération de conversion en caractères. On a donc
|
||||
autant de caractères que juste avant. ça ressemble déjà plus à un message pas évident à déchiffrer. Le nouveau
|
||||
texte fait {{ layerOnSpread.length }} caractères.
|
||||
<strong class="cipher-result">
|
||||
<app-copy-text [textToCopy]="layerOnSpread" [displayLabelButton]="false"></app-copy-text>
|
||||
{{ layerOnSpread }}
|
||||
</strong>
|
||||
</section>
|
||||
<section class="boxed-shadow">
|
||||
<h3 class="title">Déchiffrement simple</h3>
|
||||
|
||||
Collez ici le texte chiffré à convertir en truc lisible par l'opération
|
||||
<s>du saint esprit</s> réalisée dans l'autre sens.
|
||||
<br />
|
||||
<input type="text" [(ngModel)]="cipheredText" />
|
||||
<br />
|
||||
Simple texte déchiffré. Il suffit de séparer les numéros en caractère et trouver à quel caractère correspond une
|
||||
autre pour trouver n'importe quel caractère ayant le même chiffre.
|
||||
<strong class="cipher-result">
|
||||
{{ simpleDeCipheredText }}
|
||||
</strong>
|
||||
<br />
|
||||
simple texte déchiffré avec sel, en enlevant les {{ salt.length }} premiers caractères, ceux correspondant au
|
||||
sel.
|
||||
<!-- <strong class="cipher-result">-->
|
||||
<!-- {{ simpleDeCipheredTextWithSalt }}-->
|
||||
<!-- </strong>-->
|
||||
<br />
|
||||
Simple texte déchiffré avec sel saupoudré, en enlevant à chaque caractère la valeur du caractère courant du sel.
|
||||
Sans connaître le sel il est déjà plus difficile de deviner la phrase, mais des régularités apparaissent encore.
|
||||
<!-- <strong class="cipher-result">-->
|
||||
<!-- {{ simpleDeCipheredTextWithSpreadSalt }}-->
|
||||
<!-- </strong>-->
|
||||
</section>
|
||||
<section class="boxed-shadow entropy">
|
||||
<h2>Calcul d'entropie de Shannon</h2>
|
||||
Un nombre élevé est signe de difficulté à deviner par calcul.
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>score d'entropie</td>
|
||||
<td>texte à évaluer</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tr>
|
||||
<td>{{ getEntropy(plainText).toFixed(3) }}</td>
|
||||
<td>
|
||||
<input type="text" [(ngModel)]="plainText" (ngModelChange)="encrypt(codingMatrice, plainText)" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ getEntropy(simpleCipheredText).toFixed(3) }}</td>
|
||||
<td>
|
||||
<p class="limited-text">{{ simpleCipheredText }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ getEntropy(simpleCipheredTextWithSalt).toFixed(3) }}</td>
|
||||
<td>
|
||||
<p class="limited-text">{{ simpleCipheredTextWithSalt }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ getEntropy(simpleCipheredTextWithHashedSalt).toFixed(3) }}</td>
|
||||
<td>
|
||||
<p class="limited-text">{{ simpleCipheredTextWithHashedSalt }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ getEntropy(convertIntToText(simpleCipheredTextWithHashedSalt)).toFixed(3) }}</td>
|
||||
<td>
|
||||
<p class="limited-text">{{ convertIntToText(simpleCipheredTextWithHashedSalt) }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ getEntropy(simpleCipheredTextWithHashedSpreadSalt).toFixed(3) }}</td>
|
||||
<td>
|
||||
<p class="limited-text">{{ simpleCipheredTextWithHashedSpreadSalt }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ getEntropy(layerOnSpread).toFixed(3) }}</td>
|
||||
<td>
|
||||
<p class="limited-text">{{ layerOnSpread }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{
|
||||
getEntropy(
|
||||
'KZ%feLx!D2qppSW3MEzcuMxDFpbS5&%vunsn5MpF8&VyM822Fg$$jU7Ue6PmFtujv5@ToFNp$P*3#PwS@3JAtnXFLE%9io7N23Q$Y&$&DoXEW&GsM6#Rb6m5$mvSpXAA'
|
||||
).toFixed(3)
|
||||
}}
|
||||
</td>
|
||||
<td>
|
||||
<p class="limited-text">
|
||||
comparaison avec une phrase de passe générée.
|
||||
{{
|
||||
'KZ%feLx!D2qppSW3MEzcuMxDFpbS5&%vunsn5MpF8&VyM822Fg$$jU7Ue6PmFtujv5@ToFNp$P*3#PwS@3JAtnXFLE%9io7N23Q$Y&$&DoXEW&GsM6#Rb6m5$mvSpXAA'
|
||||
}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="is-boxed">
|
||||
<h3>Chiffrement AES</h3>
|
||||
|
||||
<app-wip-todo></app-wip-todo>
|
||||
<!-- <input type="text" [(ngModel)]="cipheredText" />-->
|
||||
<!-- texte avancé à déchiffrer-->
|
||||
<!-- <br />-->
|
||||
<!-- <input type="text" [(ngModel)]="salt" />-->
|
||||
<!-- salt-->
|
||||
<!-- <input type="text" [(ngModel)]="initial_vector" />-->
|
||||
<!-- initial_vector-->
|
||||
<!-- <br />-->
|
||||
<!-- <input type="text" [(ngModel)]="key" />-->
|
||||
<!-- key-->
|
||||
</section>
|
||||
</div>
|
@ -0,0 +1,42 @@
|
||||
.boxed-shadow {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
max-width: 85%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cipher-result {
|
||||
border-radius: 3px;
|
||||
background: #00003b;
|
||||
color: #7d6c99;
|
||||
padding: 0.5em;
|
||||
margin-left: 1em;
|
||||
min-width: 10ch;
|
||||
min-height: 2em;
|
||||
max-width: 80%;
|
||||
overflow-x: scroll;
|
||||
line-height: 2em;
|
||||
display: flex;
|
||||
app-copy-text {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.entropy {
|
||||
.limited-text {
|
||||
max-width: 50% !important;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: 4em;
|
||||
padding: 1em;
|
||||
display: block;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CipheringComponent } from './ciphering.component';
|
||||
|
||||
describe('CipheringComponent', () => {
|
||||
let component: CipheringComponent;
|
||||
let fixture: ComponentFixture<CipheringComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CipheringComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CipheringComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,306 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import CipherLib from './lib_cipher';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
||||
// documentation:
|
||||
// https://medium.com/@spatocode/symmetric-encryption-in-javascript-bcb5fd14c273
|
||||
// https://nodejs.org/api/crypto.html
|
||||
// https://preview.npmjs.com/package/ecma-nacl
|
||||
@Component({
|
||||
selector: 'app-ciphering',
|
||||
templateUrl: './ciphering.component.html',
|
||||
styleUrls: ['./ciphering.component.scss'],
|
||||
})
|
||||
export class CipheringComponent implements OnInit {
|
||||
public plainText = 'coucou !';
|
||||
// public plainText = '1234';
|
||||
public cipheredText =
|
||||
'121-127-42-110-127-42-122-121-115-128-124-111-125-107-118-127-126-42-118-111-125-42-122-115-122-121-118-111';
|
||||
public algorithm = 'aes-192-cbc';
|
||||
public salt = 'ou du poivre heh';
|
||||
public initial_vector = '';
|
||||
public key;
|
||||
public otherCipheredText = 'le texte à chiffrer, coucou !';
|
||||
public alphab: string;
|
||||
public prem: number;
|
||||
public dico: string[];
|
||||
public separatedCouples: any[];
|
||||
public codingMatrice = '2 3 5 8';
|
||||
public cipheredTextCIL: string;
|
||||
|
||||
constructor() {
|
||||
this.alphab = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .+-*/_!?,éèêëàçôî'âù()@&$";
|
||||
this.prem = this.alphab.length;
|
||||
this.dico = this.alphab.split('');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.encrypt(this.codingMatrice, this.plainText);
|
||||
}
|
||||
|
||||
// ---------------------- make unreadable ----------------------
|
||||
get simpleCipheredText() {
|
||||
return this.convertTextToInt(this.plainText);
|
||||
}
|
||||
|
||||
get simpleCipheredTextWithSalt() {
|
||||
return this.convertTextToInt(this.salt + this.plainText);
|
||||
}
|
||||
|
||||
get simpleCipheredTextWithSpreadSalt() {
|
||||
return this.convertTextToIntMixedSalt(this.salt + this.plainText);
|
||||
}
|
||||
|
||||
get simpleCipheredTextWithHashedSalt() {
|
||||
return this.convertTextToIntMixedSalt(this.md5(this.salt) + this.plainText);
|
||||
}
|
||||
|
||||
get simpleCipheredTextWithHashedSpreadSalt() {
|
||||
return this.convertTextToIntMixedSalt(this.md5(this.salt) + this.plainText);
|
||||
}
|
||||
|
||||
get layerOnSpread() {
|
||||
return this.convertIntToText(this.simpleCipheredTextWithHashedSalt, false);
|
||||
}
|
||||
|
||||
// ---------------------- make readable ----------------------
|
||||
get simpleDeCipheredText() {
|
||||
return this.convertIntToText(this.cipheredText);
|
||||
}
|
||||
|
||||
get simpleDeCipheredTextWithSalt() {
|
||||
return this.convertIntToText(this.salt + this.cipheredText);
|
||||
}
|
||||
|
||||
get simpleDeCipheredTextWithSpreadSalt() {
|
||||
return this.convertIntToTextMixedSalt(this.salt + this.cipheredText);
|
||||
}
|
||||
|
||||
// ---------------------- conversions ----------------------
|
||||
|
||||
/**
|
||||
* chiffrer avec la méthode du CIL-gometz
|
||||
* avec une matrice
|
||||
* @param someText
|
||||
*/
|
||||
encrypt(matriceDefinition, messageToCipher) {
|
||||
if (!matriceDefinition || !messageToCipher) {
|
||||
return null;
|
||||
}
|
||||
let cipheredText = '';
|
||||
const lcpl = this.separecpl(this.filterCharacters(messageToCipher));
|
||||
this.separatedCouples = lcpl;
|
||||
console.log('lcpl', lcpl);
|
||||
console.log('this.dico', this.dico);
|
||||
/**
|
||||
* Pour chaque couple de caractères
|
||||
* on crée le couple d'indices de ces caractères
|
||||
* On applique la matrice au couple d'indices
|
||||
* et on en déduit le couple de caractères codés (ou décodés)
|
||||
* qu'on accumule dans la chaine msgout
|
||||
*/
|
||||
lcpl.forEach((couple) => {
|
||||
console.log('couple', couple);
|
||||
const cplnum = [this.dico.indexOf(couple[0]), this.dico.indexOf(couple[1])];
|
||||
console.log('cplnum', cplnum);
|
||||
const cplnumout = this.applique(matriceDefinition, cplnum);
|
||||
console.log('cplnumout', cplnumout);
|
||||
const cplout = [this.alphab[cplnumout[0]], this.alphab[cplnumout[1]]];
|
||||
console.log('cplout', cplout);
|
||||
cipheredText += cplout[0];
|
||||
cipheredText += cplout[1];
|
||||
});
|
||||
console.log('cipheredText', cipheredText);
|
||||
this.cipheredTextCIL = cipheredText;
|
||||
return cipheredText;
|
||||
}
|
||||
|
||||
/**
|
||||
* séparation d'une chaine de caractères en liste de couples de caractères
|
||||
* @param msg
|
||||
*/
|
||||
separecpl(msg) {
|
||||
/**
|
||||
* Produit une liste formée des couples de caractères de la chaine
|
||||
msg reçue en argument.
|
||||
Dans le cas où le nb de caractères de la chaine est impair,
|
||||
un underscore est ajouté en fin de chaine de façon à pouvoir
|
||||
inclure le dernier caractère dans un couple.
|
||||
*/
|
||||
const nbcar = msg.length;
|
||||
if (nbcar % 2) {
|
||||
msg += '_';
|
||||
}
|
||||
const coupleList = [];
|
||||
let i = 0;
|
||||
while (i < nbcar) {
|
||||
coupleList.push([msg[i], msg[i + 1]]);
|
||||
i += 2;
|
||||
}
|
||||
return coupleList;
|
||||
}
|
||||
|
||||
/**
|
||||
* calcul de l'inverse de d dans Z/pZ
|
||||
* @param d
|
||||
* @param p
|
||||
*/
|
||||
invdet(d, p) {
|
||||
let inv = 1;
|
||||
while ((d * inv) % p !== 1) {
|
||||
inv++;
|
||||
}
|
||||
return inv;
|
||||
}
|
||||
|
||||
/**
|
||||
* inversion de matrice
|
||||
* @param matriceString
|
||||
* @param p
|
||||
*/
|
||||
invmat(matriceString: string, p) {
|
||||
const [a, b, c, d] = this.convertStringToMatrix(matriceString);
|
||||
const mul = this.invdet(a * d - b * c, p);
|
||||
return [(d * mul) % p, (-b * mul) % p, (-c * mul) % p, (a * mul) % p];
|
||||
}
|
||||
|
||||
applique(mat, cplnum) {
|
||||
const [a, b, c, d] = this.convertStringToMatrix(mat);
|
||||
const [x, y] = cplnum;
|
||||
const xout = (a * x + b * y) % this.prem;
|
||||
const yout = (c * x + d * y) % this.prem;
|
||||
return [xout, yout];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pour chaque caractère de la chaine msg, on vérifie qu'il est
|
||||
dans l'alphabet des caractères autorisés : si oui on le garde,
|
||||
sinon on le remplace par un underscore
|
||||
* @param monTexte
|
||||
*/
|
||||
filterCharacters(monTexte) {
|
||||
const convertedText = monTexte.split('').map((letter) => {
|
||||
if (this.alphab.indexOf(letter) === -1) {
|
||||
letter = '_';
|
||||
}
|
||||
return letter;
|
||||
});
|
||||
|
||||
return convertedText.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* on entre une définition de matrice en écrivant 4 nombres dans un input
|
||||
* @param stringMatrix
|
||||
*/
|
||||
convertStringToMatrix(stringMatrix: string): number[] {
|
||||
return stringMatrix
|
||||
.replace(',', ' ')
|
||||
.replace(' ', ' ')
|
||||
.split(' ')
|
||||
.map((elem) => parseInt(elem));
|
||||
}
|
||||
|
||||
/**
|
||||
* chiffrer avec la méthode du CIL-gometz
|
||||
* @param someText
|
||||
*/
|
||||
decrypt(sometext) {
|
||||
return sometext;
|
||||
}
|
||||
|
||||
convertTextToInt(value) {
|
||||
let result = '';
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (i < value.length - 1) {
|
||||
result += value.charCodeAt(i) + 10;
|
||||
result += '-';
|
||||
} else {
|
||||
result += value.charCodeAt(i) + 10;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
convertIntToText(value, removeBeginning = true) {
|
||||
let result = '';
|
||||
const array = value.split('-');
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
result += String.fromCharCode(array[i] - 10);
|
||||
}
|
||||
// remove salt characters
|
||||
if (removeBeginning) {
|
||||
result = result.slice(this.salt.length, -1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
convertTextToIntMixedSalt(value) {
|
||||
let result = '';
|
||||
let saltIndex = 0;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (i < value.length - 1) {
|
||||
result += value.charCodeAt(i) + 10 + this.salt.charCodeAt(saltIndex);
|
||||
result += '-';
|
||||
} else {
|
||||
result += value.charCodeAt(i) + 10 + this.salt.charCodeAt(saltIndex);
|
||||
}
|
||||
saltIndex++;
|
||||
if (saltIndex >= this.salt.length) {
|
||||
saltIndex = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
convertIntToTextMixedSalt(value) {
|
||||
let result = '';
|
||||
const array = value.split('-');
|
||||
let saltIndex = -1;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
// console.log(
|
||||
// 'encodage',
|
||||
// i,
|
||||
// value.charCodeAt(i),
|
||||
// saltIndex,
|
||||
// this.salt.charCodeAt(saltIndex),
|
||||
// value.charCodeAt(i) + 10 + this.salt.charCodeAt(saltIndex)
|
||||
// );
|
||||
|
||||
result += String.fromCharCode(array[i] - 10 - this.salt.charCodeAt(saltIndex));
|
||||
if (saltIndex > this.salt.length) {
|
||||
saltIndex = 0;
|
||||
} else {
|
||||
saltIndex++;
|
||||
}
|
||||
}
|
||||
// remove salt characters
|
||||
return result.slice(this.salt.length);
|
||||
}
|
||||
|
||||
md5(someText) {
|
||||
return CipherLib.md5(someText);
|
||||
}
|
||||
|
||||
getEntropy(s) {
|
||||
let sum = 0;
|
||||
const len = s.length;
|
||||
this.process(s, (k, f) => {
|
||||
const p = f / len;
|
||||
sum -= (p * Math.log(p)) / Math.log(2);
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
|
||||
process(s, evaluator) {
|
||||
let h = Object.create(null),
|
||||
k;
|
||||
s.split('').forEach((c) => {
|
||||
(h[c] && h[c]++) || (h[c] = 1);
|
||||
});
|
||||
if (evaluator) for (k in h) evaluator(k, h[k]);
|
||||
return h;
|
||||
}
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
class lib_cipher {
|
||||
hex_chr = '0123456789abcdef'.split('');
|
||||
|
||||
md5cycle(x, k) {
|
||||
let a = x[0],
|
||||
b = x[1],
|
||||
c = x[2],
|
||||
d = x[3];
|
||||
|
||||
a = this.ff(a, b, c, d, k[0], 7, -680876936);
|
||||
d = this.ff(d, a, b, c, k[1], 12, -389564586);
|
||||
c = this.ff(c, d, a, b, k[2], 17, 606105819);
|
||||
b = this.ff(b, c, d, a, k[3], 22, -1044525330);
|
||||
a = this.ff(a, b, c, d, k[4], 7, -176418897);
|
||||
d = this.ff(d, a, b, c, k[5], 12, 1200080426);
|
||||
c = this.ff(c, d, a, b, k[6], 17, -1473231341);
|
||||
b = this.ff(b, c, d, a, k[7], 22, -45705983);
|
||||
a = this.ff(a, b, c, d, k[8], 7, 1770035416);
|
||||
d = this.ff(d, a, b, c, k[9], 12, -1958414417);
|
||||
c = this.ff(c, d, a, b, k[10], 17, -42063);
|
||||
b = this.ff(b, c, d, a, k[11], 22, -1990404162);
|
||||
a = this.ff(a, b, c, d, k[12], 7, 1804603682);
|
||||
d = this.ff(d, a, b, c, k[13], 12, -40341101);
|
||||
c = this.ff(c, d, a, b, k[14], 17, -1502002290);
|
||||
b = this.ff(b, c, d, a, k[15], 22, 1236535329);
|
||||
|
||||
a = this.gg(a, b, c, d, k[1], 5, -165796510);
|
||||
d = this.gg(d, a, b, c, k[6], 9, -1069501632);
|
||||
c = this.gg(c, d, a, b, k[11], 14, 643717713);
|
||||
b = this.gg(b, c, d, a, k[0], 20, -373897302);
|
||||
a = this.gg(a, b, c, d, k[5], 5, -701558691);
|
||||
d = this.gg(d, a, b, c, k[10], 9, 38016083);
|
||||
c = this.gg(c, d, a, b, k[15], 14, -660478335);
|
||||
b = this.gg(b, c, d, a, k[4], 20, -405537848);
|
||||
a = this.gg(a, b, c, d, k[9], 5, 568446438);
|
||||
d = this.gg(d, a, b, c, k[14], 9, -1019803690);
|
||||
c = this.gg(c, d, a, b, k[3], 14, -187363961);
|
||||
b = this.gg(b, c, d, a, k[8], 20, 1163531501);
|
||||
a = this.gg(a, b, c, d, k[13], 5, -1444681467);
|
||||
d = this.gg(d, a, b, c, k[2], 9, -51403784);
|
||||
c = this.gg(c, d, a, b, k[7], 14, 1735328473);
|
||||
b = this.gg(b, c, d, a, k[12], 20, -1926607734);
|
||||
|
||||
a = this.hh(a, b, c, d, k[5], 4, -378558);
|
||||
d = this.hh(d, a, b, c, k[8], 11, -2022574463);
|
||||
c = this.hh(c, d, a, b, k[11], 16, 1839030562);
|
||||
b = this.hh(b, c, d, a, k[14], 23, -35309556);
|
||||
a = this.hh(a, b, c, d, k[1], 4, -1530992060);
|
||||
d = this.hh(d, a, b, c, k[4], 11, 1272893353);
|
||||
c = this.hh(c, d, a, b, k[7], 16, -155497632);
|
||||
b = this.hh(b, c, d, a, k[10], 23, -1094730640);
|
||||
a = this.hh(a, b, c, d, k[13], 4, 681279174);
|
||||
d = this.hh(d, a, b, c, k[0], 11, -358537222);
|
||||
c = this.hh(c, d, a, b, k[3], 16, -722521979);
|
||||
b = this.hh(b, c, d, a, k[6], 23, 76029189);
|
||||
a = this.hh(a, b, c, d, k[9], 4, -640364487);
|
||||
d = this.hh(d, a, b, c, k[12], 11, -421815835);
|
||||
c = this.hh(c, d, a, b, k[15], 16, 530742520);
|
||||
b = this.hh(b, c, d, a, k[2], 23, -995338651);
|
||||
|
||||
a = this.ii(a, b, c, d, k[0], 6, -198630844);
|
||||
d = this.ii(d, a, b, c, k[7], 10, 1126891415);
|
||||
c = this.ii(c, d, a, b, k[14], 15, -1416354905);
|
||||
b = this.ii(b, c, d, a, k[5], 21, -57434055);
|
||||
a = this.ii(a, b, c, d, k[12], 6, 1700485571);
|
||||
d = this.ii(d, a, b, c, k[3], 10, -1894986606);
|
||||
c = this.ii(c, d, a, b, k[10], 15, -1051523);
|
||||
b = this.ii(b, c, d, a, k[1], 21, -2054922799);
|
||||
a = this.ii(a, b, c, d, k[8], 6, 1873313359);
|
||||
d = this.ii(d, a, b, c, k[15], 10, -30611744);
|
||||
c = this.ii(c, d, a, b, k[6], 15, -1560198380);
|
||||
b = this.ii(b, c, d, a, k[13], 21, 1309151649);
|
||||
a = this.ii(a, b, c, d, k[4], 6, -145523070);
|
||||
d = this.ii(d, a, b, c, k[11], 10, -1120210379);
|
||||
c = this.ii(c, d, a, b, k[2], 15, 718787259);
|
||||
b = this.ii(b, c, d, a, k[9], 21, -343485551);
|
||||
|
||||
x[0] = this.add32(a, x[0]);
|
||||
x[1] = this.add32(b, x[1]);
|
||||
x[2] = this.add32(c, x[2]);
|
||||
x[3] = this.add32(d, x[3]);
|
||||
}
|
||||
|
||||
cmn(q, a, b, x, s, t) {
|
||||
a = this.add32(this.add32(a, q), this.add32(x, t));
|
||||
return this.add32((a << s) | (a >>> (32 - s)), b);
|
||||
}
|
||||
|
||||
ff(a, b, c, d, x, s, t) {
|
||||
return this.cmn((b & c) | (~b & d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
gg(a, b, c, d, x, s, t) {
|
||||
return this.cmn((b & d) | (c & ~d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
hh(a, b, c, d, x, s, t) {
|
||||
return this.cmn(b ^ c ^ d, a, b, x, s, t);
|
||||
}
|
||||
|
||||
ii(a, b, c, d, x, s, t) {
|
||||
return this.cmn(c ^ (b | ~d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
md51(s) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let n = s.length,
|
||||
// eslint-disable-next-line prefer-const
|
||||
state = [1732584193, -271733879, -1732584194, 271733878],
|
||||
i;
|
||||
for (i = 64; i <= s.length; i += 64) {
|
||||
this.md5cycle(state, this.md5blk(s.substring(i - 64, i)));
|
||||
}
|
||||
s = s.substring(i - 64);
|
||||
const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
|
||||
tail[i >> 2] |= 0x80 << (i % 4 << 3);
|
||||
if (i > 55) {
|
||||
this.md5cycle(state, tail);
|
||||
for (i = 0; i < 16; i++) tail[i] = 0;
|
||||
}
|
||||
tail[14] = n * 8;
|
||||
this.md5cycle(state, tail);
|
||||
return state;
|
||||
}
|
||||
|
||||
/* there needs to be support for Unicode here,
|
||||
* unless we pretend that we can redefine the MD-5
|
||||
* algorithm for multi-byte characters (perhaps
|
||||
* by adding every four 16-bit characters and
|
||||
* shortening the sum to 32 bits). Otherwise
|
||||
* I suggest performing MD-5 as if every character
|
||||
* was two bytes--e.g., 0040 0025 = @%--but then
|
||||
* how will an ordinary MD-5 sum be matched?
|
||||
* There is no way to standardize text to something
|
||||
* like UTF-8 before transformation; speed cost is
|
||||
* utterly prohibitive. The JavaScript standard
|
||||
* itself needs to look at this: it should start
|
||||
* providing access to strings as preformed UTF-8
|
||||
* 8-bit unsigned value arrays.
|
||||
*/
|
||||
md5blk(s) {
|
||||
/* I figured global was faster. */
|
||||
|
||||
const md5blks = []; /* Andy King said do it this way. */
|
||||
for (let i = 0; i < 64; i += 4) {
|
||||
md5blks[i >> 2] =
|
||||
s.charCodeAt(i) +
|
||||
(s.charCodeAt(i + 1) << 8) +
|
||||
(s.charCodeAt(i + 2) << 16) +
|
||||
(s.charCodeAt(i + 3) << 24);
|
||||
}
|
||||
return md5blks;
|
||||
}
|
||||
|
||||
rhex(n) {
|
||||
let s = '',
|
||||
j = 0;
|
||||
for (; j < 4; j++) s += this.hex_chr[(n >> (j * 8 + 4)) & 0x0f] + this.hex_chr[(n >> (j * 8)) & 0x0f];
|
||||
return s;
|
||||
}
|
||||
|
||||
hex(x) {
|
||||
for (let i = 0; i < x.length; i++) x[i] = this.rhex(x[i]);
|
||||
return x.join('');
|
||||
}
|
||||
|
||||
md5(s) {
|
||||
return this.hex(this.md51(s));
|
||||
}
|
||||
|
||||
/* this function is much faster,
|
||||
so if possible we use it. Some IEs
|
||||
are the only ones I know of that
|
||||
need the idiotic second function,
|
||||
generated by an if clause. */
|
||||
|
||||
add32(a, b) {
|
||||
return (a + b) & 0xffffffff;
|
||||
}
|
||||
}
|
||||
const CipherLib = new lib_cipher();
|
||||
|
||||
export default CipherLib;
|
@ -0,0 +1,14 @@
|
||||
<div class="static-page content">
|
||||
<article>
|
||||
<section class="hero is-info">
|
||||
<div class="hero-body">
|
||||
<p class="title">
|
||||
Mentions légales
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
détail des mentions légales
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LegalComponent } from './legal.component';
|
||||
|
||||
describe('LegalComponent', () => {
|
||||
let component: LegalComponent;
|
||||
let fixture: ComponentFixture<LegalComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LegalComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LegalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-legal',
|
||||
templateUrl: './legal.component.html',
|
||||
styleUrls: ['./legal.component.scss'],
|
||||
})
|
||||
export class LegalComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<section class="hero is-info">
|
||||
<div class="hero-body">
|
||||
<p class="title">
|
||||
Politique de confidentialité
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
Privacy policy
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PrivacyComponent } from './privacy.component';
|
||||
|
||||
describe('PrivacyComponent', () => {
|
||||
let component: PrivacyComponent;
|
||||
let fixture: ComponentFixture<PrivacyComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PrivacyComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PrivacyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-privacy',
|
||||
templateUrl: './privacy.component.html',
|
||||
styleUrls: ['./privacy.component.scss'],
|
||||
})
|
||||
export class PrivacyComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
@ -4,6 +4,19 @@
|
||||
<h1>Mes sondages</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<form (submit)="sendRetrieveEmail()">
|
||||
<input type="email" id="search_field" autofocus="autofocus" placeholder="contact@exemple.com" />
|
||||
<button class="button is-primary">
|
||||
envoyez-moi la liste par email
|
||||
</button>
|
||||
<label for="search_field" class="clickable padded">
|
||||
<img src="assets/img/undraw_prototyping_process_rswj.svg" alt="image my polls" />
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="pollsAreLoaded">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
@ -23,17 +36,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<form (submit)="sendRetrieveEmail()">
|
||||
<label for="search_field">
|
||||
<img src="assets/img/undraw_prototyping_process_rswj.svg" alt="image my polls" />
|
||||
<input type="email" id="search_field" autofocus="autofocus" placeholder="contact@exemple.com" />
|
||||
</label>
|
||||
<button class="button is-primary">
|
||||
envoyez-moi la liste par email
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,9 +5,16 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { UserPollsComponent } from './user-polls/user-polls.component';
|
||||
import { UserProfileRoutingModule } from './user-profile-routing.module';
|
||||
import { AppModule } from '../../app.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserPollsComponent],
|
||||
imports: [CommonModule, UserProfileRoutingModule, SharedModule, TranslateModule.forChild({ extend: true })],
|
||||
imports: [
|
||||
CommonModule,
|
||||
UserProfileRoutingModule,
|
||||
SharedModule,
|
||||
TranslateModule.forChild({ extend: true }),
|
||||
AppModule,
|
||||
],
|
||||
})
|
||||
export class UserProfileModule {}
|
||||
|
@ -2,6 +2,12 @@ import { Routes } from '@angular/router';
|
||||
import { HomeComponent } from './core/components/home/home.component';
|
||||
import { PollService } from './core/services/poll.service';
|
||||
import { PageNotFoundComponent } from './shared/components/page-not-found/page-not-found.component';
|
||||
import { SuccessComponent } from './features/administration/success/success.component';
|
||||
import { WipTodoComponent } from './shared/components/ui/wip-todo/wip-todo.component';
|
||||
import { CguComponent } from './features/shared/components/ui/static-pages/cgu/cgu.component';
|
||||
import { LegalComponent } from './features/shared/components/ui/static-pages/legal/legal.component';
|
||||
import { PrivacyComponent } from './features/shared/components/ui/static-pages/privacy/privacy.component';
|
||||
import { CipheringComponent } from './features/shared/components/ui/static-pages/ciphering/ciphering.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
@ -35,6 +41,30 @@ export const routes: Routes = [
|
||||
path: 'oldstuff',
|
||||
loadChildren: () => import('./features/old-stuff/old-stuff.module').then((m) => m.OldStuffModule),
|
||||
},
|
||||
{
|
||||
path: 'success',
|
||||
component: SuccessComponent,
|
||||
},
|
||||
{
|
||||
path: 'todo',
|
||||
component: WipTodoComponent,
|
||||
},
|
||||
{
|
||||
path: 'cgu',
|
||||
component: CguComponent,
|
||||
},
|
||||
{
|
||||
path: 'legal',
|
||||
component: LegalComponent,
|
||||
},
|
||||
{
|
||||
path: 'privacy',
|
||||
component: PrivacyComponent,
|
||||
},
|
||||
{
|
||||
path: 'ciphering',
|
||||
component: CipheringComponent,
|
||||
},
|
||||
{ path: 'page-not-found', component: PageNotFoundComponent },
|
||||
{ path: '**', redirectTo: 'page-not-found', pathMatch: 'full' },
|
||||
];
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Component, DoCheck, OnInit } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { Language } from '../../../../core/enums/language.enum';
|
||||
import { StorageService } from '../../../../core/services/storage.service';
|
||||
import { LanguageService } from '../../../../core/services/language.service';
|
||||
|
||||
@Component({
|
||||
|
@ -1,5 +1,7 @@
|
||||
<button (click)="copy()" class="btn btn--primary btn--outline" id="copyLink">
|
||||
<i class="fa fa-copy" aria-hidden="true"></i>
|
||||
<span *ngIf="displayLabelButton">
|
||||
{{ 'admin.copy_link' | translate }}
|
||||
</span>
|
||||
<span *ngIf="displayContentToCopy"> " {{ textToCopy }}" </span>
|
||||
</button>
|
||||
|
@ -11,6 +11,7 @@ import { ToastService } from '../../../../core/services/toast.service';
|
||||
export class CopyTextComponent implements OnInit {
|
||||
@Input() public textToCopy: string;
|
||||
public displayContentToCopy = false;
|
||||
@Input() public displayLabelButton = true;
|
||||
|
||||
constructor(private _clipboardService: ClipboardService, private toastService: ToastService) {}
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
<div class="notification is-info is-light">
|
||||
<div class="columns">
|
||||
<div class="column is-narrow">
|
||||
<i class="fa fa-info-circle fa-2x"></i>
|
||||
</div>
|
||||
<div class="column">
|
||||
Cette fonctionnalité est <strong>en cours de développement</strong>, vous pouvez contribuer à son
|
||||
amélioration avec le bouton de feedback.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WipTodoComponent } from './wip-todo.component';
|
||||
|
||||
describe('WipTodoComponent', () => {
|
||||
let component: WipTodoComponent;
|
||||
let fixture: ComponentFixture<WipTodoComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [WipTodoComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(WipTodoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
12
src/app/shared/components/ui/wip-todo/wip-todo.component.ts
Normal file
12
src/app/shared/components/ui/wip-todo/wip-todo.component.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-wip-todo',
|
||||
templateUrl: './wip-todo.component.html',
|
||||
styleUrls: ['./wip-todo.component.scss'],
|
||||
})
|
||||
export class WipTodoComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
@ -25,6 +25,7 @@ import { SettingsComponent } from './components/settings/settings.component';
|
||||
import { SpinnerComponent } from './components/spinner/spinner.component';
|
||||
import { CopyTextComponent } from './components/ui/copy-text/copy-text.component';
|
||||
import { ErasableInputComponent } from './components/ui/erasable-input/erasable-input.component';
|
||||
import { WipTodoComponent } from './components/ui/wip-todo/wip-todo.component';
|
||||
|
||||
const COMPONENTS = [
|
||||
ChoiceDetailsComponent,
|
||||
@ -37,6 +38,7 @@ const COMPONENTS = [
|
||||
ThemeSelectorComponent,
|
||||
CopyTextComponent,
|
||||
ErasableInputComponent,
|
||||
WipTodoComponent,
|
||||
];
|
||||
|
||||
const ANGULAR_MODULES = [CommonModule, ChartsModule, FormsModule, TranslateModule];
|
||||
|
@ -584,5 +584,41 @@
|
||||
"NL": "Néérlandais",
|
||||
"OC": "oc",
|
||||
"SV": "sv"
|
||||
},
|
||||
|
||||
"calendar_widget" : {
|
||||
"startsWith": "Starts with",
|
||||
"contains": "Contains",
|
||||
"notContains": "Not contains",
|
||||
"endsWith": "Ends with",
|
||||
"equals": "Equals",
|
||||
"notEquals": "Not equals",
|
||||
"noFilter": "No Filter",
|
||||
"lt": "Less than",
|
||||
"lte": "Less than or equal to",
|
||||
"gt": "Greater than",
|
||||
"gte": "Great then or equals",
|
||||
"is": "Is",
|
||||
"isNot": "Is not",
|
||||
"before": "Before",
|
||||
"after": "After",
|
||||
"clear": "Clear",
|
||||
"apply": "Apply",
|
||||
"matchAll": "Match All",
|
||||
"matchAny": "Match Any",
|
||||
"addRule": "Add Rule",
|
||||
"removeRule": "Remove Rule",
|
||||
"accept": "Yes",
|
||||
"reject": "No",
|
||||
"choose": "Choose",
|
||||
"upload": "Upload",
|
||||
"cancel": "Cancel",
|
||||
"dayNames": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
|
||||
"dayNamesShort": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
"dayNamesMin": ["Su","Mo","Tu","We","Th","Fr","Sa"],
|
||||
"monthNames": ["January","February","March","April","May","June","July","August","September","October","November","December"],
|
||||
"monthNamesShort": ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
"today": "Today",
|
||||
"weekHeader": "Wk"
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,8 @@
|
||||
"choices_hint": "Utilisez les flèches haut ⬆️ et bas ⬇️ pour passer d'un choix à un autre",
|
||||
"name": "Je peux aussi préciser mon nom si je le souhaite",
|
||||
"name_placeholder": "mon nom",
|
||||
"email": "Mon email",
|
||||
"email_placeholder": "mon-email@example.com",
|
||||
"description": "et la description serait",
|
||||
"description_placeholder": "description"
|
||||
},
|
||||
@ -149,7 +151,8 @@
|
||||
"do-you-want-to": "Voulez-vous",
|
||||
"framadate-is-an-online-service-for-planning-an-appointment-or-making-a-decision-quickly-and-easily-n": "Framadate est un service en ligne permettant de planifier un rendez-vous ou prendre des décisions rapidement et simplement. Aucune inscription préalable n’est nécessaire.",
|
||||
"here-is-how-it-works": "Voici comment ça fonctionne :",
|
||||
"send-the-poll-link-to-your-friends-or-colleagues": "Envoyez le lien du sondage à vos ami·e·s ou collègues",
|
||||
"send-the-poll-link-to-your-friends-or-colleagues": "Créez un sondage en choisissant les réponses possibles, et vous recevez un lien par email. Envoyez le lien du sondage à vos ami·e·s ou collègues.",
|
||||
"what-you-can-do": "Vous pouvez faire des propositions de dates et horaires de rendez-vous, limiter le nombre de participants, poser des questions de toutes sortes, permettre de la finesse dans les réponses, exporter les données... tout reste sous votre contrôle et vos libertés seront toujours respectées, car c'est un logiciel libre.",
|
||||
"what-is-framadate": "Prise en main",
|
||||
"view-an-example": "voir un exemple ?",
|
||||
"cecill-b-license": "licence CeCILL-B",
|
||||
@ -585,5 +588,40 @@
|
||||
"NL": "Néérlandais",
|
||||
"OC": "oc",
|
||||
"SV": "sv"
|
||||
}
|
||||
},
|
||||
"calendar_widget" : {
|
||||
"startsWith": "Starts with",
|
||||
"contains": "Contains",
|
||||
"notContains": "Not contains",
|
||||
"endsWith": "Ends with",
|
||||
"equals": "Equals",
|
||||
"notEquals": "Not equals",
|
||||
"noFilter": "No Filter",
|
||||
"lt": "Less than",
|
||||
"lte": "Less than or equal to",
|
||||
"gt": "Greater than",
|
||||
"gte": "Great then or equals",
|
||||
"is": "Is",
|
||||
"isNot": "Is not",
|
||||
"before": "Before",
|
||||
"after": "After",
|
||||
"clear": "Clear",
|
||||
"apply": "Apply",
|
||||
"matchAll": "Match All",
|
||||
"matchAny": "Match Any",
|
||||
"addRule": "Add Rule",
|
||||
"removeRule": "Remove Rule",
|
||||
"accept": "Yes",
|
||||
"reject": "No",
|
||||
"choose": "Choose",
|
||||
"upload": "Upload",
|
||||
"cancel": "Cancel",
|
||||
"dayNames": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
|
||||
"dayNamesShort": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
"dayNamesMin": ["Su","Mo","Tu","We","Th","Fr","Sa"],
|
||||
"monthNames": ["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"],
|
||||
"monthNamesShort": ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
"today": "Aujourd'hui",
|
||||
"weekHeader": "Wk"
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,28 @@ const backendApiUrlsInDev = {
|
||||
local: '/api/v1',
|
||||
remote: 'https://framadate-api.cipherbliss.com/api/v1',
|
||||
};
|
||||
const apiV1 = {
|
||||
baseHref: 'http://localhost:8000/api/v1',
|
||||
api_new_poll: '/poll/',
|
||||
api_get_poll: '/poll/{id}',
|
||||
'api_test-mail-poll': '/api/v1/poll/mail/test-mail-poll/{emailChoice}',
|
||||
'app.swagger': '/api/doc.json',
|
||||
};
|
||||
|
||||
export const environment = {
|
||||
production: true,
|
||||
appTitle: 'FramaDate',
|
||||
appVersion: '2.0.0',
|
||||
appLogo: '/assets/img/logo.png',
|
||||
appTitle: 'FramaDate Funky',
|
||||
appVersion: '2.1.0',
|
||||
appLogo: 'assets/img/logo.png',
|
||||
api: {
|
||||
versionToUse: 'apiV1',
|
||||
version: {
|
||||
apiV1,
|
||||
},
|
||||
baseHref: backendApiUrlsInDev.remote,
|
||||
endpoints: {
|
||||
polls: {
|
||||
name: '/polls',
|
||||
name: '/poll',
|
||||
choices: {
|
||||
name: '/choices',
|
||||
},
|
||||
@ -39,8 +50,12 @@ export const environment = {
|
||||
},
|
||||
poll: {
|
||||
defaultConfig: {
|
||||
expiracyInDays: 60,
|
||||
maxCountOfAnswers: 150,
|
||||
expiresDaysDelay: 60,
|
||||
expiracyAfterLastModificationInDays: 180,
|
||||
whoCanChangeAnswers: 'everybody',
|
||||
visibility: 'link_only',
|
||||
voteChoices: 'only_yes',
|
||||
},
|
||||
},
|
||||
localStorage: {
|
||||
|
@ -4,10 +4,10 @@
|
||||
|
||||
const backendApiUrlsInDev = {
|
||||
local: '/api/v1',
|
||||
remote: 'https://framadate-api.cipherbliss.com/api/v1',
|
||||
remote: 'http://localhost:8000/api/v1',
|
||||
};
|
||||
const apiV1 = {
|
||||
baseHref: 'https://framadate-api.cipherbliss.com/api/v1',
|
||||
baseHref: 'http://localhost:8000/api/v1',
|
||||
api_new_poll: '/poll/',
|
||||
api_get_poll: '/poll/{id}',
|
||||
'api_test-mail-poll': '/api/v1/poll/mail/test-mail-poll/{emailChoice}',
|
||||
@ -17,7 +17,7 @@ const apiV1 = {
|
||||
export const environment = {
|
||||
production: false,
|
||||
appTitle: 'FramaDate Funky',
|
||||
appVersion: '2.0.0',
|
||||
appVersion: '2.1.0',
|
||||
appLogo: 'assets/img/logo.png',
|
||||
api: {
|
||||
versionToUse: 'apiV1',
|
||||
@ -54,8 +54,12 @@ export const environment = {
|
||||
},
|
||||
poll: {
|
||||
defaultConfig: {
|
||||
expiracyInDays: 60,
|
||||
maxCountOfAnswers: 150,
|
||||
expiresDaysDelay: 60,
|
||||
expiracyAfterLastModificationInDays: 180,
|
||||
whoCanChangeAnswers: 'everybody',
|
||||
visibility: 'link_only',
|
||||
voteChoices: 'only_yes',
|
||||
},
|
||||
},
|
||||
localStorage: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:8000",
|
||||
"target": "http://localhost:3001",
|
||||
"secure": false,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
|
@ -15,6 +15,12 @@
|
||||
box-shadow: $dark-lavender 0 0 10px;
|
||||
}
|
||||
|
||||
.is-boxed {
|
||||
border: 1px solid #ddd;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.nobold {
|
||||
font-weight: normal;
|
||||
}
|
||||
@ -22,3 +28,7 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 1em;
|
||||
}
|
||||
|
@ -11,3 +11,7 @@ $body-background-color: $black;
|
||||
$control-border-width: 2px;
|
||||
$input-border-color: transparent;
|
||||
$input-shadow: none;
|
||||
|
||||
.notification {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
background: $primary;
|
||||
|
||||
main {
|
||||
padding: 0;
|
||||
margin-bottom: 2em;
|
||||
padding-bottom: 5em;
|
||||
padding-top: 1em;
|
||||
|
@ -11,7 +11,9 @@
|
||||
}
|
||||
|
||||
main {
|
||||
.container {
|
||||
padding-top: 0;
|
||||
|
||||
> .container {
|
||||
background: #fff;
|
||||
padding-bottom: 10em;
|
||||
}
|
||||
|
@ -8193,6 +8193,11 @@ node-forge@0.9.0:
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
|
||||
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
|
||||
|
||||
node-forge@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
|
||||
|
||||
node-int64@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
||||
|
Loading…
Reference in New Issue
Block a user