Merge branch 'master' into form-login-register
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details

pull/14/head
Clément FRÉVILLE 1 year ago
commit 3067443127

@ -9,6 +9,13 @@ steps:
- npm install - npm install
- npm run build - npm run build
- name: test-chrome
image: timbru31/node-chrome:20-slim
commands:
- npm run test -- --browsers=ChromeHeadlessCI --watch=false
depends_on:
- build
- name: sonar - name: sonar
image: sonarsource/sonar-scanner-cli:5 image: sonarsource/sonar-scanner-cli:5
commands: commands:

1
.gitignore vendored

@ -19,6 +19,7 @@ yarn-error.log
*.launch *.launch
.settings/ .settings/
*.sublime-workspace *.sublime-workspace
.nx
# Visual Studio Code # Visual Studio Code
.vscode/* .vscode/*

@ -16,9 +16,14 @@
"outputPath": "dist/sandkasten", "outputPath": "dist/sandkasten",
"index": "src/index.html", "index": "src/index.html",
"browser": "src/main.ts", "browser": "src/main.ts",
"polyfills": ["zone.js"], "polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"], "assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/indigo-pink.css", "@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss" "src/styles.scss"
@ -27,6 +32,12 @@
}, },
"configurations": { "configurations": {
"production": { "production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
@ -42,6 +53,7 @@
"outputHashing": "all" "outputHashing": "all"
}, },
"development": { "development": {
"fileReplacements": [],
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true
@ -70,20 +82,30 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"polyfills": ["zone.js", "zone.js/testing"], "polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"assets": ["src/favicon.ico", "src/assets"], "assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/indigo-pink.css", "@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": [],
"karmaConfig": "karma.conf.js"
} }
}, },
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] "lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
} }
} }
} }
@ -91,6 +113,8 @@
}, },
"cli": { "cli": {
"analytics": false, "analytics": false,
"schematicCollections": ["@angular-eslint/schematics"] "schematicCollections": [
"@angular-eslint/schematics"
]
} }
} }

@ -0,0 +1,46 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-firefox-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/sandkasten'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
browsers: ['Chrome', 'Firefox'],
restartOnFileChange: true,
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
}
});
};

54
package-lock.json generated

@ -18,8 +18,11 @@
"@angular/platform-browser": "^17.3.7", "@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7", "@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7", "@angular/router": "^17.3.7",
"@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.2",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.26.3", "@codemirror/view": "^6.26.3",
"@emailjs/browser": "^4.3.3", "@emailjs/browser": "^4.3.3",
@ -50,6 +53,7 @@
"karma": "~6.4.2", "karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.1",
"karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
@ -2512,6 +2516,15 @@
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
}, },
"node_modules/@codemirror/collab": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz",
"integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0"
}
},
"node_modules/@codemirror/commands": { "node_modules/@codemirror/commands": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz",
@ -2547,9 +2560,9 @@
} }
}, },
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.10.1", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@ -2559,6 +2572,14 @@
"style-mod": "^4.0.0" "style-mod": "^4.0.0"
} }
}, },
"node_modules/@codemirror/legacy-modes": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.0.tgz",
"integrity": "sha512-5m/K+1A6gYR0e+h/dEde7LoGimMjRtWXZFg4Lo70cc8HzjSdHe3fLwjWMR0VRl5KFT1SxalSap7uMgPKF28wBA==",
"dependencies": {
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@codemirror/lint": { "node_modules/@codemirror/lint": {
"version": "6.7.0", "version": "6.7.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.7.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.7.0.tgz",
@ -10467,6 +10488,33 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/karma-firefox-launcher": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz",
"integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-wsl": "^2.2.0",
"which": "^3.0.0"
}
},
"node_modules/karma-firefox-launcher/node_modules/which": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
"integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/karma-jasmine": { "node_modules/karma-jasmine": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",

@ -22,8 +22,11 @@
"@angular/platform-browser": "^17.3.7", "@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7", "@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7", "@angular/router": "^17.3.7",
"@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.2",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.26.3", "@codemirror/view": "^6.26.3",
"@emailjs/browser": "^4.3.3", "@emailjs/browser": "^4.3.3",
@ -54,6 +57,7 @@
"karma": "~6.4.2", "karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.1",
"karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",

@ -1,11 +1,16 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(() => beforeEach(() =>
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule, AppComponent], imports: [
RouterModule.forRoot([]),
AppComponent,
TranslateModule.forRoot(),
],
}) })
); );
@ -19,8 +24,6 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement; const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain( expect(compiled.textContent).toContain('HeaderPage.Editor');
'sandkasten app is running!'
);
}); });
}); });

@ -1,5 +1,4 @@
import { NgModule } from '@angular/core'; import { Routes } from '@angular/router';
import { RouterModule, Routes } from '@angular/router';
import { EditorComponent } from './components/editor/editor.component'; import { EditorComponent } from './components/editor/editor.component';
import { LandingPageComponent } from './components/landing-page/landing-page.component'; import { LandingPageComponent } from './components/landing-page/landing-page.component';
import { DocumentationComponent } from './components/documentation/documentation.component'; import { DocumentationComponent } from './components/documentation/documentation.component';
@ -11,9 +10,10 @@ import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component'; import { LoginComponent } from './components/login/login.component';
// Toutes les routes de l'application sont définies ici // Toutes les routes de l'application sont définies ici
const routes: Routes = [ export const routes: Routes = [
{ path: '', component: LandingPageComponent }, { path: '', component: LandingPageComponent },
{ path: 'editor', component: EditorComponent }, { path: 'editor', component: EditorComponent },
{ path: 'editor-live/:idRoom', component: EditorComponent },
{ path: 'documentation', component: DocumentationComponent }, { path: 'documentation', component: DocumentationComponent },
{ path: 'contact', component: FormComponent }, { path: 'contact', component: FormComponent },
{ path: 'our-story', component: OurStoryComponent }, { path: 'our-story', component: OurStoryComponent },
@ -22,9 +22,3 @@ const routes: Routes = [
{ path: 'register', component: RegisterComponent }, { path: 'register', component: RegisterComponent },
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },
]; ];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

@ -59,6 +59,10 @@
</div> </div>
} }
<button class="button-join" (click)="onCreateRoomButtonClicked()">
Créer une salle
</button>
<select id="language" [(ngModel)]="selectedLanguage"> <select id="language" [(ngModel)]="selectedLanguage">
@for (language of languages; track language.name) { @for (language of languages; track language.name) {
<option [ngValue]="language">{{ language.name }}</option> <option [ngValue]="language">{{ language.name }}</option>
@ -68,7 +72,6 @@
<div class="param-editor"> <div class="param-editor">
<button <button
class="button-icon button-run" class="button-icon button-run"
type="button"
(click)="onRunButtonClicked()" (click)="onRunButtonClicked()"
[disabled]="isLoaded"> [disabled]="isLoaded">
<div>RUN</div> <div>RUN</div>

@ -40,6 +40,16 @@ svg {
border-radius: 10px; border-radius: 10px;
} }
.button-join {
background-color: #1c53bf;
border: none;
color: white;
padding: 12px 16px;
font-size: 16px;
cursor: pointer;
border-radius: 10px;
}
select { select {
background-color: #0000f0; background-color: #0000f0;
border: none; border: none;

@ -1,11 +1,12 @@
import { Component, ViewChild } from '@angular/core'; import { Component, Input, ViewChild } from '@angular/core';
import { CodeExecutionService } from 'src/app/services/codeExecution.service'; import { BackendService } from 'src/app/services/backendService.service';
import { Compartment } from '@codemirror/state'; import { Compartment, StateEffect } from '@codemirror/state';
import { CodeMirrorComponent } from '@sandkasten/codemirror6-editor'; import { CodeMirrorComponent } from '@sandkasten/codemirror6-editor';
import { LanguageDescription } from '@codemirror/language'; import { LanguageDescription } from '@codemirror/language';
import { CODE_DEFAULTS, LANGUAGES } from '../languages'; import { CODE_DEFAULTS, LANGUAGES } from '../languages';
import { SafeHTMLPipe } from '../../safe-html.pipe'; import { SafeHTMLPipe } from '../../safe-html.pipe';
import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { import {
keymap, keymap,
highlightSpecialChars, highlightSpecialChars,
@ -36,6 +37,12 @@ import {
closeBracketsKeymap, closeBracketsKeymap,
} from '@codemirror/autocomplete'; } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint'; import { lintKeymap } from '@codemirror/lint';
import {
Connection,
getDocument,
peerExtension,
} from '../../services/connection.service';
import { environment } from '../../../environments/environment';
const basicSetup: Extension = (() => [ const basicSetup: Extension = (() => [
highlightActiveLineGutter(), highlightActiveLineGutter(),
@ -88,6 +95,7 @@ export class EditorComponent {
get selectedLanguage(): LanguageDescription { get selectedLanguage(): LanguageDescription {
return this._selectedLanguage; return this._selectedLanguage;
} }
set selectedLanguage(value: LanguageDescription) { set selectedLanguage(value: LanguageDescription) {
this._selectedLanguage = value; this._selectedLanguage = value;
if (value.name in CODE_DEFAULTS) { if (value.name in CODE_DEFAULTS) {
@ -100,10 +108,12 @@ export class EditorComponent {
}); });
}); });
} }
private _linesNumbers: boolean = true; private _linesNumbers: boolean = true;
get linesNumbers() { get linesNumbers() {
return this._linesNumbers; return this._linesNumbers;
} }
set linesNumbers(lines: boolean) { set linesNumbers(lines: boolean) {
this._linesNumbers = lines; this._linesNumbers = lines;
this.codemirror.editor?.dispatch({ this.codemirror.editor?.dispatch({
@ -131,21 +141,58 @@ export class EditorComponent {
this.languageCompartment.of(this.selectedLanguage.support!), this.languageCompartment.of(this.selectedLanguage.support!),
]; ];
constructor(private codeExecutionService: CodeExecutionService) {} private client: WebSocket | undefined;
@Input()
set idRoom(idRoom: string) {
if (idRoom === undefined) {
return;
}
this.client = new WebSocket(`${environment.webSocketUrl}/live/${idRoom}`);
this.client.addEventListener('open', async () => {
let conn = new Connection(this.client!);
let { version, doc } = await getDocument(conn);
this.codemirror.editor?.dispatch({
changes: {
from: 0,
to: this.codemirror.editor.state.doc.length,
insert: doc,
},
});
this.codemirror.editor?.dispatch({
effects: StateEffect.appendConfig.of([peerExtension(version, conn)]),
});
});
}
constructor(
private router: Router,
private backendService: BackendService
) {
backendService.getResult().subscribe((msg) => {
if (msg.type === 'stdout' || msg.type === 'stderr') {
this.resultContent += msg.text;
}
});
}
// Efface le contenu de l'éditeur // Efface le contenu de l'éditeur
clear(): void { clear(): void {
this.editorContent = ''; this.editorContent = '';
} }
async onCreateRoomButtonClicked() {
const idRoom = await this.backendService.createRoom(this.editorContent);
await this.router.navigate([`./editor-live/${idRoom}`]);
}
onRunButtonClicked() { onRunButtonClicked() {
// Le code à exécuter est le contenu de l'éditeur // Le code à exécuter est le contenu de l'éditeur
const codeToExecute = this.editorContent; const codeToExecute = this.editorContent;
this.codeExecutionService.executeCode( this.backendService.executeCode(codeToExecute, this.selectedLanguage.name);
codeToExecute,
this.selectedLanguage.name
);
this.resultContent = ''; this.resultContent = '';
} }

@ -1,6 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component'; import { HeaderComponent } from './header.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
describe('HeaderComponent', () => { describe('HeaderComponent', () => {
let component: HeaderComponent; let component: HeaderComponent;
@ -8,7 +10,11 @@ describe('HeaderComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HeaderComponent], imports: [
RouterModule.forRoot([]),
HeaderComponent,
TranslateModule.forRoot(),
],
}); });
fixture = TestBed.createComponent(HeaderComponent); fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

@ -1,6 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LandingPageComponent } from './landing-page.component'; import { LandingPageComponent } from './landing-page.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
describe('LandingPageComponent', () => { describe('LandingPageComponent', () => {
let component: LandingPageComponent; let component: LandingPageComponent;
@ -8,7 +10,11 @@ describe('LandingPageComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [LandingPageComponent], imports: [
RouterModule.forRoot([]),
LandingPageComponent,
TranslateModule.forRoot(),
],
}); });
fixture = TestBed.createComponent(LandingPageComponent); fixture = TestBed.createComponent(LandingPageComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

@ -1,6 +1,15 @@
import { LanguageDescription } from '@codemirror/language'; import {
LanguageDescription,
LanguageSupport,
StreamLanguage,
StreamParser,
} from '@codemirror/language';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
function legacy(parser: StreamParser<unknown>): LanguageSupport {
return new LanguageSupport(StreamLanguage.define(parser));
}
export const CODE_DEFAULTS = { export const CODE_DEFAULTS = {
C: /** @lang C */ `#include <stdio.h> C: /** @lang C */ `#include <stdio.h>
int main() { int main() {
@ -14,6 +23,7 @@ int main() {
}`, }`,
JavaScript: /** @lang JS */ `console.log("Hello, World!");`, JavaScript: /** @lang JS */ `console.log("Hello, World!");`,
TypeScript: /** @lang TS */ `console.log("Hello, World!");`, TypeScript: /** @lang TS */ `console.log("Hello, World!");`,
Bash: 'echo "Hello, world!"',
}; };
export const LANGUAGES = [ export const LANGUAGES = [
@ -48,4 +58,15 @@ export const LANGUAGES = [
); );
}, },
}), }),
LanguageDescription.of({
name: 'Bash',
alias: ['bash', 'sh', 'sh'],
extensions: ['sh', 'ksh', 'bash'],
filename: /^PKGBUILD$/,
load() {
return import('@codemirror/legacy-modes/mode/shell').then((m) =>
legacy(m.shell)
);
},
}),
]; ];

@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PrivacyPolicyComponent } from './privacy-policy.component'; import { PrivacyPolicyComponent } from './privacy-policy.component';
import { TranslateModule } from '@ngx-translate/core';
describe('PrivacyPolicyComponent', () => { describe('PrivacyPolicyComponent', () => {
let component: PrivacyPolicyComponent; let component: PrivacyPolicyComponent;
@ -8,7 +9,7 @@ describe('PrivacyPolicyComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PrivacyPolicyComponent], imports: [PrivacyPolicyComponent, TranslateModule.forRoot()],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(PrivacyPolicyComponent); fixture = TestBed.createComponent(PrivacyPolicyComponent);

@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TermsOfServiceComponent } from './terms-of-service.component'; import { TermsOfServiceComponent } from './terms-of-service.component';
import { TranslateModule } from '@ngx-translate/core';
describe('TermsOfServiceComponent', () => { describe('TermsOfServiceComponent', () => {
let component: TermsOfServiceComponent; let component: TermsOfServiceComponent;
@ -8,7 +9,7 @@ describe('TermsOfServiceComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TermsOfServiceComponent], imports: [TermsOfServiceComponent, TranslateModule.forRoot()],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(TermsOfServiceComponent); fixture = TestBed.createComponent(TermsOfServiceComponent);

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SSE } from 'sse.js'; import { SSE } from 'sse.js';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { environment } from '../../environments/environment';
export type ExecutionMessage = { export type ExecutionMessage = {
type: 'stdout' | 'stderr' | 'exit'; type: 'stdout' | 'stderr' | 'exit';
@ -10,14 +11,26 @@ export type ExecutionMessage = {
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class CodeExecutionService { export class BackendService {
private apiUrl = 'http://localhost:3000/run'; private apiUrl = environment.apiUrl;
private resultSubject = new Subject<ExecutionMessage>(); private resultSubject = new Subject<ExecutionMessage>();
constructor() {} 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) { executeCode(code: string, language: string) {
const sse = new SSE(this.apiUrl, { const sse = new SSE(`${this.apiUrl}/run`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

@ -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<number, (value: any) => 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<string, unknown>): Promise<any> {
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<boolean> {
// 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<readonly Update[]> {
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];
}

@ -0,0 +1,5 @@
export const environment = {
production: true,
apiUrl: 'https://tododomain.com',
webSocketUrl: 'ws://tododomain.com',
};

@ -0,0 +1,5 @@
export const environment = {
production: false,
apiUrl: 'http://localhost:3000',
webSocketUrl: 'ws://localhost:3000',
};

@ -8,16 +8,17 @@ import {
HttpClient, HttpClient,
} from '@angular/common/http'; } from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app/app-routing.module';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { TranslationService } from './app/services/translation.service'; import { TranslationService } from './app/services/translation.service';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, {
providers: [ providers: [
provideRouter(routes, withComponentInputBinding()),
importProvidersFrom( importProvidersFrom(
BrowserModule, BrowserModule,
AppRoutingModule,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
TranslateModule.forRoot({ TranslateModule.forRoot({

Loading…
Cancel
Save