;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [LoginComponent, HttpClientModule, NoopAnimationsModule],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(LoginComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts
new file mode 100644
index 0000000..51179e1
--- /dev/null
+++ b/src/app/components/login/login.component.ts
@@ -0,0 +1,71 @@
+import { Component } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import {
+ FormControl,
+ Validators,
+ FormsModule,
+ ReactiveFormsModule,
+ NgForm,
+ FormGroup,
+ FormBuilder,
+} from '@angular/forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { merge } from 'rxjs';
+import { MatIconModule } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { CommonModule } from '@angular/common';
+import { UserService } from 'src/app/services/user.service';
+import { User } from 'src/app/models/user.model';
+
+@Component({
+ selector: 'app-auth',
+ templateUrl: './login.component.html',
+ styleUrl: './login.component.css',
+ standalone: true,
+ imports: [
+ MatFormFieldModule,
+ MatInputModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatButtonModule,
+ MatIconModule,
+ CommonModule,
+ ],
+})
+export class LoginComponent {
+ hide = true;
+
+ loginForm = this.formBuilder.group({
+ login: ['', Validators.required],
+ password: ['', Validators.required],
+ });
+
+ errorMessage = '';
+
+ successLogin = '';
+ errorLogin = '';
+
+ constructor(
+ private userService: UserService,
+ private formBuilder: FormBuilder
+ ) {
+ }
+
+ loginAction() {
+ const formValue = this.loginForm.value;
+
+ this.userService
+ .loginUser(formValue.login!, formValue.password!)
+ .subscribe((response) => {
+ console.log('response :', response);
+ if (response.success) {
+ this.successLogin = 'Vous êtes connecté.';
+ this.errorLogin = '';
+ } else {
+ this.errorLogin = "L'authentification a échoué.";
+ this.successLogin = '';
+ }
+ });
+ }
+}
diff --git a/src/app/components/privacy-policy/privacy-policy.component.spec.ts b/src/app/components/privacy-policy/privacy-policy.component.spec.ts
index 54a75e8..c93d9f4 100644
--- a/src/app/components/privacy-policy/privacy-policy.component.spec.ts
+++ b/src/app/components/privacy-policy/privacy-policy.component.spec.ts
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PrivacyPolicyComponent } from './privacy-policy.component';
+import { TranslateModule } from '@ngx-translate/core';
describe('PrivacyPolicyComponent', () => {
let component: PrivacyPolicyComponent;
@@ -8,7 +9,7 @@ describe('PrivacyPolicyComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [PrivacyPolicyComponent],
+ imports: [PrivacyPolicyComponent, TranslateModule.forRoot()],
}).compileComponents();
fixture = TestBed.createComponent(PrivacyPolicyComponent);
diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css
new file mode 100644
index 0000000..5eb5cdd
--- /dev/null
+++ b/src/app/components/register/register.component.css
@@ -0,0 +1,18 @@
+.form-container {
+ display: flex;
+ flex-direction: column;
+}
+
+.form-field {
+ width: 100%;
+}
+
+.success-message {
+ color: green;
+ margin-top: 20px;
+}
+
+.error-message {
+ color: red;
+ margin-top: 20px;
+}
diff --git a/src/app/components/register/register.component.html b/src/app/components/register/register.component.html
new file mode 100644
index 0000000..1c3bf41
--- /dev/null
+++ b/src/app/components/register/register.component.html
@@ -0,0 +1,53 @@
+
diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts
new file mode 100644
index 0000000..d5467a4
--- /dev/null
+++ b/src/app/components/register/register.component.ts
@@ -0,0 +1,75 @@
+import { Component } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import {
+ FormControl,
+ Validators,
+ FormsModule,
+ ReactiveFormsModule,
+ NgForm,
+ FormBuilder,
+} from '@angular/forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { merge } from 'rxjs';
+import { MatIconModule } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { CommonModule } from '@angular/common';
+import { UserService } from 'src/app/services/user.service';
+import { User } from 'src/app/models/user.model';
+
+@Component({
+ selector: 'app-auth',
+ templateUrl: './register.component.html',
+ styleUrl: './register.component.css',
+ standalone: true,
+ imports: [
+ MatFormFieldModule,
+ MatInputModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatButtonModule,
+ MatIconModule,
+ CommonModule,
+ ],
+})
+export class RegisterComponent {
+ hide = true;
+
+ registerForm = this.formBuilder.group({
+ email: ['', Validators.required],
+ login: ['', Validators.required],
+ password: ['', Validators.required],
+ });
+ errorMessage = '';
+
+ successRegister = '';
+ errorRegister = '';
+
+ constructor(
+ private userService: UserService,
+ private formBuilder: FormBuilder
+ ) {
+ }
+
+ register() {
+ const formRegisterValue = this.registerForm.value;
+
+ this.userService
+ .postUser(
+ formRegisterValue.email!,
+ formRegisterValue.login!,
+ formRegisterValue.password!
+ )
+ .subscribe((response) => {
+ console.log('response :', response);
+ if (response.success) {
+ this.successRegister = 'Votre compte a été créé avec succès.';
+ this.errorRegister = '';
+ } else {
+ this.errorRegister =
+ "L'inscription a échoué : un compte avec ce login existe déjà.";
+ this.successRegister = '';
+ }
+ });
+ }
+}
diff --git a/src/app/components/terms-of-service/terms-of-service.component.spec.ts b/src/app/components/terms-of-service/terms-of-service.component.spec.ts
index 7faa79d..1fcbf16 100644
--- a/src/app/components/terms-of-service/terms-of-service.component.spec.ts
+++ b/src/app/components/terms-of-service/terms-of-service.component.spec.ts
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TermsOfServiceComponent } from './terms-of-service.component';
+import { TranslateModule } from '@ngx-translate/core';
describe('TermsOfServiceComponent', () => {
let component: TermsOfServiceComponent;
@@ -8,7 +9,7 @@ describe('TermsOfServiceComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [TermsOfServiceComponent],
+ imports: [TermsOfServiceComponent, TranslateModule.forRoot()],
}).compileComponents();
fixture = TestBed.createComponent(TermsOfServiceComponent);
diff --git a/src/app/components/work-list-detail/work-list-detail.component.html b/src/app/components/work-list-detail/work-list-detail.component.html
index 613041f..9dcd285 100644
--- a/src/app/components/work-list-detail/work-list-detail.component.html
+++ b/src/app/components/work-list-detail/work-list-detail.component.html
@@ -1,9 +1,9 @@
-
{{ work.title }}
+ {{ work?.title }}
{{
- work.content | slice: 0 : 50
+ work?.content | slice: 0 : 50
}}
-
diff --git a/src/app/components/work-list-detail/work-list-detail.component.spec.ts b/src/app/components/work-list-detail/work-list-detail.component.spec.ts
index d546f31..cf4a10d 100644
--- a/src/app/components/work-list-detail/work-list-detail.component.spec.ts
+++ b/src/app/components/work-list-detail/work-list-detail.component.spec.ts
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
-
import { WorkListDetailComponent } from './work-list-detail.component';
+import { HttpClientModule } from '@angular/common/http';
+import { RouterModule } from '@angular/router';
describe('WorkListDetailComponent', () => {
let component: WorkListDetailComponent;
@@ -8,7 +9,7 @@ describe('WorkListDetailComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [WorkListDetailComponent],
+ imports: [WorkListDetailComponent, HttpClientModule, RouterModule.forRoot([])],
}).compileComponents();
fixture = TestBed.createComponent(WorkListDetailComponent);
diff --git a/src/app/components/work-list-detail/work-list-detail.component.ts b/src/app/components/work-list-detail/work-list-detail.component.ts
index 77cd65c..3ac9d7e 100644
--- a/src/app/components/work-list-detail/work-list-detail.component.ts
+++ b/src/app/components/work-list-detail/work-list-detail.component.ts
@@ -11,5 +11,5 @@ import { SlicePipe } from '@angular/common';
styleUrl: './work-list-detail.component.scss',
})
export class WorkListDetailComponent {
- @Input() work!: Work;
+ @Input() work?: Work;
}
diff --git a/src/app/components/work/work.component.html b/src/app/components/work/work.component.html
deleted file mode 100644
index 7f9c5cf..0000000
--- a/src/app/components/work/work.component.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/src/app/components/work/work.component.scss b/src/app/components/work/work.component.scss
deleted file mode 100644
index e69de29..0000000
diff --git a/src/app/components/work/work.component.spec.ts b/src/app/components/work/work.component.spec.ts
deleted file mode 100644
index 0498a4c..0000000
--- a/src/app/components/work/work.component.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { WorkComponent } from './work.component';
-
-describe('WorkComponent', () => {
- let component: WorkComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [WorkComponent],
- }).compileComponents();
-
- fixture = TestBed.createComponent(WorkComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/src/app/components/work/work.component.ts b/src/app/components/work/work.component.ts
deleted file mode 100644
index 0e13aa2..0000000
--- a/src/app/components/work/work.component.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { RouterLink, ActivatedRoute } from '@angular/router';
-import { ThemeService } from '../../services/theme.service';
-import { NgClass, NgIf } from '@angular/common';
-import { TranslateModule } from '@ngx-translate/core';
-import { Work } from '../../models/work.model';
-import { WorkService } from '../../services/work.service';
-import { NgForOf } from '@angular/common';
-import { FormsModule, NgForm } from '@angular/forms';
-import { EditorComponent } from '../editor/editor.component';
-import { WorkListDetailComponent } from '../work-list-detail/work-list-detail.component';
-
-@Component({
- selector: 'app-work',
- templateUrl: './work.component.html',
- styleUrl: './work.component.scss',
- standalone: true,
- imports: [
- NgClass,
- TranslateModule,
- RouterLink,
- NgForOf,
- FormsModule,
- EditorComponent,
- WorkListDetailComponent,
- NgIf,
- ],
-})
-export class WorkComponent implements OnInit {
- // à retirer quand les boutons seront dans editor.component
- isLoaded: boolean = false; // Pour vérifier si le chargement est terminé
-
- themeClass!: string;
- work!: Work;
-
- constructor(
- private route: ActivatedRoute,
- private themeService: ThemeService,
- protected workService: WorkService
- ) {}
-
- ngOnInit() {
- this.themeService.isDarkTheme.subscribe((value) => {
- value
- ? (this.themeClass = 'dark-theme')
- : (this.themeClass = 'light-theme');
- });
-
- const work_link = String(this.route.snapshot.paramMap.get('link'));
-
- this.workService.getWorkByLink(work_link).subscribe((response: Work) => {
- this.work = response as Work;
- });
- }
-
- onSubmit(form: NgForm) {
- this.workService.saveWork(form);
- }
-}
diff --git a/src/app/components/works-list/works-list.component.spec.ts b/src/app/components/works-list/works-list.component.spec.ts
index 9bbe47c..cc3130c 100644
--- a/src/app/components/works-list/works-list.component.spec.ts
+++ b/src/app/components/works-list/works-list.component.spec.ts
@@ -1,6 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WorksListComponent } from './works-list.component';
+import { RouterModule } from '@angular/router';
+import { HttpClientModule } from '@angular/common/http';
describe('WorksListComponent', () => {
let component: WorksListComponent;
@@ -8,7 +10,7 @@ describe('WorksListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [WorksListComponent],
+ imports: [WorksListComponent, HttpClientModule, RouterModule.forRoot([])],
}).compileComponents();
fixture = TestBed.createComponent(WorksListComponent);
diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts
new file mode 100644
index 0000000..55c3826
--- /dev/null
+++ b/src/app/models/user.model.ts
@@ -0,0 +1,6 @@
+export interface User {
+ id_user: number;
+ login: string;
+ password: string;
+ permissions: number;
+}
diff --git a/src/app/services/codeExecution.service.ts b/src/app/services/backendService.service.ts
similarity index 70%
rename from src/app/services/codeExecution.service.ts
rename to src/app/services/backendService.service.ts
index 3d57318..d6808f4 100644
--- a/src/app/services/codeExecution.service.ts
+++ b/src/app/services/backendService.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { SSE } from 'sse.js';
import { Observable, Subject } from 'rxjs';
+import { environment } from '../../environments/environment';
export type ExecutionMessage = {
type: 'stdout' | 'stderr' | 'exit';
@@ -10,14 +11,26 @@ export type ExecutionMessage = {
@Injectable({
providedIn: 'root',
})
-export class CodeExecutionService {
- private apiUrl = 'http://localhost:3000/run';
+export class BackendService {
+ private apiUrl = environment.apiUrl;
+
private resultSubject = new Subject();
constructor() {}
+ async createRoom(code: string) {
+ const reponse = await fetch(`${this.apiUrl}/live`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ code }),
+ });
+ return reponse.text();
+ }
+
executeCode(code: string, language: string) {
- const sse = new SSE(this.apiUrl, {
+ const sse = new SSE(`${this.apiUrl}/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/src/app/services/connection.service.ts b/src/app/services/connection.service.ts
new file mode 100644
index 0000000..a3b15c8
--- /dev/null
+++ b/src/app/services/connection.service.ts
@@ -0,0 +1,120 @@
+import { ChangeSet, Text } from '@codemirror/state';
+import { EditorView } from 'codemirror';
+import {
+ Update,
+ collab,
+ getSyncedVersion,
+ receiveUpdates,
+ sendableUpdates,
+} from '@codemirror/collab';
+import { ViewPlugin, ViewUpdate } from '@codemirror/view';
+
+export class Connection {
+ private requestId = 0;
+ private resolves: Record void> = {};
+
+ constructor(private client: WebSocket) {
+ client.addEventListener('message', (event) => {
+ const response = JSON.parse(event.data);
+ if ('_request' in response) {
+ const resolve = this.resolves[response._request];
+ if (resolve) {
+ resolve(response.payload);
+ } else {
+ console.error(
+ 'Received response for unknown or already used request',
+ response._request
+ );
+ }
+ } else {
+ console.error('Received invalid response', response._request);
+ }
+ });
+ }
+
+ request(body: Record): Promise {
+ body['_request'] = this.requestId;
+ this.client.send(JSON.stringify(body));
+ return new Promise(
+ (resolve) => (this.resolves[this.requestId++] = resolve)
+ );
+ }
+}
+
+function pushUpdates(
+ connection: Connection,
+ version: number,
+ fullUpdates: readonly Update[]
+): Promise {
+ // Strip off transaction data
+ let updates = fullUpdates.map((u) => ({
+ clientID: u.clientID,
+ changes: u.changes.toJSON(),
+ }));
+ return connection.request({ type: 'pushUpdates', version, updates });
+}
+
+function pullUpdates(
+ connection: Connection,
+ version: number
+): Promise {
+ return connection.request({ type: 'pullUpdates', version }).then((updates) =>
+ updates.map((u: any) => ({
+ changes: ChangeSet.fromJSON(u.changes),
+ clientID: u.clientID,
+ }))
+ );
+}
+
+export function getDocument(
+ connection: Connection
+): Promise<{ version: number; doc: Text }> {
+ return connection.request({ type: 'getDocument' }).then((data) => ({
+ version: data.version,
+ doc: Text.of(data.doc.split('\n')),
+ }));
+}
+
+export function peerExtension(startVersion: number, connection: Connection) {
+ let plugin = ViewPlugin.fromClass(
+ class {
+ private pushing = false;
+ private done = false;
+
+ constructor(private view: EditorView) {
+ this.pull();
+ }
+
+ update(update: ViewUpdate) {
+ if (update.docChanged) this.push();
+ }
+
+ async push() {
+ let updates = sendableUpdates(this.view.state);
+
+ if (this.pushing || !updates.length) return;
+ this.pushing = true;
+ let version = getSyncedVersion(this.view.state);
+ await pushUpdates(connection, version, updates);
+ this.pushing = false;
+ // Regardless of whether the push failed or new updates came in
+ // while it was running, try again if there's updates remaining
+ if (sendableUpdates(this.view.state).length)
+ setTimeout(() => this.push(), 100);
+ }
+
+ async pull() {
+ while (!this.done) {
+ let version = getSyncedVersion(this.view.state);
+ let updates = await pullUpdates(connection, version);
+ this.view.dispatch(receiveUpdates(this.view.state, updates));
+ }
+ }
+
+ destroy() {
+ this.done = true;
+ }
+ }
+ );
+ return [collab({ startVersion }), plugin];
+}
diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts
new file mode 100644
index 0000000..d2ca330
--- /dev/null
+++ b/src/app/services/user.service.ts
@@ -0,0 +1,50 @@
+import { Injectable } from '@angular/core';
+import { User } from '../models/user.model';
+import { HttpClient, HttpResponse } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { NgForm } from '@angular/forms';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class UserService {
+ API_URL = 'http://127.0.0.1:3000';
+
+ constructor(private http: HttpClient) {}
+
+ postUser(
+ email: string,
+ login: string,
+ password: string
+ ): Observable {
+ const body = {
+ email: email,
+ login: login,
+ password: password,
+ permissions: 0,
+ };
+
+ return this.http.post(`${this.API_URL}/users`, body);
+ }
+
+ loginUser(login: string, password: string): Observable {
+ const body = {
+ login: login,
+ password: password,
+ };
+
+ return this.http.post(`${this.API_URL}/users/login`, body, {
+ withCredentials: true,
+ });
+ }
+
+ logoutUser(): Observable {
+ return this.http.post(`${this.API_URL}/users/logout`, {
+ withCredentials: true,
+ });
+ }
+}
+
+type Response = {
+ success: boolean;
+};
diff --git a/src/app/services/work.service.spec.ts b/src/app/services/work.service.spec.ts
index f6cda06..6ddb976 100644
--- a/src/app/services/work.service.spec.ts
+++ b/src/app/services/work.service.spec.ts
@@ -1,12 +1,15 @@
import { TestBed } from '@angular/core/testing';
import { WorkService } from './work.service';
+import { HttpClientModule } from '@angular/common/http';
describe('WorkService', () => {
let service: WorkService;
beforeEach(() => {
- TestBed.configureTestingModule({});
+ TestBed.configureTestingModule({
+ imports: [HttpClientModule]
+ });
service = TestBed.inject(WorkService);
});
diff --git a/src/app/services/work.service.ts b/src/app/services/work.service.ts
index 2422ce6..b46ff13 100644
--- a/src/app/services/work.service.ts
+++ b/src/app/services/work.service.ts
@@ -14,30 +14,30 @@ export class WorkService {
constructor(private http: HttpClient) {}
- getWorks(): Observable {
- return this.http.get(`${this.API_URL}/works`);
+ getWorks(): Observable {
+ return this.http.get(`${this.API_URL}/works`);
}
- getWorkByLink(link: string): Observable {
- return this.http.get(`${this.API_URL}/works/${link}`);
+ getWorkByLink(link: string): Observable {
+ return this.http.get(`${this.API_URL}/works/${link}`);
}
saveWork(form: NgForm): void {
const code = form.value.content;
- this.http.post(`${this.API_URL}/works/save`, code).subscribe();
+ this.http.post(`${this.API_URL}/works/save`, code).subscribe();
}
postWork(code: string, language: string, id_user: number): string {
- let body = {
- id_user: id_user, // tant que ça pas résolu -> je peux pas faire le share
+ const body = {
+ id_user, // tant que ça pas résolu -> je peux pas faire le share
link: crypto.randomUUID(),
language: language,
title: `Basic ${language}`,
code: code,
};
- this.http.post(`${this.API_URL}/works`, body).subscribe();
+ this.http.post(`${this.API_URL}/works`, body).subscribe();
return body.link;
}
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
new file mode 100644
index 0000000..0900c39
--- /dev/null
+++ b/src/environments/environment.prod.ts
@@ -0,0 +1,5 @@
+export const environment = {
+ production: true,
+ apiUrl: 'https://tododomain.com',
+ webSocketUrl: 'ws://tododomain.com',
+};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
new file mode 100644
index 0000000..0411da0
--- /dev/null
+++ b/src/environments/environment.ts
@@ -0,0 +1,5 @@
+export const environment = {
+ production: false,
+ apiUrl: 'http://localhost:3000',
+ webSocketUrl: 'ws://localhost:3000',
+};
diff --git a/src/index.html b/src/index.html
index 4adf77b..464867f 100644
--- a/src/index.html
+++ b/src/index.html
@@ -11,8 +11,14 @@
+
+
-
+
diff --git a/src/main.ts b/src/main.ts
index 4d15ebd..713ebd7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -8,15 +8,17 @@ import {
HttpClient,
} from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
-import { AppRoutingModule } from './app/app-routing.module';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { TranslationService } from './app/services/translation.service';
+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
+import { provideRouter, withComponentInputBinding } from '@angular/router';
+import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
+ provideRouter(routes, withComponentInputBinding()),
importProvidersFrom(
BrowserModule,
- AppRoutingModule,
ReactiveFormsModule,
FormsModule,
TranslateModule.forRoot({
@@ -30,5 +32,6 @@ bootstrapApplication(AppComponent, {
),
TranslationService,
provideHttpClient(withInterceptorsFromDi()),
+ provideAnimationsAsync(),
],
}).catch(console.error);