Merge branch 'creation-form'

This commit is contained in:
Tykayn 2021-03-29 10:56:55 +02:00 committed by tykayn
commit b483cc520e
84 changed files with 2833 additions and 804 deletions

1
CHANGELOG.md Normal file
View File

@ -0,0 +1 @@
# Changelog

5
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 {}

View File

@ -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

View File

@ -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',

View File

@ -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,

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
// })
);
}
}

View File

@ -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())

View File

@ -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

View File

@ -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 {}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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) });
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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
);
}
}
}

View File

@ -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>

View File

@ -83,4 +83,7 @@
border-left: $success 3px solid;
padding-left: 1em;
}
.btn {
margin: 0.5em;
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {}
}

View File

@ -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>

View File

@ -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],
],
});

View File

@ -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>

View File

@ -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();
});
});

View 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 {}
}

View File

@ -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">

View File

@ -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

View File

@ -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">

View File

@ -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,

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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 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;
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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' },
];

View File

@ -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({

View File

@ -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>

View File

@ -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) {}

View File

@ -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>

View File

@ -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();
});
});

View 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 {}
}

View File

@ -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];

View File

@ -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"
}
}

View File

@ -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 nest 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"
}
}

View File

@ -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: {

View File

@ -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: {

View File

@ -1,6 +1,6 @@
{
"/api/*": {
"target": "http://localhost:8000",
"target": "http://localhost:3001",
"secure": false,
"logLevel": "debug"
}

View File

@ -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;
}

View File

@ -11,3 +11,7 @@ $body-background-color: $black;
$control-border-width: 2px;
$input-border-color: transparent;
$input-shadow: none;
.notification {
margin: 1em 0;
}

View File

@ -2,6 +2,7 @@
background: $primary;
main {
padding: 0;
margin-bottom: 2em;
padding-bottom: 5em;
padding-top: 1em;

View File

@ -11,7 +11,9 @@
}
main {
.container {
padding-top: 0;
> .container {
background: #fff;
padding-bottom: 10em;
}

View File

@ -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"