aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortranstrike <transtrike@gmail.com>2021-02-12 19:04:53 +0200
committertranstrike <transtrike@gmail.com>2021-02-12 19:04:53 +0200
commitbcd88af53b1a920d728ec98b45daa9ac2e2c0917 (patch)
treefd27eef086822b0f02f74364cac0b940956d2458 /src
parent1d1f05e3f74d70a558b4847a9107afa7952131cf (diff)
downloadDevHive-Angular-bcd88af53b1a920d728ec98b45daa9ac2e2c0917.tar
DevHive-Angular-bcd88af53b1a920d728ec98b45daa9ac2e2c0917.tar.gz
DevHive-Angular-bcd88af53b1a920d728ec98b45daa9ac2e2c0917.zip
Moved from DevHive
Diffstat (limited to 'src')
-rw-r--r--src/app/app-constants.module.ts20
-rw-r--r--src/app/app-routing.module.ts30
-rw-r--r--src/app/app-shell/app-shell.component.css0
-rw-r--r--src/app/app-shell/app-shell.component.html1
-rw-r--r--src/app/app-shell/app-shell.component.spec.ts25
-rw-r--r--src/app/app-shell/app-shell.component.ts15
-rw-r--r--src/app/app.component.css4
-rw-r--r--src/app/app.component.html3
-rw-r--r--src/app/app.component.spec.ts35
-rw-r--r--src/app/app.component.ts10
-rw-r--r--src/app/app.module.ts62
-rw-r--r--src/app/app.server.module.ts20
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.css42
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.html85
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.ts334
-rw-r--r--src/app/components/comment-page/comment-page.component.css27
-rw-r--r--src/app/components/comment-page/comment-page.component.html14
-rw-r--r--src/app/components/comment-page/comment-page.component.ts91
-rw-r--r--src/app/components/comment/comment.component.css59
-rw-r--r--src/app/components/comment/comment.component.html23
-rw-r--r--src/app/components/comment/comment.component.ts54
-rw-r--r--src/app/components/error-bar/error-bar.component.css12
-rw-r--r--src/app/components/error-bar/error-bar.component.html1
-rw-r--r--src/app/components/error-bar/error-bar.component.ts34
-rw-r--r--src/app/components/feed/feed.component.css179
-rw-r--r--src/app/components/feed/feed.component.html52
-rw-r--r--src/app/components/feed/feed.component.ts122
-rw-r--r--src/app/components/kaleidoscope/kaleidoscope.component.css0
-rw-r--r--src/app/components/kaleidoscope/kaleidoscope.component.html8
-rw-r--r--src/app/components/kaleidoscope/kaleidoscope.component.ts24
-rw-r--r--src/app/components/loading/loading.component.css0
-rw-r--r--src/app/components/loading/loading.component.html3
-rw-r--r--src/app/components/loading/loading.component.ts15
-rw-r--r--src/app/components/login/login.component.css32
-rw-r--r--src/app/components/login/login.component.html30
-rw-r--r--src/app/components/login/login.component.ts59
-rw-r--r--src/app/components/not-found/not-found.component.css23
-rw-r--r--src/app/components/not-found/not-found.component.html10
-rw-r--r--src/app/components/not-found/not-found.component.ts27
-rw-r--r--src/app/components/post-attachment/post-attachment.component.css75
-rw-r--r--src/app/components/post-attachment/post-attachment.component.html18
-rw-r--r--src/app/components/post-attachment/post-attachment.component.ts27
-rw-r--r--src/app/components/post-page/post-page.component.css62
-rw-r--r--src/app/components/post-page/post-page.component.html37
-rw-r--r--src/app/components/post-page/post-page.component.ts162
-rw-r--r--src/app/components/post/post.component.css134
-rw-r--r--src/app/components/post/post.component.html49
-rw-r--r--src/app/components/post/post.component.ts57
-rw-r--r--src/app/components/profile-settings/profile-settings.component.css124
-rw-r--r--src/app/components/profile-settings/profile-settings.component.html116
-rw-r--r--src/app/components/profile-settings/profile-settings.component.ts307
-rw-r--r--src/app/components/profile/profile.component.css105
-rw-r--r--src/app/components/profile/profile.component.html60
-rw-r--r--src/app/components/profile/profile.component.ts203
-rw-r--r--src/app/components/register/register.component.css40
-rw-r--r--src/app/components/register/register.component.html65
-rw-r--r--src/app/components/register/register.component.ts86
-rw-r--r--src/app/components/success-bar/success-bar.component.css11
-rw-r--r--src/app/components/success-bar/success-bar.component.html1
-rw-r--r--src/app/components/success-bar/success-bar.component.ts33
-rw-r--r--src/app/services/cloudinary.service.ts17
-rw-r--r--src/app/services/comment.service.ts83
-rw-r--r--src/app/services/feed.service.ts50
-rw-r--r--src/app/services/language.service.ts113
-rw-r--r--src/app/services/post.service.ts86
-rw-r--r--src/app/services/technology.service.ts113
-rw-r--r--src/app/services/token.service.ts47
-rw-r--r--src/app/services/user.service.ts179
-rw-r--r--src/assets/.gitkeep0
-rw-r--r--src/assets/images/comment.pngbin0 -> 31366 bytes
-rw-r--r--src/assets/images/feed/chat-pic.pngbin0 -> 7634 bytes
-rw-r--r--src/assets/images/feed/profile-pic.pngbin0 -> 7870 bytes
-rw-r--r--src/assets/images/paper-clip.pngbin0 -> 2923 bytes
-rw-r--r--src/environments/environment.prod.ts3
-rw-r--r--src/environments/environment.ts16
-rw-r--r--src/favicon.icobin0 -> 948 bytes
-rw-r--r--src/index.html16
-rw-r--r--src/interfaces/api-error.ts6
-rw-r--r--src/interfaces/jwt-payload.ts3
-rw-r--r--src/interfaces/user-credentials.ts6
-rw-r--r--src/main.server.ts10
-rw-r--r--src/main.ts14
-rw-r--r--src/models/comment.ts70
-rw-r--r--src/models/identity/friend.ts3
-rw-r--r--src/models/identity/role.ts3
-rw-r--r--src/models/identity/user.ts100
-rw-r--r--src/models/language.ts6
-rw-r--r--src/models/post-comment.ts5
-rw-r--r--src/models/post.ts81
-rw-r--r--src/models/technology.ts6
-rw-r--r--src/polyfills.ts63
-rw-r--r--src/reset.css52
-rw-r--r--src/styles.css262
-rw-r--r--src/theme.scss14
94 files changed, 4784 insertions, 0 deletions
diff --git a/src/app/app-constants.module.ts b/src/app/app-constants.module.ts
new file mode 100644
index 0000000..d72af53
--- /dev/null
+++ b/src/app/app-constants.module.ts
@@ -0,0 +1,20 @@
+export class AppConstants {
+ public static BASE_API_URL = 'http://localhost:5000/api';
+
+ public static API_USER_URL = AppConstants.BASE_API_URL + '/User';
+ public static API_USER_LOGIN_URL = AppConstants.API_USER_URL + '/login';
+ public static API_USER_REGISTER_URL = AppConstants.API_USER_URL + '/register';
+
+ public static API_LANGUAGE_URL = AppConstants.BASE_API_URL + '/Language';
+ public static API_TECHNOLOGY_URL = AppConstants.BASE_API_URL + '/Technology';
+
+ public static API_POST_URL = AppConstants.BASE_API_URL + '/Post';
+ public static API_FEED_URL = AppConstants.BASE_API_URL + '/Feed';
+ public static API_COMMENT_URL = AppConstants.BASE_API_URL + '/Comment';
+
+ public static PAGE_SIZE = 10;
+ public static FALLBACK_PROFILE_ICON = 'assets/images/feed/profile-pic.png';
+
+ public static SESSION_TOKEN_KEY = 'UserCred';
+ public static ADMIN_ROLE_NAME = 'Admin';
+}
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
new file mode 100644
index 0000000..0d83079
--- /dev/null
+++ b/src/app/app-routing.module.ts
@@ -0,0 +1,30 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { FeedComponent } from './components/feed/feed.component';
+import { LoginComponent } from './components/login/login.component';
+import { RegisterComponent } from './components/register/register.component';
+import { ProfileComponent } from './components/profile/profile.component';
+import { ProfileSettingsComponent } from './components/profile-settings/profile-settings.component';
+import { NotFoundComponent } from './components/not-found/not-found.component';
+import { PostPageComponent } from './components/post-page/post-page.component';
+import {AdminPanelPageComponent} from './components/admin-panel-page/admin-panel-page.component';
+import {CommentPageComponent} from './components/comment-page/comment-page.component';
+
+const routes: Routes = [
+ { path: '', component: FeedComponent },
+ { path: 'login', component: LoginComponent },
+ { path: 'register', component: RegisterComponent },
+ { path: 'profile/:username', component: ProfileComponent },
+ { path: 'profile/:username/settings', component: ProfileSettingsComponent },
+ { path: 'post/:id', component: PostPageComponent },
+ { path: 'comment/:id', component: CommentPageComponent },
+ { path: 'admin-panel', component: AdminPanelPageComponent },
+ { path: 'not-found', component: NotFoundComponent },
+ { path: '**', component: NotFoundComponent }
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes)],
+ exports: [RouterModule]
+})
+export class AppRoutingModule { }
diff --git a/src/app/app-shell/app-shell.component.css b/src/app/app-shell/app-shell.component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/app-shell/app-shell.component.css
diff --git a/src/app/app-shell/app-shell.component.html b/src/app/app-shell/app-shell.component.html
new file mode 100644
index 0000000..b5f713a
--- /dev/null
+++ b/src/app/app-shell/app-shell.component.html
@@ -0,0 +1 @@
+<p>app-shell works!</p>
diff --git a/src/app/app-shell/app-shell.component.spec.ts b/src/app/app-shell/app-shell.component.spec.ts
new file mode 100644
index 0000000..2c4fabc
--- /dev/null
+++ b/src/app/app-shell/app-shell.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppShellComponent } from './app-shell.component';
+
+describe('AppShellComponent', () => {
+ let component: AppShellComponent;
+ let fixture: ComponentFixture<AppShellComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ AppShellComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AppShellComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/app-shell/app-shell.component.ts b/src/app/app-shell/app-shell.component.ts
new file mode 100644
index 0000000..f1bca89
--- /dev/null
+++ b/src/app/app-shell/app-shell.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-app-shell',
+ templateUrl: './app-shell.component.html',
+ styleUrls: ['./app-shell.component.css']
+})
+export class AppShellComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/app.component.css b/src/app/app.component.css
new file mode 100644
index 0000000..aaebe5b
--- /dev/null
+++ b/src/app/app.component.css
@@ -0,0 +1,4 @@
+.main {
+ width: 100%;
+ height: 100%;
+}
diff --git a/src/app/app.component.html b/src/app/app.component.html
new file mode 100644
index 0000000..6c51026
--- /dev/null
+++ b/src/app/app.component.html
@@ -0,0 +1,3 @@
+<div class="main">
+ <router-outlet></router-outlet>
+</div>
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
new file mode 100644
index 0000000..e4bf775
--- /dev/null
+++ b/src/app/app.component.spec.ts
@@ -0,0 +1,35 @@
+import { TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { AppComponent } from './app.component';
+
+describe('AppComponent', () => {
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule
+ ],
+ declarations: [
+ AppComponent
+ ],
+ }).compileComponents();
+ });
+
+ it('should create the app', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.componentInstance;
+ expect(app).toBeTruthy();
+ });
+
+ it(`should have as title 'Angular'`, () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.componentInstance;
+ expect(app.title).toEqual('Angular');
+ });
+
+ it('should render title', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
+ const compiled = fixture.nativeElement;
+ expect(compiled.querySelector('.content span').textContent).toContain('Angular app is running!');
+ });
+});
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
new file mode 100644
index 0000000..b9f46ae
--- /dev/null
+++ b/src/app/app.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.css']
+})
+export class AppComponent {
+ title = 'Angular';
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
new file mode 100644
index 0000000..1bf44ad
--- /dev/null
+++ b/src/app/app.module.ts
@@ -0,0 +1,62 @@
+import { BrowserModule } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { HttpClientModule } from '@angular/common/http';
+
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { LoginComponent } from './components/login/login.component';
+import { RegisterComponent } from './components/register/register.component';
+import { FeedComponent } from './components/feed/feed.component';
+import { PostComponent } from './components/post/post.component';
+import { ProfileComponent } from './components/profile/profile.component';
+import { ProfileSettingsComponent } from './components/profile-settings/profile-settings.component';
+import { NotFoundComponent } from './components/not-found/not-found.component';
+import { LoadingComponent } from './components/loading/loading.component';
+import { ErrorBarComponent } from './components/error-bar/error-bar.component';
+import { SuccessBarComponent } from './components/success-bar/success-bar.component';
+import { PostPageComponent } from './components/post-page/post-page.component';
+import { AdminPanelPageComponent } from './components/admin-panel-page/admin-panel-page.component';
+import { CommentComponent } from './components/comment/comment.component';
+import { CommentPageComponent } from './components/comment-page/comment-page.component';
+import { PostAttachmentComponent } from './components/post-attachment/post-attachment.component';
+import { RouterModule } from '@angular/router';
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ LoginComponent,
+ RegisterComponent,
+ FeedComponent,
+ PostComponent,
+ ProfileComponent,
+ ProfileSettingsComponent,
+ NotFoundComponent,
+ LoadingComponent,
+ ErrorBarComponent,
+ SuccessBarComponent,
+ PostPageComponent,
+ AdminPanelPageComponent,
+ CommentComponent,
+ CommentPageComponent,
+ PostAttachmentComponent
+ ],
+ imports: [
+ BrowserModule.withServerTransition({ appId: 'serverApp' }),
+ AppRoutingModule,
+ ReactiveFormsModule,
+ BrowserAnimationsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatButtonModule,
+ HttpClientModule,
+ RouterModule
+ ],
+ providers: [],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/src/app/app.server.module.ts b/src/app/app.server.module.ts
new file mode 100644
index 0000000..18b08b1
--- /dev/null
+++ b/src/app/app.server.module.ts
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { ServerModule } from '@angular/platform-server';
+
+import { AppModule } from './app.module';
+import { AppComponent } from './app.component';
+import { Routes, RouterModule } from '@angular/router';
+import { AppShellComponent } from './app-shell/app-shell.component';
+
+const routes: Routes = [ { path: 'shell', component: AppShellComponent }];
+
+@NgModule({
+ imports: [
+ AppModule,
+ ServerModule,
+ RouterModule.forRoot(routes),
+ ],
+ bootstrap: [AppComponent],
+ declarations: [AppShellComponent],
+})
+export class AppServerModule {}
diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.css b/src/app/components/admin-panel-page/admin-panel-page.component.css
new file mode 100644
index 0000000..1f98e20
--- /dev/null
+++ b/src/app/components/admin-panel-page/admin-panel-page.component.css
@@ -0,0 +1,42 @@
+#content {
+ max-width: 22em;
+ justify-content: start;
+}
+
+hr {
+ width: calc(100% - 1em);
+ color: black;
+ border: 1px solid black;
+}
+
+#navigation {
+ width: 100%;
+ display: flex;
+}
+
+#navigation > * {
+ flex: 1;
+ margin-left: .4em;
+}
+
+.submit-btn:first-of-type {
+ margin-left: 0 !important;
+}
+
+#all-languages, #all-technologies {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.flexbox {
+ display: flex;
+}
+
+.flexbox > * {
+ flex: 1;
+ margin-left: 1em;
+}
+
+.flexbox > *:first-child {
+ margin-left: 0;
+}
diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.html b/src/app/components/admin-panel-page/admin-panel-page.component.html
new file mode 100644
index 0000000..980f12c
--- /dev/null
+++ b/src/app/components/admin-panel-page/admin-panel-page.component.html
@@ -0,0 +1,85 @@
+<!-- <app&#45;loading *ngIf="!dataArrived"></app&#45;loading> -->
+
+<div id="content" *ngIf="!dataArrived">
+ <nav id="navigation">
+ <button class="submit-btn" (click)="backToProfile()">ᐊ Back to profile</button>
+ <button class="submit-btn" (click)="backToFeed()">ᐊ Back to feed</button>
+ <button class="submit-btn" (click)="logout()">Logout</button>
+ </nav>
+ <hr>
+ <div class="scroll-standalone">
+ <app-success-bar></app-success-bar>
+ <app-error-bar></app-error-bar>
+
+ <button type="button" class="submit-btn edit-btn" (click)="toggleLanguages()">▼ Edit Languages ▼</button>
+ <form [formGroup]="languageForm" (ngSubmit)="submitLanguages()" *ngIf="showLanguages">
+ <div class="input-selection">
+ <label>Create language:</label>
+ <input type="text" class="input-field" formControlName="languageCreate" placeholder="New language name">
+ </div>
+ <label>Update language:</label>
+ <div class="flexbox input-selection">
+ <input type="text" class="input-field" formControlName="updateLanguageOldName" placeholder="Old language name">
+ <input type="text" class="input-field" formControlName="updateLanguageNewName" placeholder="New language name">
+ </div>
+ <label>Delete language:</label>
+ <div class="flexbox input-selection">
+ <input type="text" class="input-field" formControlName="deleteLanguageName" placeholder="Language name">
+ </div>
+ <button class="submit-btn" type="submit">Modify languages</button>
+ <hr>
+ Available languages:
+ <div id="all-languages">
+ <div class="user-language" *ngFor="let lang of availableLanguages">
+ {{ lang.name }}
+ </div>
+ </div>
+ <hr>
+ </form>
+
+ <button type="button" class="submit-btn edit-btn" (click)="toggleTechnologies()">▼ Edit Technologies ▼</button>
+ <form [formGroup]="technologyForm" (ngSubmit)="submitTechnologies()" *ngIf="showTechnologies">
+ <div class="input-selection">
+ <label>Create technology:</label>
+ <input type="text" class="input-field" formControlName="technologyCreate" placeholder="New technology name">
+ </div>
+ <label>Update technology:</label>
+ <div class="flexbox input-selection">
+ <input type="text" class="input-field" formControlName="updateTechnologyOldName" placeholder="Old technology name">
+ <input type="text" class="input-field" formControlName="updateTechnologyNewName" placeholder="New technology name">
+ </div>
+ <label>Delete technology:</label>
+ <div class="flexbox input-selection">
+ <input type="text" class="input-field" formControlName="deleteTechnologyName" placeholder="Technology name">
+ </div>
+ <button class="submit-btn" type="submit">Modify technologies</button>
+ <hr>
+ Available technologies:
+ <div id="all-technologies">
+ <div class="user-technology" *ngFor="let tech of availableTechnologies">
+ {{ tech.name }}
+ </div>
+ </div>
+ <hr>
+ </form>
+
+ <button type="button" class="submit-btn delete-btn" (click)="toggleDeletions()">▼ Deletions ▼</button>
+ <form [formGroup]="deleteForm" (ngSubmit)="submitDelete()" *ngIf="showDeletions">
+ <div class="input-selection">
+ <label>Delete user by Id:</label>
+ <input type="text" class="input-field" formControlName="deleteUser" placeholder="User Id">
+ </div>
+
+ <div class="input-selection">
+ <label>Delete post by Id:</label>
+ <input type="text" class="input-field" formControlName="deletePost" placeholder="Post Id">
+ </div>
+ <div class="input-selection">
+ <label>Delete comment by Id:</label>
+ <input type="text" class="input-field" formControlName="deleteComment" placeholder="Comment Id">
+ </div>
+ <button class="submit-btn" type="submit">Delete</button>
+ <hr>
+ </form>
+ </div>
+</div>
diff --git a/src/app/components/admin-panel-page/admin-panel-page.component.ts b/src/app/components/admin-panel-page/admin-panel-page.component.ts
new file mode 100644
index 0000000..99c0721
--- /dev/null
+++ b/src/app/components/admin-panel-page/admin-panel-page.component.ts
@@ -0,0 +1,334 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { Title } from '@angular/platform-browser';
+import { Router } from '@angular/router';
+import { Guid } from 'guid-typescript';
+import { AppConstants } from 'src/app/app-constants.module';
+import { CommentService } from 'src/app/services/comment.service';
+import { LanguageService } from 'src/app/services/language.service';
+import { PostService } from 'src/app/services/post.service';
+import { TechnologyService } from 'src/app/services/technology.service';
+import { TokenService } from 'src/app/services/token.service';
+import { UserService } from 'src/app/services/user.service';
+import { User } from 'src/models/identity/user';
+import { Language } from 'src/models/language';
+import { Technology } from 'src/models/technology';
+import { ErrorBarComponent } from '../error-bar/error-bar.component';
+import { SuccessBarComponent } from '../success-bar/success-bar.component';
+
+@Component({
+ selector: 'app-admin-panel-page',
+ templateUrl: './admin-panel-page.component.html',
+ styleUrls: ['./admin-panel-page.component.css']
+})
+export class AdminPanelPageComponent implements OnInit {
+ private _title = 'Admin Panel';
+ @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent;
+ @ViewChild(SuccessBarComponent) private _successBar: SuccessBarComponent;
+ public dataArrived = false;
+ public showLanguages = false;
+ public showTechnologies = false;
+ public showDeletions = false;
+ public availableLanguages: Language[];
+ public availableTechnologies: Technology[];
+ public languageForm: FormGroup;
+ public technologyForm: FormGroup;
+ public deleteForm: FormGroup;
+
+ constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService) {
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ if (!this._tokenService.getTokenFromSessionStorage()) {
+ this._router.navigate(['/login']);
+ return;
+ }
+
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (result: object) => {
+ const user = result as User;
+ if (!user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME)) {
+ this._router.navigate(['/login']);
+ }
+ },
+ (err: HttpErrorResponse) => {
+ this._router.navigate(['/login']);
+ }
+ );
+
+ this.languageForm = this._fb.group({
+ languageCreate: new FormControl(''),
+ updateLanguageOldName: new FormControl(''),
+ updateLanguageNewName: new FormControl(''),
+ deleteLanguageName: new FormControl('')
+ });
+
+ this.languageForm.valueChanges.subscribe(() => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ });
+
+ this.technologyForm = this._fb.group({
+ technologyCreate: new FormControl(''),
+ updateTechnologyOldName: new FormControl(''),
+ updateTechnologyNewName: new FormControl(''),
+ deleteTechnologyName: new FormControl('')
+ });
+
+ this.technologyForm.valueChanges.subscribe(() => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ });
+
+ this.deleteForm = this._fb.group({
+ deleteUser: new FormControl(''),
+ deletePost: new FormControl(''),
+ deleteComment: new FormControl('')
+ });
+
+ this.deleteForm.valueChanges.subscribe(() => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ });
+
+ this.loadAvailableLanguages();
+ this.loadAvailableTechnologies();
+ }
+
+ // Navigation
+
+ backToProfile(): void {
+ this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]);
+ }
+
+ backToFeed(): void {
+ this._router.navigate(['/']);
+ }
+
+ logout(): void {
+ this._tokenService.logoutUserFromSessionStorage();
+ this._router.navigate(['/login']);
+ }
+
+ // Language modifying
+
+ toggleLanguages(): void {
+ this.showLanguages = !this.showLanguages;
+ }
+
+ submitLanguages(): void {
+ this.tryCreateLanguage();
+ this.tryUpdateLanguage();
+ this.tryDeleteLanguage();
+ }
+
+ private tryCreateLanguage(): void {
+ const languageCreate: string = this.languageForm.get('languageCreate')?.value;
+
+ if (languageCreate !== '' && languageCreate !== null) {
+ this._languageService.createLanguageWithSessionStorageRequest(languageCreate.trim()).subscribe(
+ (result: object) => {
+ this.languageModifiedSuccess('Successfully updated languages!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private tryUpdateLanguage(): void {
+ const updateLanguageOldName: string = this.languageForm.get('updateLanguageOldName')?.value;
+ const updateLanguageNewName: string = this.languageForm.get('updateLanguageNewName')?.value;
+
+ if (updateLanguageOldName !== '' && updateLanguageOldName !== null && updateLanguageNewName !== '' && updateLanguageNewName !== null) {
+ const langId = this.availableLanguages.filter(x => x.name === updateLanguageOldName.trim())[0].id;
+
+ this._languageService.putLanguageWithSessionStorageRequest(langId, updateLanguageNewName.trim()).subscribe(
+ (result: object) => {
+ this.languageModifiedSuccess('Successfully updated languages!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private tryDeleteLanguage(): void {
+ const deleteLanguageName: string = this.languageForm.get('deleteLanguageName')?.value;
+
+ if (deleteLanguageName !== '' && deleteLanguageName !== null) {
+ const langId = this.availableLanguages.filter(x => x.name === deleteLanguageName.trim())[0].id;
+
+ this._languageService.deleteLanguageWithSessionStorageRequest(langId).subscribe(
+ (result: object) => {
+ this.languageModifiedSuccess('Successfully deleted language!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private languageModifiedSuccess(successMsg: string): void {
+ this.languageForm.reset();
+ this._successBar.showMsg(successMsg);
+ this.loadAvailableLanguages();
+ }
+
+ private loadAvailableLanguages(): void {
+ this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe(
+ (result: object) => {
+ this.availableLanguages = result as Language[];
+ }
+ );
+ }
+
+ // Technology modifying
+
+ toggleTechnologies(): void {
+ this.showTechnologies = !this.showTechnologies;
+ }
+
+ submitTechnologies(): void {
+ this.tryCreateTechnology();
+ this.tryUpdateTechnology();
+ this.tryDeleteTechnology();
+ }
+
+ private tryCreateTechnology(): void {
+ const technologyCreate: string = this.technologyForm.get('technologyCreate')?.value;
+
+ if (technologyCreate !== '' && technologyCreate !== null) {
+ this._technologyService.createTechnologyWithSessionStorageRequest(technologyCreate.trim()).subscribe(
+ (result: object) => {
+ this.technologyModifiedSuccess('Successfully updated technologies!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private tryUpdateTechnology(): void {
+ const updateTechnologyOldName: string = this.technologyForm.get('updateTechnologyOldName')?.value;
+ const updateTechnologyNewName: string = this.technologyForm.get('updateTechnologyNewName')?.value;
+
+ if (updateTechnologyOldName !== '' && updateTechnologyOldName !== null && updateTechnologyNewName !== '' && updateTechnologyNewName !== null) {
+ const techId = this.availableTechnologies.filter(x => x.name === updateTechnologyOldName.trim())[0].id;
+
+ this._technologyService.putTechnologyWithSessionStorageRequest(techId, updateTechnologyNewName.trim()).subscribe(
+ (result: object) => {
+ this.technologyModifiedSuccess('Successfully updated technologies!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private tryDeleteTechnology(): void {
+ const deleteTechnologyName: string = this.technologyForm.get('deleteTechnologyName')?.value;
+
+ if (deleteTechnologyName !== '' && deleteTechnologyName !== null) {
+ const techId = this.availableTechnologies.filter(x => x.name === deleteTechnologyName.trim())[0].id;
+
+ this._technologyService.deleteTechnologyWithSessionStorageRequest(techId).subscribe(
+ (result: object) => {
+ this.technologyModifiedSuccess('Successfully deleted technology!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private technologyModifiedSuccess(successMsg: string): void {
+ this.technologyForm.reset();
+ this._successBar.showMsg(successMsg);
+ this.loadAvailableTechnologies();
+ }
+
+ private loadAvailableTechnologies(): void {
+ this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe(
+ (result: object) => {
+ this.availableTechnologies = result as Technology[];
+ }
+ );
+ }
+
+ // Deletions
+
+ toggleDeletions(): void {
+ this.showDeletions = !this.showDeletions;
+ }
+
+ submitDelete(): void {
+ this.tryDeleteUser();
+ this.tryDeletePost();
+ this.tryDeleteComment();
+ }
+
+ private tryDeleteUser(): void {
+ const deleteUser: string = this.deleteForm.get('deleteUser')?.value;
+
+ if (deleteUser !== '' && deleteUser !== null) {
+ const userId: Guid = Guid.parse(deleteUser);
+
+ this._userService.deleteUserRequest(userId, this._tokenService.getTokenFromSessionStorage()).subscribe(
+ (result: object) => {
+ this.deletionSuccess('Successfully deleted user!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private tryDeletePost(): void {
+ const deletePost: string = this.deleteForm.get('deletePost')?.value;
+
+ if (deletePost !== '' && deletePost !== null) {
+ const postId: Guid = Guid.parse(deletePost);
+
+ this._postService.deletePostRequest(postId, this._tokenService.getTokenFromSessionStorage()).subscribe(
+ (result: object) => {
+ this.deletionSuccess('Successfully deleted user!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private tryDeleteComment(): void {
+ const deleteComment: string = this.deleteForm.get('deleteComment')?.value;
+
+ if (deleteComment !== '' && deleteComment !== null) {
+ const commentId: Guid = Guid.parse(deleteComment);
+
+ this._commentService.deleteCommentWithSessionStorage(commentId).subscribe(
+ (result: object) => {
+ this.deletionSuccess('Successfully deleted comment!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ }
+
+ private deletionSuccess(successMsg: string): void {
+ this.deleteForm.reset();
+ this._successBar.showMsg(successMsg);
+ }
+}
diff --git a/src/app/components/comment-page/comment-page.component.css b/src/app/components/comment-page/comment-page.component.css
new file mode 100644
index 0000000..b886bc1
--- /dev/null
+++ b/src/app/components/comment-page/comment-page.component.css
@@ -0,0 +1,27 @@
+#content {
+ justify-content: flex-start !important;
+}
+
+#content > * {
+ width: 100%;
+}
+
+.many-buttons {
+ width: 100%;
+ display: flex;
+}
+
+.many-buttons > * {
+ flex: 1;
+ margin-right: .3em;
+}
+
+.many-buttons > *:last-of-type {
+ margin-right: 0;
+}
+
+.submit-btn {
+ max-width: 98%;
+ margin: 0 auto;
+ margin-bottom: .5em;
+}
diff --git a/src/app/components/comment-page/comment-page.component.html b/src/app/components/comment-page/comment-page.component.html
new file mode 100644
index 0000000..2d110d6
--- /dev/null
+++ b/src/app/components/comment-page/comment-page.component.html
@@ -0,0 +1,14 @@
+<app-loading *ngIf="!loaded"></app-loading>
+
+<div id="content" *ngIf="loaded">
+ <button class="submit-btn" type="submit" (click)="toPost()">ᐊ Back to post</button>
+ <app-comment [paramId]="commentId.toString()"></app-comment>
+ <div class="many-buttons" *ngIf="editable">
+ <button class="submit-btn" (click)="editComment()">Edit comment</button>
+ <button class="submit-btn delete-btn" (click)="deleteComment()">Delete comment</button>
+ </div>
+ <form [formGroup]="editCommentFormGroup" (ngSubmit)="editComment()">
+ <input type="text" *ngIf="editingComment" placeholder="New comment message" class="input-field" formControlName="newCommentMessage">
+ <input type="submit" style="display: none" />
+ </form>
+<div>
diff --git a/src/app/components/comment-page/comment-page.component.ts b/src/app/components/comment-page/comment-page.component.ts
new file mode 100644
index 0000000..bd4cfe5
--- /dev/null
+++ b/src/app/components/comment-page/comment-page.component.ts
@@ -0,0 +1,91 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { Title } from '@angular/platform-browser';
+import { Router } from '@angular/router';
+import { Guid } from 'guid-typescript';
+import { CommentService } from 'src/app/services/comment.service';
+import { TokenService } from 'src/app/services/token.service';
+import { Comment } from 'src/models/comment';
+
+@Component({
+ selector: 'app-comment-page',
+ templateUrl: './comment-page.component.html',
+ styleUrls: ['./comment-page.component.css']
+})
+export class CommentPageComponent implements OnInit {
+ private _title = 'Comment';
+ public loaded = false;
+ public loggedIn = false;
+ public editable = false;
+ public editingComment = false;
+ public commentId: Guid;
+ public comment: Comment;
+ public editCommentFormGroup: FormGroup;
+
+ constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _commentService: CommentService){
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== '';
+ this.commentId = Guid.parse(this._router.url.substring(9));
+
+ // Gets the post and the logged in user and compares them,
+ // to determine if the current post is made by the user
+ this._commentService.getCommentRequest(this.commentId).subscribe(
+ (result: object) => {
+ this.comment = result as Comment;
+ if (this.loggedIn) {
+ this.editable = this.comment.issuerUsername === this._tokenService.getUsernameFromSessionStorageToken();
+ }
+ this.loaded = true;
+ },
+ (err: HttpErrorResponse) => {
+ this._router.navigate(['/not-found']);
+ }
+ );
+
+ this.editCommentFormGroup = this._fb.group({
+ newCommentMessage: new FormControl('')
+ });
+ }
+
+ toPost(): void {
+ this._router.navigate(['/post/' + this.comment.postId]);
+ }
+
+ editComment(): void {
+ if (this._tokenService.getTokenFromSessionStorage() === '') {
+ this._router.navigate(['/login']);
+ return;
+ }
+
+ if (this.editingComment) {
+ const newMessage = this.editCommentFormGroup.get('newCommentMessage')?.value;
+ if (newMessage !== '') {
+ console.log(this.commentId);
+ this._commentService.putCommentWithSessionStorageRequest(this.commentId, this.comment.postId, newMessage).subscribe(
+ (result: object) => {
+ this.reloadPage();
+ }
+ );
+ }
+ }
+ this.editingComment = !this.editingComment;
+ }
+
+ deleteComment(): void {
+ this._commentService.deleteCommentWithSessionStorage(this.commentId).subscribe(
+ (result: object) => {
+ this.toPost();
+ }
+ );
+ }
+
+ private reloadPage(): void {
+ this._router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this._router.onSameUrlNavigation = 'reload';
+ this._router.navigate([this._router.url]);
+ }
+}
diff --git a/src/app/components/comment/comment.component.css b/src/app/components/comment/comment.component.css
new file mode 100644
index 0000000..d82f10e
--- /dev/null
+++ b/src/app/components/comment/comment.component.css
@@ -0,0 +1,59 @@
+.comment {
+ display: flex;
+ width: 100%;
+
+ margin: .5em auto;
+ box-sizing: border-box;
+ padding: .5em;
+ background-color: var(--card-bg);
+}
+
+.comment:first-child {
+ margin-top: 0;
+}
+
+/* Author */
+
+.author {
+ display: flex;
+ margin-bottom: .2em;
+}
+
+.author:hover {
+ cursor: pointer;
+}
+
+.author > img {
+ width: 1.7em;
+ height: 1.7em;
+ margin-right: .2em;
+}
+
+.author-info > .name {
+ font-size: .8em;
+}
+
+.author-info > .handle {
+ font-size: .6em;
+ color: gray;
+}
+
+/* Content */
+
+.content {
+ flex: 1;
+}
+
+.message {
+ margin: .3em 0;
+ word-break: break-all;
+}
+
+.timestamp {
+ font-size: .5em;
+ color: gray;
+}
+
+.message:hover, .timestamp:hover {
+ cursor: pointer;
+}
diff --git a/src/app/components/comment/comment.component.html b/src/app/components/comment/comment.component.html
new file mode 100644
index 0000000..718e25c
--- /dev/null
+++ b/src/app/components/comment/comment.component.html
@@ -0,0 +1,23 @@
+<app-loading *ngIf="!loaded"></app-loading>
+
+<div class="comment rounded-border" *ngIf="loaded">
+ <div class="content">
+ <div class="author" (click)="goToAuthorProfile()">
+ <img class="round-image" [src]="user.profilePictureURL">
+ <div class="author-info">
+ <div class="name">
+ {{ user.firstName }} {{ user.lastName }}
+ </div>
+ <div class="handle">
+ @{{ user.userName }}
+ </div>
+ </div>
+ </div>
+ <div class="message" (click)="goToCommentPage()">
+ {{ comment.message }}
+ </div>
+ <div class="timestamp" (click)="goToCommentPage()">
+ {{ timeCreated }}
+ </div>
+ </div>
+</div>
diff --git a/src/app/components/comment/comment.component.ts b/src/app/components/comment/comment.component.ts
new file mode 100644
index 0000000..5076769
--- /dev/null
+++ b/src/app/components/comment/comment.component.ts
@@ -0,0 +1,54 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { Guid } from 'guid-typescript';
+import { CommentService } from 'src/app/services/comment.service';
+import { UserService } from 'src/app/services/user.service';
+import { Comment } from 'src/models/comment';
+import { User } from 'src/models/identity/user';
+
+@Component({
+ selector: 'app-comment',
+ templateUrl: './comment.component.html',
+ styleUrls: ['./comment.component.css']
+})
+export class CommentComponent implements OnInit {
+ public loaded = false;
+ public user: User;
+ public comment: Comment;
+ public timeCreated: string;
+ @Input() paramId: string;
+
+ constructor(private _router: Router, private _commentService: CommentService, private _userService: UserService)
+ { }
+
+ ngOnInit(): void {
+ this.comment = this._commentService.getDefaultComment();
+ this.user = this._userService.getDefaultUser();
+
+ this._commentService.getCommentRequest(Guid.parse(this.paramId)).subscribe(
+ (result: object) => {
+ Object.assign(this.comment, result);
+
+ this.timeCreated = new Date(this.comment.timeCreated).toLocaleString('en-GB');
+ this.loadUser();
+ }
+ );
+ }
+
+ private loadUser(): void {
+ this._userService.getUserByUsernameRequest(this.comment.issuerUsername).subscribe(
+ (result: object) => {
+ Object.assign(this.user, result);
+ this.loaded = true;
+ }
+ );
+ }
+
+ goToAuthorProfile(): void {
+ this._router.navigate(['/profile/' + this.comment.issuerUsername]);
+ }
+
+ goToCommentPage(): void {
+ this._router.navigate(['/comment/' + this.comment.commentId]);
+ }
+}
diff --git a/src/app/components/error-bar/error-bar.component.css b/src/app/components/error-bar/error-bar.component.css
new file mode 100644
index 0000000..8f8edd9
--- /dev/null
+++ b/src/app/components/error-bar/error-bar.component.css
@@ -0,0 +1,12 @@
+#error-bar {
+ box-sizing: border-box;
+ width: 100%;
+ background-color: var(--failure);
+ color: white;
+ padding: .2em;
+ text-align: center;
+}
+
+#error-bar:empty {
+ display: none;
+}
diff --git a/src/app/components/error-bar/error-bar.component.html b/src/app/components/error-bar/error-bar.component.html
new file mode 100644
index 0000000..f1995ab
--- /dev/null
+++ b/src/app/components/error-bar/error-bar.component.html
@@ -0,0 +1 @@
+<div id="error-bar">{{errorMsg}}</div>
diff --git a/src/app/components/error-bar/error-bar.component.ts b/src/app/components/error-bar/error-bar.component.ts
new file mode 100644
index 0000000..111bac8
--- /dev/null
+++ b/src/app/components/error-bar/error-bar.component.ts
@@ -0,0 +1,34 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit } from '@angular/core';
+import { IApiError } from 'src/interfaces/api-error';
+
+@Component({
+ selector: 'app-error-bar',
+ templateUrl: './error-bar.component.html',
+ styleUrls: ['./error-bar.component.css']
+})
+export class ErrorBarComponent implements OnInit {
+ public errorMsg = '';
+
+ constructor()
+ { }
+
+ ngOnInit(): void {
+ this.hideError();
+ }
+
+ showError(error: HttpErrorResponse): void {
+ const test: IApiError = {
+ type: '',
+ title: 'Error!',
+ status: 0,
+ traceId: ''
+ };
+ Object.assign(test, error.error);
+ this.errorMsg = test.title;
+ }
+
+ hideError(): void {
+ this.errorMsg = '';
+ }
+}
diff --git a/src/app/components/feed/feed.component.css b/src/app/components/feed/feed.component.css
new file mode 100644
index 0000000..cb155c6
--- /dev/null
+++ b/src/app/components/feed/feed.component.css
@@ -0,0 +1,179 @@
+#feed-page {
+ height: 100%;
+ max-width: 40em;
+ box-sizing: border-box;
+ border: .5em solid var(--bg-color);
+
+ margin: 0 auto;
+
+ display: flex;
+}
+
+@media screen and (max-width: 750px) {
+ #profile-bar {
+ display: none !important;
+ }
+ #top-bar-profile-pic {
+ display: initial;
+ }
+}
+@media screen and (min-width: 750px) {
+ #profile-bar {
+ display: initial;
+ }
+ #top-bar-profile-pic {
+ display: none !important;
+ }
+}
+
+/* Content */
+
+#feed-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Profile bar */
+
+#profile-bar {
+ width: 20%;
+ height: fit-content;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ box-sizing: border-box;
+ background-color: var(--bg-color);
+
+ margin-right: .5em;
+ padding-bottom: 1em;
+ padding: .5em;
+ border-radius: .6em;
+}
+
+#profile-bar-profile-pic {
+ width: 7em;
+ height: 7em;
+ box-sizing: border-box;
+ padding: .5em;
+}
+
+#profile-bar-name {
+ text-align: center;
+}
+
+#profile-bar-username {
+ margin: .5em 0;
+}
+
+#profile-bar > #profile-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+/* Top bar */
+
+#top-bar {
+ display: flex;
+ flex-direction: column;
+ width: 98%;
+ margin: 0 auto;
+ margin-bottom: .5em;
+ box-sizing: border-box;
+}
+
+#top-bar-profile-pic {
+ height: 2.5em;
+ width: 2.5em;
+ margin-right: .5em;
+}
+
+#top-bar-profile-pic > img {
+ height: inherit;
+ width: inherit;
+}
+
+#top-bar-open-chat {
+ /* Until implemented */
+ display: none;
+}
+
+#main-content {
+ display: flex;
+}
+
+/* Create post */
+
+#create-post-form {
+ width: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+#form-inputs {
+ display: flex;
+}
+
+#top-bar-create-post {
+ flex: 1;
+ font-size: inherit;
+ width: 100%;
+ height: 100%;
+ margin: 0 auto;
+ box-sizing: border-box;
+ border: 2px solid var(--bg-color);
+ border-radius: .6em;
+}
+
+#file-upload {
+ font-size: inherit;
+ color: transparent;
+ width: 1.5em;
+ height: 1.5em;
+}
+
+#file-upload:hover {
+ cursor: pointer;
+}
+
+#file-upload::-webkit-file-upload-button {
+ visibility: hidden;
+}
+
+#attachment-img {
+ height: 1.5em;
+ width: 1.5em;
+ position: absolute;
+ right: .4em;
+ pointer-events: none;
+}
+
+/* Posts */
+
+#no-posts-msg {
+ width: 100%;
+ margin-top: 1em;
+ color: gray;
+ text-align: center;
+}
+
+/* Elements, that act as buttons */
+
+#profile-bar > #profile-info:hover,
+#top-bar-profile-pic:hover {
+ cursor: pointer;
+}
+
+/* Can't copy text from */
+
+#profile-bar,
+.vote {
+ -webkit-user-select: none; /* Safari */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* IE10+/Edge */
+ user-select: none; /* Standard */
+}
diff --git a/src/app/components/feed/feed.component.html b/src/app/components/feed/feed.component.html
new file mode 100644
index 0000000..1a03dcc
--- /dev/null
+++ b/src/app/components/feed/feed.component.html
@@ -0,0 +1,52 @@
+<app-loading *ngIf="!dataArrived"></app-loading>
+
+<div id="feed-page" *ngIf="dataArrived">
+ <nav id="profile-bar" class="round-image rounded-border">
+ <div id="profile-info" (click)="goToProfile()">
+ <img id="profile-bar-profile-pic" class="round-image" [src]="user.profilePictureURL" alt=""/>
+ <div id="profile-bar-name">
+ {{ user.firstName }} {{ user.lastName }}
+ </div>
+ <div id="profile-bar-username">
+ @{{ user.userName }}
+ </div>
+ </div>
+ <button class="submit-btn" (click)="goToSettings()">Settings</button>
+ <button class="submit-btn" (click)="logout()">Logout</button>
+ </nav>
+ <div id="feed-content">
+ <nav id="top-bar">
+ <div id="main-content">
+ <img id="top-bar-profile-pic" class="round-image" [src]="user.profilePictureURL" alt="" (click)="goToProfile()">
+ <form id="create-post-form" class="rounded-border" [formGroup]="createPostFormGroup" (ngSubmit)="createPost()">
+ <div id="form-inputs">
+ <input id="top-bar-create-post" type="text" formControlName="newPostMessage" placeholder="What's on your mind?"/>
+ <input type="submit" style="display: none" /> <!-- You need this element, so when you press enter the request is sent -->
+ <img id="attachment-img" src="assets/images/paper-clip.png">
+ <input id="file-upload" type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple>
+ </div>
+ <div class="form-attachments">
+ <div *ngFor="let file of files" class="form-attachment">
+ {{ file.name ? file.name : 'Attachment' }}
+ <div class="remove-form-attachment" (click)="removeAttachment(file.name)">
+ ☒
+ </div>
+ </div>
+ </div>
+ </form>
+ <a id="top-bar-open-chat" href="">
+ <img src="assets/images/feed/chat-pic.png" alt=""/>
+ </a>
+ </div>
+ </nav>
+ <div id="posts" class="scroll-standalone" (scroll)="onScroll($event)">
+ <div id="no-posts-msg" *ngIf="posts.length === 0">
+ None of your friends have posted anything yet!<br>
+ Try refreshing your page!
+ </div>
+ <div *ngFor="let friendPost of posts" class="post">
+ <app-post [paramId]="friendPost.postId.toString()"></app-post>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/app/components/feed/feed.component.ts b/src/app/components/feed/feed.component.ts
new file mode 100644
index 0000000..b412b3c
--- /dev/null
+++ b/src/app/components/feed/feed.component.ts
@@ -0,0 +1,122 @@
+import { Component, OnInit } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { Router } from '@angular/router';
+import { User } from 'src/models/identity/user';
+import { UserService } from '../../services/user.service';
+import { AppConstants } from 'src/app/app-constants.module';
+import { HttpErrorResponse } from '@angular/common/http';
+import { FeedService } from 'src/app/services/feed.service';
+import { Post } from 'src/models/post';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { PostService } from 'src/app/services/post.service';
+import { TokenService } from 'src/app/services/token.service';
+
+@Component({
+ selector: 'app-feed',
+ templateUrl: './feed.component.html',
+ styleUrls: ['./feed.component.css']
+})
+export class FeedComponent implements OnInit {
+ private _title = 'Feed';
+ private _timeLoaded: string; // we send the time to the api as a string
+ private _currentPage: number;
+ public dataArrived = false;
+ public user: User;
+ public posts: Post[];
+ public createPostFormGroup: FormGroup;
+ public files: File[];
+
+ constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _feedService: FeedService, private _postService: PostService, private _tokenService: TokenService) {
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ if (!this._tokenService.getTokenFromSessionStorage()) {
+ this._router.navigate(['/login']);
+ return;
+ }
+
+ this._currentPage = 1;
+ this.posts = [];
+ this.user = this._userService.getDefaultUser();
+ this.files = [];
+
+ const now = new Date();
+ now.setHours(now.getHours() + 2); // accounting for eastern european timezone
+ this._timeLoaded = now.toISOString();
+
+ this.createPostFormGroup = this._fb.group({
+ newPostMessage: new FormControl(''),
+ fileUpload: new FormControl('')
+ });
+
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (res: object) => {
+ Object.assign(this.user, res);
+ this.loadFeed();
+ },
+ (err: HttpErrorResponse) => {
+ this.logout();
+ }
+ );
+ }
+
+ private loadFeed(): void {
+ this._feedService.getUserFeedFromSessionStorageRequest(this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe(
+ (result: object) => {
+ this.posts.push(...Object.values(result)[0]);
+ this.finishUserLoading();
+ },
+ (err) => {
+ this.finishUserLoading();
+ }
+ );
+ }
+
+ private finishUserLoading(): void {
+ this.dataArrived = true;
+ }
+
+ goToProfile(): void {
+ this._router.navigate(['/profile/' + this.user.userName]);
+ }
+
+ goToSettings(): void {
+ this._router.navigate(['/profile/' + this.user.userName + '/settings']);
+ }
+
+ logout(): void {
+ this._tokenService.logoutUserFromSessionStorage();
+ this._router.navigate(['/login']);
+ }
+
+ onFileUpload(event: any): void {
+ this.files.push(...event.target.files);
+ this.createPostFormGroup.get('fileUpload')?.reset();
+ }
+
+ removeAttachment(fileName: string): void {
+ this.files = this.files.filter(x => x.name !== fileName);
+ }
+
+ createPost(): void {
+ const postMessage = this.createPostFormGroup.get('newPostMessage')?.value;
+ this.dataArrived = false;
+
+ this._postService.createPostWithSessionStorageRequest(postMessage, this.files).subscribe(
+ (result: object) => {
+ this.goToProfile();
+ },
+ (err: HttpErrorResponse) => {
+ this.dataArrived = true;
+ }
+ );
+ }
+
+ onScroll(event: any): void {
+ // Detects when the element has reached the bottom, thx https://stackoverflow.com/a/50038429/12036073
+ if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight && this._currentPage > 0) {
+ this.loadFeed();
+ }
+ }
+}
diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.css b/src/app/components/kaleidoscope/kaleidoscope.component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/components/kaleidoscope/kaleidoscope.component.css
diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.html b/src/app/components/kaleidoscope/kaleidoscope.component.html
new file mode 100644
index 0000000..7392a21
--- /dev/null
+++ b/src/app/components/kaleidoscope/kaleidoscope.component.html
@@ -0,0 +1,8 @@
+<div class="kaleidoscope">
+ <ng-container *ngComponentOutlet="component"></ng-container>
+ <app-login *ngIf="logged!"></app-login>
+ <app-register *ngIf="!logged!"></app-register>
+ <!-- <app-feed></app-feed> -->
+ <script>var logged = false;</script>
+ <!-- to be fixed tomorow -->
+</div> \ No newline at end of file
diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.ts b/src/app/components/kaleidoscope/kaleidoscope.component.ts
new file mode 100644
index 0000000..1c5cda1
--- /dev/null
+++ b/src/app/components/kaleidoscope/kaleidoscope.component.ts
@@ -0,0 +1,24 @@
+import { Component, OnInit } from '@angular/core';
+import { LoginComponent } from '../login/login.component';
+
+@Component({
+ selector: 'app-kaleidoscope',
+ templateUrl: './kaleidoscope.component.html',
+ styleUrls: ['./kaleidoscope.component.css']
+})
+export class KaleidoscopeComponent implements OnInit {
+
+ public _component: Component;
+
+ constructor(loginComponent: LoginComponent) {
+ this._component = loginComponent as Component;
+ }
+
+ ngOnInit(): void {
+
+ }
+
+ assignComponent(component: Component) {
+ this._component = component;
+ }
+}
diff --git a/src/app/components/loading/loading.component.css b/src/app/components/loading/loading.component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/components/loading/loading.component.css
diff --git a/src/app/components/loading/loading.component.html b/src/app/components/loading/loading.component.html
new file mode 100644
index 0000000..8440f4e
--- /dev/null
+++ b/src/app/components/loading/loading.component.html
@@ -0,0 +1,3 @@
+<div id="content">
+ Loading...
+</div>
diff --git a/src/app/components/loading/loading.component.ts b/src/app/components/loading/loading.component.ts
new file mode 100644
index 0000000..e203484
--- /dev/null
+++ b/src/app/components/loading/loading.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-loading',
+ templateUrl: './loading.component.html',
+ styleUrls: ['./loading.component.css']
+})
+export class LoadingComponent implements OnInit {
+
+ constructor()
+ { }
+
+ ngOnInit(): void {
+ }
+}
diff --git a/src/app/components/login/login.component.css b/src/app/components/login/login.component.css
new file mode 100644
index 0000000..766522e
--- /dev/null
+++ b/src/app/components/login/login.component.css
@@ -0,0 +1,32 @@
+* {
+ transition: .2s;
+}
+
+form {
+ width: 100%;
+}
+
+#content hr {
+ width: 100%;
+ border: 1px solid black;
+ box-sizing: border-box;
+}
+
+.input-selection:nth-of-type(1) {
+ margin-top: 1.2em;
+}
+
+.submit-btn {
+ margin-bottom: .2em;
+}
+
+.redirect-to-register {
+ color: var(--focus-color);
+ background-color: var(--bg-color);
+ border-color: var(--focus-color);
+}
+
+.redirect-to-register:hover {
+ border-color: black !important;
+ color: black;
+}
diff --git a/src/app/components/login/login.component.html b/src/app/components/login/login.component.html
new file mode 100644
index 0000000..13f9bbf
--- /dev/null
+++ b/src/app/components/login/login.component.html
@@ -0,0 +1,30 @@
+<div id="content">
+ <div class="title">Login</div>
+
+ <form [formGroup]="loginUserFormGroup" (ngSubmit)="onSubmit()">
+ <hr>
+
+ <div class="input-selection">
+ <input type="text" placeholder="Username" class="input-field" formControlName="username" required>
+ <label class="input-field-label">Username</label>
+
+ <div class="input-errors">
+ <label *ngIf="loginUserFormGroup.get('username')?.errors?.required" class="error">*Required</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="password" placeholder="Password" class="input-field" formControlName="password" required>
+ <label class="input-field-label">Password</label>
+
+ <div class="input-errors">
+ <label *ngIf="loginUserFormGroup.get('password')?.errors?.required" class="error">*Required</label>
+ </div>
+ </div>
+
+ <hr>
+ <button class="submit-btn" type="submit">Submit</button>
+ <app-error-bar></app-error-bar>
+ </form>
+ <button class="submit-btn redirect-to-register" (click)="onRedirectRegister()">New to DevHive? Register instead</button>
+</div>
diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts
new file mode 100644
index 0000000..c3fb79c
--- /dev/null
+++ b/src/app/components/login/login.component.ts
@@ -0,0 +1,59 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { FormGroup, FormBuilder, Validators, FormControl, AbstractControl } from '@angular/forms';
+import { Router } from '@angular/router';
+import { Title } from '@angular/platform-browser';
+import { UserService } from 'src/app/services/user.service';
+import { HttpErrorResponse } from '@angular/common/http';
+import { ErrorBarComponent } from '../error-bar/error-bar.component';
+import { TokenService } from 'src/app/services/token.service';
+
+@Component({
+ selector: 'app-login',
+ templateUrl: './login.component.html',
+ styleUrls: ['./login.component.css']
+})
+export class LoginComponent implements OnInit {
+ @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent;
+ private _title = 'Login';
+ public loginUserFormGroup: FormGroup;
+
+ constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) {
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ this.loginUserFormGroup = this._fb.group({
+ username: new FormControl('', [
+ Validators.required
+ ]),
+ password: new FormControl('', [
+ Validators.required
+ ])
+ });
+ }
+
+ onSubmit(): void {
+ this._errorBar.hideError();
+ this._userService.loginUserRequest(this.loginUserFormGroup).subscribe(
+ (res: object) => {
+ this._tokenService.setUserTokenToSessionStorage(res);
+ this._router.navigate(['/']);
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+
+ onRedirectRegister(): void {
+ this._router.navigate(['/register']);
+ }
+
+ get username(): AbstractControl | null {
+ return this.loginUserFormGroup.get('username');
+ }
+
+ get password(): AbstractControl | null {
+ return this.loginUserFormGroup.get('password');
+ }
+}
diff --git a/src/app/components/not-found/not-found.component.css b/src/app/components/not-found/not-found.component.css
new file mode 100644
index 0000000..ef6231d
--- /dev/null
+++ b/src/app/components/not-found/not-found.component.css
@@ -0,0 +1,23 @@
+#content {
+ font-size: 1.3em;
+}
+
+#content hr {
+ width: 100%;
+ border: 1px solid black;
+ box-sizing: border-box;
+}
+
+#back-btns {
+ width: 100%;
+ display: flex;
+}
+
+#back-btns > * {
+ flex: 1;
+ margin-right: .2em;
+}
+
+#back-btns > *:nth-last-child(1) {
+ margin-right: 0;
+}
diff --git a/src/app/components/not-found/not-found.component.html b/src/app/components/not-found/not-found.component.html
new file mode 100644
index 0000000..8394810
--- /dev/null
+++ b/src/app/components/not-found/not-found.component.html
@@ -0,0 +1,10 @@
+<div id="content">
+ <div class="title">
+ Page not found!
+ </div>
+ <hr>
+ <div id="back-btns">
+ <button class="submit-btn" type="submit" (click)="backToFeed()">Back to feed</button>
+ <button class="submit-btn" type="submit" (click)="backToLogin()">Back to login</button>
+ </div>
+</div>
diff --git a/src/app/components/not-found/not-found.component.ts b/src/app/components/not-found/not-found.component.ts
new file mode 100644
index 0000000..b1f8cc1
--- /dev/null
+++ b/src/app/components/not-found/not-found.component.ts
@@ -0,0 +1,27 @@
+import { Component, OnInit } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'app-not-found',
+ templateUrl: './not-found.component.html',
+ styleUrls: ['./not-found.component.css']
+})
+export class NotFoundComponent implements OnInit {
+ private _title = 'Not Found!';
+
+ constructor(private _titleService: Title, private _router: Router) {
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ }
+
+ backToFeed(): void {
+ this._router.navigate(['/']);
+ }
+
+ backToLogin(): void {
+ this._router.navigate(['/login']);
+ }
+}
diff --git a/src/app/components/post-attachment/post-attachment.component.css b/src/app/components/post-attachment/post-attachment.component.css
new file mode 100644
index 0000000..572cc99
--- /dev/null
+++ b/src/app/components/post-attachment/post-attachment.component.css
@@ -0,0 +1,75 @@
+/* Attachment */
+
+.attachment {
+ border: 2px solid black;
+ border-top: 0;
+ border-radius: 0 0 .6em .6em;
+ padding: .4em;
+ padding-top: 1em;
+ margin-top: calc(-0.8em - 2px);
+}
+
+.clickable {
+ width: 100%;
+ height: 100%;
+ display: flex;
+}
+
+.clickable:hover {
+ cursor: pointer;
+}
+
+.attachment-name {
+ flex: 1;
+}
+
+.attachment-type {
+ margin-left: .2em;
+}
+
+/* Full attachment */
+
+.show-full-attachment {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000000AF;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 2;
+}
+
+.show-full-attachment > * {
+ max-height: 100vh;
+ max-width: 100vw;
+}
+
+.attachment-download {
+ max-width: 420px !important;
+ margin: 0 .4em;
+ text-decoration: none;
+ color: white;
+ border-color: white;
+ background-color: black;
+}
+
+.attachment-download:hover {
+ background-color: white;
+ color: var(--focus-color);
+}
+
+.close {
+ position: fixed;
+ top: .2em;
+ right: .2em;
+ font-size: 2em;
+ color: whitesmoke;
+}
+
+.close:hover {
+ color: var(--failure);
+ cursor: pointer;
+}
diff --git a/src/app/components/post-attachment/post-attachment.component.html b/src/app/components/post-attachment/post-attachment.component.html
new file mode 100644
index 0000000..4d381d1
--- /dev/null
+++ b/src/app/components/post-attachment/post-attachment.component.html
@@ -0,0 +1,18 @@
+<div class="attachment">
+ <div class="clickable" (click)="toggleShowFull()">
+ <div class="attachment-name">
+ {{ fileName }}
+ </div>
+ <div class="attachment-type">
+ {{ fileType }}
+ </div>
+ </div>
+</div>
+
+<div class="show-full-attachment" *ngIf="showFull" (click)="toggleShowFull()">
+ <img class="attachment-img" *ngIf="isImage" src="{{paramURL}}">
+ <a class="attachment-download submit-btn" *ngIf="!isImage" href="{{paramURL}}">Download attachment</a>
+ <div class="close">
+ ☒
+ </div>
+</div>
diff --git a/src/app/components/post-attachment/post-attachment.component.ts b/src/app/components/post-attachment/post-attachment.component.ts
new file mode 100644
index 0000000..1d00def
--- /dev/null
+++ b/src/app/components/post-attachment/post-attachment.component.ts
@@ -0,0 +1,27 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-post-attachment',
+ templateUrl: './post-attachment.component.html',
+ styleUrls: ['./post-attachment.component.css']
+})
+export class PostAttachmentComponent implements OnInit {
+ @Input() paramURL: string;
+ public isImage = false;
+ public showFull = false;
+ public fileName: string;
+ public fileType: string;
+
+ constructor()
+ { }
+
+ ngOnInit(): void {
+ this.isImage = this.paramURL.includes('image') && !this.paramURL.endsWith('pdf');
+ this.fileType = this.isImage ? 'img' : 'raw';
+ this.fileName = this.paramURL.match('(?<=\/)(?:.(?!\/))+$')?.pop() ?? 'Attachment';
+ }
+
+ toggleShowFull(): void {
+ this.showFull = !this.showFull;
+ }
+}
diff --git a/src/app/components/post-page/post-page.component.css b/src/app/components/post-page/post-page.component.css
new file mode 100644
index 0000000..3eec851
--- /dev/null
+++ b/src/app/components/post-page/post-page.component.css
@@ -0,0 +1,62 @@
+#content {
+ justify-content: flex-start !important;
+}
+
+#content > * {
+ width: 100%;
+}
+
+.many-buttons {
+ width: 100%;
+ display: flex;
+}
+
+.many-buttons > * {
+ flex: 1;
+ margin-right: .3em;
+}
+
+.many-buttons > *:last-of-type {
+ margin-right: 0;
+}
+
+#editPost {
+ display: flex;
+ position: relative;
+}
+
+#new-message-input {
+ flex: 1;
+ box-sizing: border-box;
+}
+
+#file-upload {
+ font-size: inherit;
+ color: transparent;
+ width: 1.99em;
+ height: 1.99em;
+ margin-left: .3em;
+}
+
+#file-upload:hover {
+ cursor: pointer;
+}
+
+#file-upload::-webkit-file-upload-button {
+ visibility: hidden;
+}
+
+#attachment-img {
+ height: 1.99em;
+ width: 1.99em;
+ position: absolute;
+ right: 0;
+ pointer-events: none;
+}
+
+
+.submit-btn {
+ max-width: 98%;
+ margin: 0 auto;
+ margin-bottom: .5em;
+}
diff --git a/src/app/components/post-page/post-page.component.html b/src/app/components/post-page/post-page.component.html
new file mode 100644
index 0000000..8665865
--- /dev/null
+++ b/src/app/components/post-page/post-page.component.html
@@ -0,0 +1,37 @@
+<app-loading *ngIf="!dataArrived"></app-loading>
+
+<div id="content" *ngIf="dataArrived">
+ <div class="many-buttons" *ngIf="loggedIn">
+ <button class="submit-btn" type="submit" (click)="backToFeed()">ᐊ Back to feed</button>
+ <button class="submit-btn" type="submit" (click)="backToProfile()">ᐊ Back to profile</button>
+ </div>
+ <button class="submit-btn" type="submit" (click)="toLogin()" *ngIf="!loggedIn">Login</button>
+ <app-post [paramId]="postId.toString()"></app-post>
+ <div class="many-buttons" *ngIf="editable">
+ <button class="submit-btn" (click)="editPost()">Edit post</button>
+ <button class="submit-btn delete-btn" (click)="deletePost()">Delete post</button>
+ </div>
+ <form id="editPost" [formGroup]="editPostFormGroup" *ngIf="editingPost" (ngSubmit)="editPost()">
+ <input id="new-message-input" type="text" placeholder="New post message" class="input-field" formControlName="newPostMessage">
+ <img id="attachment-img" src="assets/images/paper-clip.png">
+ <input id="file-upload" type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple>
+ <input type="submit" style="display: none" />
+ </form>
+ <div class="form-attachments" *ngIf="editingPost">
+ <div *ngFor="let file of files" class="form-attachment">
+ {{ file.name ? file.name : 'Attachment' }}
+ <div class="remove-form-attachment" (click)="removeAttachment(file.name)">
+ ☒
+ </div>
+ </div>
+ </div>
+ <form [formGroup]="addCommentFormGroup" (ngSubmit)="addComment()">
+ <input type="text" placeholder="Add comment" class="input-field" formControlName="newComment">
+ <input type="submit" style="display: none" />
+ </form>
+ <div>
+ <div class="comment" *ngFor="let comm of post?.comments">
+ <app-comment [paramId]="comm.id.toString()"></app-comment>
+ </div>
+ </div>
+<div>
diff --git a/src/app/components/post-page/post-page.component.ts b/src/app/components/post-page/post-page.component.ts
new file mode 100644
index 0000000..413ff80
--- /dev/null
+++ b/src/app/components/post-page/post-page.component.ts
@@ -0,0 +1,162 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { Title } from '@angular/platform-browser';
+import { Router } from '@angular/router';
+import { Guid } from 'guid-typescript';
+import { CommentService } from 'src/app/services/comment.service';
+import { PostService } from 'src/app/services/post.service';
+import { TokenService } from 'src/app/services/token.service';
+import { Post } from 'src/models/post';
+import { CloudinaryService } from 'src/app/services/cloudinary.service';
+
+@Component({
+ selector: 'app-post-page',
+ templateUrl: './post-page.component.html',
+ styleUrls: ['./post-page.component.css']
+})
+export class PostPageComponent implements OnInit {
+ private _title = 'Post';
+ public dataArrived = false;
+ public loggedIn = false;
+ public editable = false;
+ public editingPost = false;
+ public postId: Guid;
+ public post: Post;
+ public files: File[];
+ public editPostFormGroup: FormGroup;
+ public addCommentFormGroup: FormGroup;
+
+ constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService, private _cloudinaryService: CloudinaryService){
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== '';
+ this.postId = Guid.parse(this._router.url.substring(6));
+ this.files = [];
+
+ // Gets the post and the logged in user and compares them,
+ // to determine if the current post is made by the user
+ this._postService.getPostRequest(this.postId).subscribe(
+ (result: object) => {
+ this.post = result as Post;
+ this.post.fileURLs = Object.values(result)[7];
+ if (this.loggedIn) {
+ this.editable = this.post.creatorUsername === this._tokenService.getUsernameFromSessionStorageToken();
+ }
+ if (this.post.fileURLs.length > 0) {
+ this.loadFiles();
+ }
+ else {
+ this.dataArrived = true;
+ }
+ },
+ (err: HttpErrorResponse) => {
+ this._router.navigate(['/not-found']);
+ }
+ );
+
+ this.editPostFormGroup = this._fb.group({
+ newPostMessage: new FormControl(''),
+ fileUpload: new FormControl('')
+ });
+
+ this.addCommentFormGroup = this._fb.group({
+ newComment: new FormControl('')
+ });
+ }
+
+ private loadFiles(): void {
+ for (const fileURL of this.post.fileURLs) {
+ this._cloudinaryService.getFileRequest(fileURL).subscribe(
+ (result: object) => {
+ const file = result as File;
+ const tmp = {
+ name: fileURL.match('(?<=\/)(?:.(?!\/))+$')?.pop() ?? 'Attachment'
+ };
+
+ Object.assign(file, tmp);
+ this.files.push(file);
+
+ if (this.files.length === this.post.fileURLs.length) {
+ this.dataArrived = true;
+ }
+ }
+ );
+ }
+ }
+
+ backToFeed(): void {
+ this._router.navigate(['/']);
+ }
+
+ backToProfile(): void {
+ this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]);
+ }
+
+ toLogin(): void {
+ this._router.navigate(['/login']);
+ }
+
+ onFileUpload(event: any): void {
+ this.files.push(...event.target.files);
+ this.editPostFormGroup.get('fileUpload')?.reset();
+ }
+
+ removeAttachment(fileName: string): void {
+ this.files = this.files.filter(x => x.name !== fileName);
+ }
+
+ editPost(): void {
+ if (this._tokenService.getTokenFromSessionStorage() === '') {
+ this.toLogin();
+ return;
+ }
+
+ if (this.editingPost) {
+ let newMessage = this.editPostFormGroup.get('newPostMessage')?.value;
+ if (newMessage === '') {
+ newMessage = this.post.message;
+ }
+ this._postService.putPostWithSessionStorageRequest(this.postId, newMessage, this.files).subscribe(
+ (result: object) => {
+ this.reloadPage();
+ }
+ );
+ this.dataArrived = false;
+ }
+ this.editingPost = !this.editingPost;
+ }
+
+ addComment(): void {
+ if (!this.loggedIn) {
+ this._router.navigate(['/login']);
+ return;
+ }
+
+ const newComment = this.addCommentFormGroup.get('newComment')?.value;
+ if (newComment !== '' && newComment !== null) {
+ this._commentService.createCommentWithSessionStorageRequest(this.postId, newComment).subscribe(
+ (result: object) => {
+ this.editPostFormGroup.reset();
+ this.reloadPage();
+ }
+ );
+ }
+ }
+
+ deletePost(): void {
+ this._postService.deletePostWithSessionStorage(this.postId).subscribe(
+ (result: object) => {
+ this._router.navigate(['/profile/' + this._tokenService.getUsernameFromSessionStorageToken()]);
+ }
+ );
+ }
+
+ private reloadPage(): void {
+ this._router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this._router.onSameUrlNavigation = 'reload';
+ this._router.navigate([this._router.url]);
+ }
+}
diff --git a/src/app/components/post/post.component.css b/src/app/components/post/post.component.css
new file mode 100644
index 0000000..1b88c7d
--- /dev/null
+++ b/src/app/components/post/post.component.css
@@ -0,0 +1,134 @@
+.post {
+ display: flex;
+ width: 98%;
+
+ margin: .5em auto;
+ box-sizing: border-box;
+ padding: .5em;
+ background-color: var(--card-bg);
+ position: relative;
+}
+
+.post:first-child {
+ margin-top: 0;
+}
+
+hr {
+ border: 1px solid black;
+ width: 90%;
+}
+
+/* Author */
+
+.author {
+ display: flex;
+ margin-bottom: .2em;
+}
+
+.author:hover {
+ cursor: pointer;
+}
+
+.author > img {
+ width: 2.2em;
+ height: 2.2em;
+ margin-right: .2em;
+}
+
+.author-info > .handle {
+ font-size: .9em;
+ color: gray;
+}
+
+/* Content */
+
+.content {
+ flex: 1;
+}
+
+.message {
+ margin: .3em 0;
+ word-break: break-all;
+}
+
+.bottom-post {
+ font-size: .5em;
+ color: gray;
+ display: flex;
+ align-items: center;
+}
+
+.separator {
+ margin: 0 .5em;
+}
+
+.comment-count {
+ font-size: 1em;
+}
+
+.comment-count > img {
+ height: .8em;
+}
+
+.message:hover, .timestamp:hover {
+ cursor: pointer;
+}
+
+/* Rating */
+
+/* Temporary, until ratings are implemented fully */
+.rating {
+ display: none !important;
+}
+
+.rating {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-height: 4.4em;
+ margin: auto -.1em auto 0;
+}
+
+.score {
+ flex: 1;
+ display: flex;
+ align-items: center;
+}
+
+
+.vote {
+ display: flex;
+ align-items: center;
+ flex: 1;
+
+ background: var(--card-bg);
+ font-size: 1em;
+
+ border: 1px solid var(--card-bg);
+ box-sizing: border-box;
+ border-radius: .2em;
+
+ }
+
+.vote:hover {
+ border: 1px solid var(--focus-color);
+ color: var(--focus-color);
+ cursor: pointer;
+}
+
+/* Attachments */
+
+.attachments {
+ display: flex;
+ width: 98%;
+ margin: -.3em auto .5em auto;
+ flex-wrap: wrap;
+}
+
+.attachments:empty {
+ display: none;
+}
+
+.attachments > * {
+ flex: 1;
+}
diff --git a/src/app/components/post/post.component.html b/src/app/components/post/post.component.html
new file mode 100644
index 0000000..bc0d84a
--- /dev/null
+++ b/src/app/components/post/post.component.html
@@ -0,0 +1,49 @@
+<app-loading *ngIf="!loaded"></app-loading>
+
+<div class="post rounded-border" *ngIf="loaded">
+ <div class="content">
+ <div class="author" (click)="goToAuthorProfile()">
+ <img class="round-image" [src]="user.profilePictureURL">
+ <div class="author-info">
+ <div class="name">
+ {{ user.firstName }} {{ user.lastName }}
+ </div>
+ <div class="handle">
+ @{{ user.userName }}
+ </div>
+ </div>
+ </div>
+ <div class="message" (click)="goToPostPage()">
+ {{ post.message }}
+ </div>
+ <div class="bottom-post" (click)="goToPostPage()">
+ <div class="timestamp">
+ {{ timeCreated }}
+ </div>
+ <div class="separator">
+ ║
+ </div>
+ <div class="comment-count">
+ {{ post.comments.length }}
+ <img src="assets/images/comment.png">
+ </div>
+ </div>
+
+ </div>
+ <div class="rating">
+ <button class="vote">
+ ᐃ
+ </button>
+ <div class="score">
+ {{ votesNumber }}
+ </div>
+ <button class="vote">
+ ᐁ
+ </button>
+ </div>
+</div>
+<div class="attachments">
+ <div *ngFor="let fileURL of post.fileURLs">
+ <app-post-attachment class="no-events" [paramURL]="fileURL"></app-post-attachment>
+ </div>
+</div>
diff --git a/src/app/components/post/post.component.ts b/src/app/components/post/post.component.ts
new file mode 100644
index 0000000..387f56f
--- /dev/null
+++ b/src/app/components/post/post.component.ts
@@ -0,0 +1,57 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { Guid } from 'guid-typescript';
+import { PostService } from 'src/app/services/post.service';
+import { UserService } from 'src/app/services/user.service';
+import { User } from 'src/models/identity/user';
+import { Post } from 'src/models/post';
+
+@Component({
+ selector: 'app-post',
+ templateUrl: './post.component.html',
+ styleUrls: ['./post.component.css'],
+})
+export class PostComponent implements OnInit {
+ public loaded = false;
+ public user: User;
+ public post: Post;
+ public votesNumber: number;
+ public timeCreated: string;
+ @Input() paramId: string;
+
+ constructor(private _postService: PostService, private _userService: UserService, private _router: Router)
+ { }
+
+ ngOnInit(): void {
+ this.post = this._postService.getDefaultPost();
+ this.user = this._userService.getDefaultUser();
+
+ this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe(
+ (result: object) => {
+ Object.assign(this.post, result);
+ this.post.fileURLs = Object.values(result)[7];
+ this.votesNumber = 23;
+
+ this.timeCreated = new Date(this.post.timeCreated).toLocaleString('en-GB');
+ this.loadUser();
+ }
+ );
+ }
+
+ private loadUser(): void {
+ this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe(
+ (result: object) => {
+ Object.assign(this.user, result);
+ this.loaded = true;
+ }
+ );
+ }
+
+ goToAuthorProfile(): void {
+ this._router.navigate(['/profile/' + this.user.userName]);
+ }
+
+ goToPostPage(): void {
+ this._router.navigate(['/post/' + this.post.postId]);
+ }
+}
diff --git a/src/app/components/profile-settings/profile-settings.component.css b/src/app/components/profile-settings/profile-settings.component.css
new file mode 100644
index 0000000..1c07d9f
--- /dev/null
+++ b/src/app/components/profile-settings/profile-settings.component.css
@@ -0,0 +1,124 @@
+* {
+ box-sizing: border-box;
+}
+
+#content {
+ max-width: 22em;
+ justify-content: start;
+}
+
+form {
+ width: 100%;
+}
+
+hr {
+ width: calc(100% - 1em);
+ color: black;
+ border: 1px solid black;
+}
+
+/* Navigation bar (for loggedin user) */
+
+#navigation {
+ display: flex;
+ width: 100%;
+}
+
+#navigation > .submit-btn {
+ flex: 1;
+ margin-top: 0;
+ margin-left: .5em;
+ font-size: inherit;
+}
+
+#navigation > .submit-btn:nth-of-type(1) {
+ margin-left: 0;
+}
+
+/* Form */
+
+#update-profile-picture {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 .5em;
+ flex-wrap: wrap;
+}
+
+#profile-picture {
+ width: 5em;
+ height: 5em;
+}
+
+#submit-file {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1em;
+}
+
+#upload-file:hover {
+ cursor: inherit;
+}
+
+#upload-file > input:hover {
+ cursor: pointer;
+}
+
+#upload-file > input::-webkit-file-upload-button {
+ visibility: hidden;
+}
+
+#update-user {
+ margin-top: 1.1em;
+}
+
+.input-field {
+ border-color: var(--focus-color) !important;
+ caret-color: var(--focus-color);
+ border-width: 2px !important;
+ margin-top: -1px !important;
+}
+
+.input-field-label {
+ font-size: .7em;
+ color: var(--focus-color);
+ transform: translate(0, -1.2em);
+}
+
+#all-languages, #all-technologies {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+/* Buttons */
+
+.edit-btn {
+ border-radius: 0 !important;
+ color: var(--focus-color);
+ background-color: white;
+ border-color: var(--focus-color);
+}
+
+.edit-btn:hover {
+ color: white;
+ background-color: black;
+ border-color: black !important;
+}
+
+.submit-btn {
+ margin-bottom: .5em;
+}
+
+#update-profile-btn {
+ margin-top: 1em;
+}
+
+#confirm-delete {
+ box-sizing: border-box;
+ width: 100%;
+ background-color: var(--failure);
+ color: white;
+ padding: .2em;
+ text-align: center;
+}
diff --git a/src/app/components/profile-settings/profile-settings.component.html b/src/app/components/profile-settings/profile-settings.component.html
new file mode 100644
index 0000000..502697d
--- /dev/null
+++ b/src/app/components/profile-settings/profile-settings.component.html
@@ -0,0 +1,116 @@
+<app-loading *ngIf="!dataArrived"></app-loading>
+
+<div id="content" *ngIf="dataArrived">
+ <nav id="navigation">
+ <button class="submit-btn" (click)="goToProfile()">ᐊ Back</button>
+ <button class="submit-btn" (click)="navigateToAdminPanel()" *ngIf="isAdminUser">Panel</button>
+ <button class="submit-btn" (click)="logout()">Logout</button>
+ </nav>
+ <hr>
+ <div class="scroll-standalone">
+ <form id="update-profile-picture" [formGroup]="updateProfilePictureFormGroup" (ngSubmit)="updateProfilePicture()">
+ <img id="profile-picture" class="round-image" [src]="user.profilePictureURL">
+ <div id="submit-file">
+ <div id="upload-file" class="submit-btn">
+ <input type="file" accept="image/*" formControlName="fileUpload" (change)="onFileUpload($event)">
+ </div>
+ <button class="submit-btn" type="submit">Update profile picture</button>
+ </div>
+ </form>
+ <hr>
+ <form id="update-user" [formGroup]="updateUserFormGroup" (ngSubmit)="onSubmit()">
+ <div class="input-selection">
+ <input type="text" class="input-field" formControlName="firstName" required>
+ <label class="input-field-label">First Name</label>
+
+ <div class="input-errors">
+ <label *ngIf="updateUserFormGroup.get('firstName')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="updateUserFormGroup.get('firstName')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="text" class="input-field" formControlName="lastName" required>
+ <label class="input-field-label">Last Name</label>
+
+ <div class="input-errors">
+ <label *ngIf="updateUserFormGroup.get('lastName')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="updateUserFormGroup.get('lastName')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="text" class="input-field" formControlName="username" required>
+ <label class="input-field-label">Username</label>
+
+ <div class="input-errors">
+ <label *ngIf="updateUserFormGroup.get('username')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="updateUserFormGroup.get('username')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="text" class="input-field" formControlName="email" required>
+ <label class="input-field-label">Email</label>
+
+ <div class="input-errors">
+ <label *ngIf="updateUserFormGroup.get('email')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="updateUserFormGroup.get('email')?.errors?.email" class="error">*Invalid email</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="password" class="input-field" formControlName="password" required>
+ <label class="input-field-label">Password</label>
+
+ <div class="input-errors">
+ <label *ngIf="updateUserFormGroup.get('password')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="updateUserFormGroup.get('password')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ <label *ngIf="updateUserFormGroup.get('password')?.errors?.pattern" class="error">*At least 1 number</label>
+ </div>
+ </div>
+ <button type="button" class="submit-btn edit-btn" (click)="toggleLanguages()">▼ Edit Languages ▼</button>
+ <div *ngIf="showLanguages">
+ <div class="input-selection">
+ <input type="text" class="input-field" formControlName="languageInput" required>
+
+ <div class="input-errors">
+ <label class="error">Type in your desired languages, separated by a space</label>
+ </div>
+ </div>
+ Available languages:
+ <div id="all-languages">
+ <div class="user-language" *ngFor="let lang of availableLanguages">
+ {{ lang.name }}
+ </div>
+ </div>
+ </div>
+
+ <button type="button" class="submit-btn edit-btn" (click)="toggleTechnologies()">▼ Edit Technologies ▼</button>
+ <div *ngIf="showTechnologies">
+ <div class="input-selection">
+ <input type="text" class="input-field" formControlName="technologyInput" required>
+
+ <div class="input-errors">
+ <label class="error">Type in your desired technologies, separated by a space</label>
+ </div>
+ </div>
+ Available technologies:
+ <div id="all-technologies">
+ <div class="user-technology" *ngFor="let tech of availableTechnologies">
+ {{ tech.name }}
+ </div>
+ </div>
+ </div>
+
+ <button id="update-profile-btn" class="submit-btn" type="submit">Update profile</button>
+ <app-success-bar></app-success-bar>
+ <app-error-bar></app-error-bar>
+ </form>
+ <hr>
+ <div id="confirm-delete" *ngIf="deleteAccountConfirm">
+ Are you sure you want to delete your account?<br>This is permanent!
+ </div>
+ <button class="submit-btn delete-btn" (click)="deleteAccount()">Delete account</button>
+ </div>
+</div>
diff --git a/src/app/components/profile-settings/profile-settings.component.ts b/src/app/components/profile-settings/profile-settings.component.ts
new file mode 100644
index 0000000..a484665
--- /dev/null
+++ b/src/app/components/profile-settings/profile-settings.component.ts
@@ -0,0 +1,307 @@
+import { Location } from '@angular/common';
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { LanguageService } from 'src/app/services/language.service';
+import { UserService } from 'src/app/services/user.service';
+import { TechnologyService } from 'src/app/services/technology.service';
+import { User } from 'src/models/identity/user';
+import { ErrorBarComponent } from '../error-bar/error-bar.component';
+import { SuccessBarComponent } from '../success-bar/success-bar.component';
+import { Language } from 'src/models/language';
+import { Technology } from 'src/models/technology';
+import { TokenService } from 'src/app/services/token.service';
+import { Title } from '@angular/platform-browser';
+import { AppConstants } from 'src/app/app-constants.module';
+
+@Component({
+ selector: 'app-profile-settings',
+ templateUrl: './profile-settings.component.html',
+ styleUrls: ['./profile-settings.component.css']
+})
+export class ProfileSettingsComponent implements OnInit {
+ private _title = 'Profile Settings';
+ @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent;
+ @ViewChild(SuccessBarComponent) private _successBar: SuccessBarComponent;
+ private _urlUsername: string;
+ public isAdminUser = false;
+ public dataArrived = false;
+ public deleteAccountConfirm = false;
+ public showLanguages = false;
+ public showTechnologies = false;
+ public updateUserFormGroup: FormGroup;
+ public updateProfilePictureFormGroup: FormGroup;
+ public newProfilePicture: File;
+ public user: User;
+ public availableLanguages: Language[];
+ public availableTechnologies: Technology[];
+
+ constructor(private _titleService: Title, private _router: Router, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _fb: FormBuilder, private _location: Location) {
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ this._urlUsername = this._router.url.substring(9);
+ this._urlUsername = this._urlUsername.substring(0, this._urlUsername.length - 9);
+
+ this.user = this._userService.getDefaultUser();
+ this.availableLanguages = [];
+ this.availableTechnologies = [];
+ this.newProfilePicture = new File([], '');
+
+ this._userService.getUserByUsernameRequest(this._urlUsername).subscribe(
+ (res: object) => {
+ Object.assign(this.user, res);
+ this.isAdminUser = this.user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME);
+ this.finishUserLoading();
+ },
+ (err: HttpErrorResponse) => {
+ this._router.navigate(['/not-found']);
+ }
+ );
+
+ this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe(
+ (result: object) => {
+ this.availableLanguages = result as Language[];
+ }
+ );
+ this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe(
+ (result: object) => {
+ this.availableTechnologies = result as Technology[];
+ }
+ );
+ }
+
+ private finishUserLoading(): void {
+ if (sessionStorage.getItem('UserCred')) {
+ const userFromToken: User = this._userService.getDefaultUser();
+
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (tokenRes: object) => {
+ Object.assign(userFromToken, tokenRes);
+
+ if (userFromToken.userName === this._urlUsername) {
+ this.initForms();
+ this.dataArrived = true;
+ }
+ else {
+ this.goToProfile();
+ }
+ },
+ (err: HttpErrorResponse) => {
+ this.logout();
+ }
+ );
+ }
+ else {
+ this.goToProfile();
+ }
+ }
+
+ private initForms(): void {
+ this.updateUserFormGroup = this._fb.group({
+ firstName: new FormControl(this.user.firstName, [
+ Validators.required,
+ Validators.minLength(3)
+ ]),
+ lastName: new FormControl(this.user.lastName, [
+ Validators.required,
+ Validators.minLength(3)
+ ]),
+ username: new FormControl(this.user.userName, [
+ Validators.required,
+ Validators.minLength(3)
+ ]),
+ email: new FormControl(this.user.email, [
+ Validators.required,
+ Validators.email,
+ ]),
+ password: new FormControl('', [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.pattern('.*[0-9].*') // Check if password contains atleast one number
+ ]),
+
+ // For language we have two different controls,
+ // the first one is used for input, the other one for sending data
+ // because if we edit the control for input,
+ // we're also gonna change the input field in the HTML
+ languageInput: new FormControl(''), // The one for input
+ languages: new FormControl(''), // The one that is sent
+
+ // For technologies it's the same as it is with languages
+ technologyInput: new FormControl(''),
+ technologies: new FormControl('')
+ });
+
+ this.getLanguagesForShowing().then(value => {
+ this.updateUserFormGroup.patchValue({ languageInput : value });
+ });
+
+ this.getTechnologiesForShowing().then(value => {
+ this.updateUserFormGroup.patchValue({ technologyInput : value });
+ });
+
+ this.updateProfilePictureFormGroup = this._fb.group({
+ fileUpload: new FormControl('')
+ });
+
+ this.updateUserFormGroup.valueChanges.subscribe(() => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ });
+ }
+
+ private getLanguagesForShowing(): Promise<string> {
+ return new Promise(resolve => {
+ this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then(value => {
+ this.user.languages = value;
+ resolve(value.map(x => x.name).join(' '));
+ });
+ });
+ }
+
+ private getTechnologiesForShowing(): Promise<string> {
+ return new Promise(resolve => {
+ this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then(value => {
+ this.user.technologies = value;
+ resolve(value.map(x => x.name).join(' '));
+ });
+ });
+ }
+
+ onFileUpload(event: any): void {
+ this.newProfilePicture = event.target.files[0];
+ }
+
+ updateProfilePicture(): void {
+ if (this.newProfilePicture.size === 0) {
+ return;
+ }
+
+ this._userService.putProfilePictureFromSessionStorageRequest(this.newProfilePicture).subscribe(
+ (result: object) => {
+ this.reloadPage();
+ }
+ );
+ this.dataArrived = false;
+ }
+
+ onSubmit(): void {
+ this._successBar.hideMsg();
+ this._errorBar.hideError();
+
+ this.patchLanguagesControl();
+ this.patchTechnologiesControl();
+
+ this._userService.putUserFromSessionStorageRequest(this.updateUserFormGroup, this.user.roles, this.user.friends).subscribe(
+ (result: object) => {
+ this._successBar.showMsg('Profile updated successfully!');
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+
+ private patchLanguagesControl(): void {
+ // Get user input
+ const langControl = this.updateUserFormGroup.get('languageInput')?.value as string ?? '';
+
+ if (langControl === '') {
+ // Add the data to the form (to the value that is going to be sent)
+ this.updateUserFormGroup.patchValue({
+ languages : []
+ });
+ }
+ else {
+ const names = langControl.split(' ');
+
+ // Transfer user input to objects of type { "name": "value" }
+ const actualLanguages = [];
+ for (const lName of names) {
+ if (lName !== '') {
+ actualLanguages.push({ name : lName });
+ }
+ }
+
+ // Add the data to the form (to the value that is going to be sent)
+ this.updateUserFormGroup.patchValue({
+ languages : actualLanguages
+ });
+ }
+ }
+
+ private patchTechnologiesControl(): void {
+ // Get user input
+ const techControl = this.updateUserFormGroup.get('technologyInput')?.value as string ?? '';
+
+ if (techControl === '') {
+ // Add the data to the form (to the value that is going to be sent)
+ this.updateUserFormGroup.patchValue({
+ technologies : []
+ });
+ }
+ else {
+ const names = techControl.split(' ');
+
+ // Transfer user input to objects of type { "name": "value" }
+ const actualTechnologies = [];
+ for (const tName of names) {
+ if (tName !== '') {
+ actualTechnologies.push({ name : tName });
+ }
+ }
+
+ // Add the data to the form (to the value that is going to be sent)
+ this.updateUserFormGroup.patchValue({
+ technologies : actualTechnologies
+ });
+ }
+ }
+
+ goToProfile(): void {
+ this._router.navigate([this._router.url.substring(0, this._router.url.length - 9)]);
+ }
+
+ navigateToAdminPanel(): void {
+ this._router.navigate(['/admin-panel']);
+ }
+
+ logout(): void {
+ this._tokenService.logoutUserFromSessionStorage();
+ this.goToProfile();
+ }
+
+ toggleLanguages(): void {
+ this.showLanguages = !this.showLanguages;
+ }
+
+ toggleTechnologies(): void {
+ this.showTechnologies = !this.showTechnologies;
+ }
+
+ deleteAccount(): void {
+ if (this.deleteAccountConfirm) {
+ this._userService.deleteUserFromSessionStorageRequest().subscribe(
+ (res: object) => {
+ this.logout();
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ this.dataArrived = false;
+ }
+ else {
+ this.deleteAccountConfirm = true;
+ }
+ }
+
+ private reloadPage(): void {
+ this._router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this._router.onSameUrlNavigation = 'reload';
+ this._router.navigate([this._router.url]);
+ }
+}
diff --git a/src/app/components/profile/profile.component.css b/src/app/components/profile/profile.component.css
new file mode 100644
index 0000000..ebcd406
--- /dev/null
+++ b/src/app/components/profile/profile.component.css
@@ -0,0 +1,105 @@
+* {
+ box-sizing: border-box;
+}
+
+#content {
+ max-width: 22em;
+ justify-content: start;
+}
+
+hr {
+ width: calc(100% - 1em);
+ color: black;
+ border: 1px solid black;
+}
+
+form {
+ width: 100%;
+}
+
+/* Navigation bar (for loggedin user) */
+
+#navigation {
+ display: flex;
+ width: 100%;
+}
+
+.submit-btn {
+ flex: 1;
+ margin-top: 0;
+ margin-left: .5em;
+ font-size: inherit;
+}
+
+#navigation > .submit-btn:first-child {
+ margin-left: 0;
+}
+
+/* Top card */
+
+#main-info {
+ display: flex;
+ width: 100%;
+ margin-bottom: .25em
+}
+
+#main-info > img {
+ width: 5em;
+ height: 5em;
+}
+
+#other-main-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+#other-main-info > * {
+ font-size: 1.4em;
+}
+
+#other-main-info *:nth-child(1) {
+ margin-top: auto;
+}
+
+#other-main-info *:nth-last-child(1) {
+ margin-bottom: auto;
+}
+
+#add-friend, #loggedin-password {
+ flex: 0 !important;
+ margin-top: .4em;
+ max-width: 8em;
+ font-size: .6em !important;
+}
+
+#loggedin-password {
+ max-width: 100%;
+}
+
+/* Languages and technologies */
+
+.secondary-info {
+ margin-top: .25em;
+ margin-bottom: .25em;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+/* Posts */
+
+#no-posts {
+ width: 100%;
+ text-align: center;
+ color: gray;
+ margin-top: .2em;
+}
+
+#posts {
+ width: 100%;
+ height: 100%;
+}
diff --git a/src/app/components/profile/profile.component.html b/src/app/components/profile/profile.component.html
new file mode 100644
index 0000000..0e5f633
--- /dev/null
+++ b/src/app/components/profile/profile.component.html
@@ -0,0 +1,60 @@
+<app-loading *ngIf="!dataArrived"></app-loading>
+
+<div id="content" *ngIf="dataArrived">
+ <nav id="navigation">
+ <button class="submit-btn" (click)="goBack()">ᐊ Back</button>
+ <button class="submit-btn" (click)="navigateToSettings()" *ngIf="isTheLoggedInUser">Settings</button>
+ <button class="submit-btn" (click)="navigateToAdminPanel()" *ngIf="isTheLoggedInUser && isAdminUser">Panel</button>
+ <button class="submit-btn" (click)="logout()" *ngIf="isTheLoggedInUser">Logout</button>
+ </nav>
+ <hr>
+ <div class="scroll-standalone" (scroll)="onScroll($event)">
+ <div id="main-info" class="rounded-border">
+ <img class="round-image" [src]="user.profilePictureURL" alt=""/>
+ <div id="other-main-info">
+ <div id="name">
+ {{ user.firstName }} {{ user.lastName }}
+ </div>
+ <div id="username">
+ @{{ user.userName }}
+ </div>
+ <form [formGroup]="updateFrienship" (ngSubmit)="modifyFriend()" *ngIf="!isTheLoggedInUser && isUserLoggedIn">
+ <button id="add-friend" type="submit" class="submit-btn">{{ friendOfUser ? 'Unfriend' : 'Add friend' }}</button>
+ <br>
+ <input id="loggedin-password" type="password" formControlName="password" class="input-field" *ngIf="updatingFriendship" placeholder="Type in password to confirm">
+ </form>
+ </div>
+ </div>
+ <div class="secondary-info rounded-border">
+ Languages:
+ <div *ngFor="let lang of user.languages">
+ <div class="user-language">
+ {{ lang.name }}
+ </div>
+ </div>
+ <div *ngIf="user.languages.length === 0">
+ &nbsp;None
+ </div>
+ </div>
+ <div class="secondary-info rounded-border">
+ Technologies:
+ <div *ngFor="let tech of user.technologies">
+ <div class="user-language">
+ {{ tech.name }}
+ </div>
+ </div>
+ <div *ngIf="user.technologies.length === 0">
+ &nbsp;None
+ </div>
+ </div>
+ <hr>
+ <div id="posts">
+ <div id="no-posts" *ngIf="userPosts.length === 0">
+ {{ user.firstName }} {{ user.lastName }} hasn't posted anything yet!
+ </div>
+ <div *ngFor="let userPost of userPosts">
+ <app-post [paramId]="userPost.postId.toString()"></app-post>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts
new file mode 100644
index 0000000..bbf8585
--- /dev/null
+++ b/src/app/components/profile/profile.component.ts
@@ -0,0 +1,203 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { UserService } from 'src/app/services/user.service';
+import { User } from 'src/models/identity/user';
+import { AppConstants } from 'src/app/app-constants.module';
+import { HttpErrorResponse } from '@angular/common/http';
+import { Location } from '@angular/common';
+import { LanguageService } from 'src/app/services/language.service';
+import { TechnologyService } from 'src/app/services/technology.service';
+import { Post } from 'src/models/post';
+import { FeedService } from 'src/app/services/feed.service';
+import { TokenService } from 'src/app/services/token.service';
+import { Title } from '@angular/platform-browser';
+import { Friend } from 'src/models/identity/friend';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+
+@Component({
+ selector: 'app-profile',
+ templateUrl: './profile.component.html',
+ styleUrls: ['./profile.component.css']
+})
+export class ProfileComponent implements OnInit {
+ private _title = 'Profile';
+ private _urlUsername: string;
+ private _timeLoaded: string;
+ private _currentPage: number;
+ public isTheLoggedInUser = false;
+ public isUserLoggedIn = false;
+ public isAdminUser = false;
+ public dataArrived = false;
+ public friendOfUser = false;
+ public updatingFriendship = false;
+ public user: User;
+ public userPosts: Post[];
+ public updateFrienship: FormGroup;
+
+ constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _feedService: FeedService, private _location: Location, private _tokenService: TokenService) {
+ this._titleService.setTitle(this._title);
+ }
+
+ private setDefaultUser(): void {
+ this.user = this._userService.getDefaultUser();
+ }
+
+ ngOnInit(): void {
+ this._urlUsername = this._router.url.substring(9);
+
+ const now = new Date();
+ now.setHours(now.getHours() + 2); // accounting for eastern europe timezone
+ this._timeLoaded = now.toISOString();
+ this._currentPage = 1;
+
+ this.user = this._userService.getDefaultUser();
+ this.userPosts = [];
+
+ this.updateFrienship = this._fb.group({
+ password: new FormControl('')
+ });
+
+ this._userService.getUserByUsernameRequest(this._urlUsername).subscribe(
+ (res: object) => {
+ Object.assign(this.user, res);
+ this.isAdminUser = this.user.roles.map(x => x.name).includes(AppConstants.ADMIN_ROLE_NAME);
+ this.loadLanguages();
+ },
+ (err: HttpErrorResponse) => {
+ this._router.navigate(['/not-found']);
+ }
+ );
+ }
+
+ private loadLanguages(): void {
+ if (this.user.languages.length > 0) {
+ // When user has languages, get their names and load technologies
+ this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then(value => {
+ this.user.languages = value;
+ this.loadTechnologies();
+ });
+ }
+ else {
+ this.loadTechnologies();
+ }
+ }
+
+ private loadTechnologies(): void {
+ if (this.user.technologies.length > 0) {
+ // When user has technologies, get their names and then load posts
+ this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then(value => {
+ this.user.technologies = value;
+ this.loadPosts();
+ });
+ }
+ else {
+ this.loadPosts();
+ }
+ }
+
+ private loadPosts(): void {
+ this._feedService.getUserPostsRequest(this.user.userName, this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe(
+ (result: object) => {
+ const resultArr: Post[] = Object.values(result)[0];
+ this.userPosts.push(...resultArr);
+ this.finishUserLoading();
+ },
+ (err: HttpErrorResponse) => {
+ this._currentPage = -1;
+ this.finishUserLoading();
+ }
+ );
+ }
+
+ private finishUserLoading(): void {
+ if (sessionStorage.getItem('UserCred')) {
+ this.isUserLoggedIn = true;
+ const userFromToken: User = this._userService.getDefaultUser();
+
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (tokenRes: object) => {
+ Object.assign(userFromToken, tokenRes);
+
+ if (userFromToken.friends.map(x => x.userName).includes(this._urlUsername)) {
+ this.friendOfUser = true;
+ }
+ if (userFromToken.userName === this._urlUsername) {
+ this.isTheLoggedInUser = true;
+ }
+ this.dataArrived = true;
+ },
+ (err: HttpErrorResponse) => {
+ this.logout();
+ }
+ );
+ }
+ else {
+ this.dataArrived = true;
+ }
+ }
+
+ goBack(): void {
+ this._router.navigate(['/']);
+ }
+
+ navigateToAdminPanel(): void {
+ this._router.navigate(['/admin-panel']);
+ }
+
+ navigateToSettings(): void {
+ this._router.navigate([this._router.url + '/settings']);
+ }
+
+ logout(): void {
+ this._tokenService.logoutUserFromSessionStorage();
+
+ // Reload the page
+ this._router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this._router.onSameUrlNavigation = 'reload';
+ this._router.navigate([this._router.url]);
+ }
+
+ modifyFriend(): void {
+ if (this.updatingFriendship) {
+ this.dataArrived = false;
+
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (result: object) => {
+ const loggedInUser: User = result as User;
+
+ if (this.friendOfUser) {
+ loggedInUser.friends = loggedInUser.friends.filter(x => x.userName !== this.user.userName);
+ }
+ else {
+ const newFriend = new Friend();
+ newFriend.userName = this.user.userName;
+ loggedInUser.friends.push(newFriend);
+ }
+
+ this._userService.putBareUserFromSessionStorageRequest(loggedInUser, this.updateFrienship.get('password')?.value).subscribe(
+ (resultUpdate: object) => {
+ this.reloadPage();
+ },
+ (err: HttpErrorResponse) => {
+ this._router.navigate(['/']);
+ }
+ );
+ }
+ );
+ }
+ this.updatingFriendship = !this.updatingFriendship;
+ }
+
+ onScroll(event: any): void {
+ // Detects when the element has reached the bottom, thx https://stackoverflow.com/a/50038429/12036073
+ if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight && this._currentPage > 0) {
+ this.loadPosts();
+ }
+ }
+
+ private reloadPage(): void {
+ this._router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this._router.onSameUrlNavigation = 'reload';
+ this._router.navigate([this._router.url]);
+ }
+}
diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css
new file mode 100644
index 0000000..93d8006
--- /dev/null
+++ b/src/app/components/register/register.component.css
@@ -0,0 +1,40 @@
+/* A lot of stuff are moved to the global styles! */
+
+* {
+ transition: 0.2s;
+}
+
+form {
+ width: 100%;
+}
+
+@media screen and (max-height: 630px) {
+ #content {
+ height: fit-content !important;
+ }
+}
+
+#content hr {
+ width: 100%;
+ border: 1px solid black;
+ box-sizing: border-box;
+}
+
+.input-selection:nth-of-type(1) {
+ margin-top: 1.2em;
+}
+
+.submit-btn {
+ margin-bottom: .2em;
+}
+
+.redirect-to-login {
+ color: var(--focus-color);
+ background-color: var(--bg-color);
+ border-color: var(--focus-color);
+}
+
+.redirect-to-login:hover {
+ border-color: black !important;
+ color: black;
+}
diff --git a/src/app/components/register/register.component.html b/src/app/components/register/register.component.html
new file mode 100644
index 0000000..4e67e0e
--- /dev/null
+++ b/src/app/components/register/register.component.html
@@ -0,0 +1,65 @@
+<div id="content">
+ <div class="title">Register</div>
+
+ <form [formGroup]="registerUserFormGroup" (ngSubmit)="onSubmit()">
+ <hr>
+ <!-- Value: {{ registerUserFormGroup.value | json }}
+ <hr> -->
+
+ <div class="input-selection">
+ <input type="text" placeholder="Goshko, is that u?" class="input-field" formControlName="firstName" required>
+ <label class="input-field-label">First Name</label>
+
+ <div class="input-errors">
+ <label *ngIf="registerUserFormGroup.get('firstName')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="registerUserFormGroup.get('firstName')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="text" placeholder="Trapov? Really??" class="input-field" formControlName="lastName" required>
+ <label class="input-field-label">Last Name</label>
+
+ <div class="input-errors">
+ <label *ngIf="registerUserFormGroup.get('lastName')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="registerUserFormGroup.get('lastName')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="text" placeholder="Think of something cool to flex on other kids" class="input-field" formControlName="username" required>
+ <label class="input-field-label">Username</label>
+
+ <div class="input-errors">
+ <label *ngIf="registerUserFormGroup.get('username')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="registerUserFormGroup.get('username')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="text" placeholder="You expect an email joke? I have none, mail me one" class="input-field" formControlName="email" required>
+ <label class="input-field-label">Email</label>
+
+ <div class="input-errors">
+ <label *ngIf="registerUserFormGroup.get('email')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="registerUserFormGroup.get('email')?.errors?.email" class="error">*Invalid email</label>
+ </div>
+ </div>
+
+ <div class="input-selection">
+ <input type="password" placeholder="Make sure it's long & strong (just like my d***)" class="input-field" formControlName="password" required>
+ <label class="input-field-label">Password</label>
+
+ <div class="input-errors">
+ <label *ngIf="registerUserFormGroup.get('password')?.errors?.required" class="error">*Required</label>
+ <label *ngIf="registerUserFormGroup.get('password')?.errors?.minlength" class="error">*Minimum 3 characters</label>
+ <label *ngIf="registerUserFormGroup.get('password')?.errors?.pattern" class="error">*At least 1 number</label>
+ </div>
+ </div>
+
+ <hr>
+ <button class="submit-btn" type="submit">Submit</button>
+ <app-error-bar></app-error-bar>
+ </form>
+ <button class="submit-btn redirect-to-login" (click)="onRedirectLogin()">Already have an account? Login here</button>
+</div>
diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts
new file mode 100644
index 0000000..36eaa55
--- /dev/null
+++ b/src/app/components/register/register.component.ts
@@ -0,0 +1,86 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { Title } from '@angular/platform-browser';
+import { Router } from '@angular/router';
+import { TokenService } from 'src/app/services/token.service';
+import { UserService } from 'src/app/services/user.service';
+import { ErrorBarComponent } from '../error-bar/error-bar.component';
+
+@Component({
+ selector: 'app-register',
+ templateUrl: './register.component.html',
+ styleUrls: ['./register.component.css']
+})
+export class RegisterComponent implements OnInit {
+ @ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent;
+ private _title = 'Register';
+ public registerUserFormGroup: FormGroup;
+
+ constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) {
+ this._titleService.setTitle(this._title);
+ }
+
+ ngOnInit(): void {
+ this.registerUserFormGroup = this._fb.group({
+ firstName: new FormControl('', [
+ Validators.required,
+ Validators.minLength(3)
+ ]),
+ lastName: new FormControl('', [
+ Validators.required,
+ Validators.minLength(3)
+ ]),
+ username: new FormControl('', [
+ Validators.required,
+ Validators.minLength(3)
+ ]),
+ email: new FormControl('', [
+ Validators.required,
+ Validators.email,
+ ]),
+ password: new FormControl('', [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.pattern('.*[0-9].*') // Check if password contains atleast one number
+ ]),
+ });
+
+ // this.registerUserFormGroup.valueChanges.subscribe(console.log);
+ }
+
+ onSubmit(): void {
+ this._userService.registerUserRequest(this.registerUserFormGroup).subscribe(
+ res => {
+ this._tokenService.setUserTokenToSessionStorage(res);
+ this._router.navigate(['/']);
+ },
+ (err: HttpErrorResponse) => {
+ this._errorBar.showError(err);
+ }
+ );
+ }
+ onRedirectLogin(): void {
+ this._router.navigate(['/login']);
+ }
+
+ get firstName(): AbstractControl | null {
+ return this.registerUserFormGroup.get('firstName');
+ }
+
+ get lastName(): AbstractControl | null {
+ return this.registerUserFormGroup.get('lastName');
+ }
+
+ get username(): AbstractControl | null {
+ return this.registerUserFormGroup.get('username');
+ }
+
+ get email(): AbstractControl | null {
+ return this.registerUserFormGroup.get('email');
+ }
+
+ get password(): AbstractControl | null {
+ return this.registerUserFormGroup.get('password');
+ }
+}
diff --git a/src/app/components/success-bar/success-bar.component.css b/src/app/components/success-bar/success-bar.component.css
new file mode 100644
index 0000000..bee634d
--- /dev/null
+++ b/src/app/components/success-bar/success-bar.component.css
@@ -0,0 +1,11 @@
+#success-bar {
+ width: 100%;
+ background-color: var(--success);
+ color: white;
+ padding: .2em;
+ text-align: center;
+}
+
+#success-bar:empty {
+ display: none;
+}
diff --git a/src/app/components/success-bar/success-bar.component.html b/src/app/components/success-bar/success-bar.component.html
new file mode 100644
index 0000000..026e955
--- /dev/null
+++ b/src/app/components/success-bar/success-bar.component.html
@@ -0,0 +1 @@
+<div id="success-bar">{{successMsg}}</div>
diff --git a/src/app/components/success-bar/success-bar.component.ts b/src/app/components/success-bar/success-bar.component.ts
new file mode 100644
index 0000000..f7c7e54
--- /dev/null
+++ b/src/app/components/success-bar/success-bar.component.ts
@@ -0,0 +1,33 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-success-bar',
+ templateUrl: './success-bar.component.html',
+ styleUrls: ['./success-bar.component.css']
+})
+export class SuccessBarComponent implements OnInit {
+ public successMsg = '';
+
+ constructor()
+ { }
+
+ ngOnInit(): void {
+ this.hideMsg();
+ }
+
+ showMsg(msg?: string | undefined): void {
+ if (msg === undefined) {
+ this.successMsg = 'Success!';
+ }
+ else if (msg.trim() === '') {
+ this.successMsg = 'Success!';
+ }
+ else {
+ this.successMsg = msg;
+ }
+ }
+
+ hideMsg(): void {
+ this.successMsg = '';
+ }
+}
diff --git a/src/app/services/cloudinary.service.ts b/src/app/services/cloudinary.service.ts
new file mode 100644
index 0000000..999e498
--- /dev/null
+++ b/src/app/services/cloudinary.service.ts
@@ -0,0 +1,17 @@
+import {HttpClient} from '@angular/common/http';
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CloudinaryService {
+ constructor(private _http: HttpClient)
+ { }
+
+ getFileRequest(fileLink: string): Observable<Blob> {
+ return this._http.get(fileLink, {
+ responseType: 'blob'
+ });
+ }
+}
diff --git a/src/app/services/comment.service.ts b/src/app/services/comment.service.ts
new file mode 100644
index 0000000..c9dbf35
--- /dev/null
+++ b/src/app/services/comment.service.ts
@@ -0,0 +1,83 @@
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Guid } from 'guid-typescript';
+import { Observable } from 'rxjs';
+import { Comment } from 'src/models/comment';
+import { AppConstants } from '../app-constants.module';
+import { TokenService } from './token.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CommentService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ getDefaultComment(): Comment {
+ return new Comment(Guid.createEmpty(), Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date());
+ }
+
+ /* Requests from session storage */
+
+ createCommentWithSessionStorageRequest(postId: Guid, commentMessage: string): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.createCommentRequest(userId, token, postId, commentMessage);
+ }
+
+ putCommentWithSessionStorageRequest(commentId: Guid, postId: Guid, newMessage: string): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putCommentRequest(userId, token, commentId, postId, newMessage);
+ }
+
+ deleteCommentWithSessionStorage(commentId: Guid): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.deleteCommentRequest(commentId, token);
+ }
+
+ /* Comment requests */
+
+ createCommentRequest(userId: Guid, authToken: string, postId: Guid, commentMessage: string): Observable<object> {
+ const body = {
+ postId: postId.toString(),
+ message: commentMessage
+ };
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.post(AppConstants.API_COMMENT_URL, body, options);
+ }
+
+ getCommentRequest(id: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', id.toString())
+ };
+ return this._http.get(AppConstants.API_COMMENT_URL, options);
+ }
+
+ putCommentRequest(userId: Guid, authToken: string, commentId: Guid, postId: Guid, newMessage: string): Observable<object> {
+ const body = {
+ commentId: commentId.toString(),
+ postId: postId.toString(),
+ newMessage: newMessage
+ };
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_COMMENT_URL, body, options);
+ }
+
+ deleteCommentRequest(commentId: Guid, authToken: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', commentId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.delete(AppConstants.API_COMMENT_URL, options);
+ }
+}
diff --git a/src/app/services/feed.service.ts b/src/app/services/feed.service.ts
new file mode 100644
index 0000000..d160f6d
--- /dev/null
+++ b/src/app/services/feed.service.ts
@@ -0,0 +1,50 @@
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Guid } from 'guid-typescript';
+import { Observable } from 'rxjs';
+import { AppConstants } from '../app-constants.module';
+import { TokenService } from './token.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class FeedService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ /* Requests from session storage */
+
+ getUserFeedFromSessionStorageRequest(pageNumber: number, firstTimeIssued: string, pageSize: number): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+
+ return this.getUserFeedRequest(userId, token, pageNumber, firstTimeIssued, pageSize);
+ }
+
+ /* Feed requests */
+
+ getUserFeedRequest(userId: Guid, authToken: string, pageNumber: number, firstTimeIssued: string, pageSize: number): Observable<object> {
+ const body = {
+ pageNumber: pageNumber,
+ firstPageTimeIssued: firstTimeIssued,
+ pageSize: pageSize
+ };
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.post(AppConstants.API_FEED_URL + '/GetPosts', body, options);
+ }
+
+ getUserPostsRequest(userName: string, pageNumber: number, firstTimeIssued: string, pageSize: number): Observable<object> {
+ const body = {
+ pageNumber: pageNumber,
+ firstPageTimeIssued: firstTimeIssued,
+ pageSize: pageSize
+ };
+ const options = {
+ params: new HttpParams().set('UserName', userName)
+ };
+ return this._http.post(AppConstants.API_FEED_URL + '/GetUserPosts', body, options);
+ }
+}
diff --git a/src/app/services/language.service.ts b/src/app/services/language.service.ts
new file mode 100644
index 0000000..15e241f
--- /dev/null
+++ b/src/app/services/language.service.ts
@@ -0,0 +1,113 @@
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Guid } from 'guid-typescript';
+import { Observable } from 'rxjs';
+import { Language } from 'src/models/language';
+import { AppConstants } from '../app-constants.module';
+import { TokenService } from './token.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LanguageService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ /* Requests from session storage */
+
+ createLanguageWithSessionStorageRequest(name: string): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.createLanguageRequest(name, token);
+ }
+
+ getAllLanguagesWithSessionStorageRequest(): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.getAllLanguagesRequest(token);
+ }
+
+ putLanguageWithSessionStorageRequest(langId: Guid, newName: string): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putLanguageRequest(token, langId, newName);
+ }
+
+ deleteLanguageWithSessionStorageRequest(langId: Guid): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.deleteLanguageRequest(token, langId);
+ }
+
+ /* Language requests */
+
+ createLanguageRequest(name: string, authToken: string): Observable<object> {
+ const body = {
+ name: name
+ };
+ const options = {
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.post(AppConstants.API_LANGUAGE_URL, body, options);
+ }
+
+ getLanguageRequest(langId: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', langId.toString()),
+ };
+ return this._http.get(AppConstants.API_LANGUAGE_URL, options);
+ }
+
+ getAllLanguagesRequest(authToken: string): Observable<object> {
+ const options = {
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.get(AppConstants.API_LANGUAGE_URL + '/GetLanguages', options);
+ }
+
+ getFullLanguagesFromIncomplete(givenLanguages: Language[]): Promise<Language[]> {
+ if (givenLanguages.length === 0) {
+ return new Promise(resolve => resolve(givenLanguages));
+ }
+
+ // This accepts language array with incomplete languages, meaning
+ // languages that only have an id, but no name
+ return new Promise(resolve => {
+ const lastGuid = givenLanguages[givenLanguages.length - 1].id;
+
+ // For each language, request his name and assign it
+ for (const lang of givenLanguages) {
+ this.getLanguageRequest(lang.id).subscribe(
+ (result: object) => {
+ // this only assigns the "name" property to the language,
+ // because only the name is returned from the request
+ Object.assign(lang, result);
+
+ if (lastGuid === lang.id) {
+ resolve(givenLanguages);
+ }
+ }
+ );
+ }
+ });
+ }
+
+ putLanguageRequest(authToken: string, langId: Guid, newName: string): Observable<object> {
+ const body = {
+ name: newName
+ };
+ const options = {
+ params: new HttpParams().set('Id', langId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_LANGUAGE_URL, body, options);
+ }
+
+ deleteLanguageRequest(authToken: string, langId: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', langId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.delete(AppConstants.API_LANGUAGE_URL, options);
+ }
+}
diff --git a/src/app/services/post.service.ts b/src/app/services/post.service.ts
new file mode 100644
index 0000000..7b2a539
--- /dev/null
+++ b/src/app/services/post.service.ts
@@ -0,0 +1,86 @@
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import * as FormData from 'form-data';
+import { Guid } from 'guid-typescript';
+import { Observable } from 'rxjs';
+import { Post } from 'src/models/post';
+import { AppConstants } from '../app-constants.module';
+import { TokenService } from './token.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class PostService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ getDefaultPost(): Post {
+ return new Post(Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date(), [], []);
+ }
+
+ /* Requests from session storage */
+
+ createPostWithSessionStorageRequest(postMessage: string, files: File[]): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.createPostRequest(userId, token, postMessage, files);
+ }
+
+ putPostWithSessionStorageRequest(postId: Guid, newMessage: string, posts: File[]): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putPostRequest(userId, token, postId, newMessage, posts);
+ }
+
+ deletePostWithSessionStorage(postId: Guid): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.deletePostRequest(postId, token);
+ }
+
+ /* Post requests */
+
+ createPostRequest(userId: Guid, authToken: string, postMessage: string, files: File[]): Observable<object> {
+ const form = new FormData();
+ form.append('message', postMessage);
+ for (const file of files) {
+ form.append('files', file, file.name);
+ }
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.post(AppConstants.API_POST_URL, form, options);
+ }
+
+ getPostRequest(id: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', id.toString())
+ };
+ return this._http.get(AppConstants.API_POST_URL, options);
+ }
+
+ putPostRequest(userId: Guid, authToken: string, postId: Guid, newMessage: string, files: File[]): Observable<object> {
+ const form = new FormData();
+ form.append('postId', postId);
+ form.append('newMessage', newMessage);
+ for (const file of files) {
+ form.append('files', file, file.name);
+ }
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_POST_URL, form, options);
+ }
+
+ deletePostRequest(postId: Guid, authToken: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', postId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.delete(AppConstants.API_POST_URL, options);
+ }
+}
diff --git a/src/app/services/technology.service.ts b/src/app/services/technology.service.ts
new file mode 100644
index 0000000..dbdc039
--- /dev/null
+++ b/src/app/services/technology.service.ts
@@ -0,0 +1,113 @@
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Guid } from 'guid-typescript';
+import { Observable } from 'rxjs';
+import { Technology } from 'src/models/technology';
+import { AppConstants } from '../app-constants.module';
+import { TokenService } from './token.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TechnologyService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ /* Requests from session storage */
+
+ createTechnologyWithSessionStorageRequest(name: string): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.createtTechnologyRequest(name, token);
+ }
+
+ getAllTechnologiesWithSessionStorageRequest(): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.getAllTechnologiesRequest(token);
+ }
+
+ putTechnologyWithSessionStorageRequest(langId: Guid, newName: string): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putTechnologyRequest(token, langId, newName);
+ }
+
+ deleteTechnologyWithSessionStorageRequest(langId: Guid): Observable<object> {
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.deleteTechnologyRequest(token, langId);
+ }
+
+ /* Technology requests */
+
+ createtTechnologyRequest(name: string, authToken: string): Observable<object> {
+ const body = {
+ name: name
+ };
+ const options = {
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.post(AppConstants.API_TECHNOLOGY_URL, body, options);
+ }
+
+ getTechnologyRequest(techId: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', techId.toString())
+ };
+ return this._http.get(AppConstants.API_TECHNOLOGY_URL, options);
+ }
+
+ getAllTechnologiesRequest(authToken: string): Observable<object> {
+ const options = {
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.get(AppConstants.API_TECHNOLOGY_URL + '/GetTechnologies', options);
+ }
+
+ getFullTechnologiesFromIncomplete(givenTechnologies: Technology[]): Promise<Technology[]> {
+ if (givenTechnologies.length === 0) {
+ return new Promise(resolve => resolve(givenTechnologies));
+ }
+
+ // This accepts language array with incomplete languages, meaning
+ // languages that only have an id, but no name
+ return new Promise(resolve => {
+ const lastGuid = givenTechnologies[givenTechnologies.length - 1].id;
+
+ // For each language, request his name and assign it
+ for (const tech of givenTechnologies) {
+ this.getTechnologyRequest(tech.id).subscribe(
+ (result: object) => {
+ // this only assigns the "name" property to the language,
+ // because only the name is returned from the request
+ Object.assign(tech, result);
+
+ if (lastGuid === tech.id) {
+ resolve(givenTechnologies);
+ }
+ }
+ );
+ }
+ });
+ }
+
+ putTechnologyRequest(authToken: string, langId: Guid, newName: string): Observable<object> {
+ const body = {
+ name: newName
+ };
+ const options = {
+ params: new HttpParams().set('Id', langId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_TECHNOLOGY_URL, body, options);
+ }
+
+ deleteTechnologyRequest(authToken: string, langId: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', langId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.delete(AppConstants.API_TECHNOLOGY_URL, options);
+ }
+}
diff --git a/src/app/services/token.service.ts b/src/app/services/token.service.ts
new file mode 100644
index 0000000..62bc07e
--- /dev/null
+++ b/src/app/services/token.service.ts
@@ -0,0 +1,47 @@
+import { Injectable } from '@angular/core';
+import { Guid } from 'guid-typescript';
+import jwt_decode from 'jwt-decode';
+import { IJWTPayload } from 'src/interfaces/jwt-payload';
+import { IUserCredentials } from 'src/interfaces/user-credentials';
+import { AppConstants } from '../app-constants.module';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TokenService {
+ constructor()
+ { }
+
+ /* Session storage */
+
+ setUserTokenToSessionStorage(response: object): void {
+ const token = JSON.stringify(response);
+ sessionStorage.setItem(AppConstants.SESSION_TOKEN_KEY, token.substr(10, token.length - 12));
+ }
+
+ getTokenFromSessionStorage(): string {
+ return sessionStorage.getItem(AppConstants.SESSION_TOKEN_KEY) ?? '';
+ }
+
+ getUserIdFromSessionStorageToken(): Guid {
+ const jwt: IJWTPayload = {
+ token: this.getTokenFromSessionStorage()
+ };
+ const userCred = jwt_decode<IUserCredentials>(jwt.token);
+
+ return userCred.ID;
+ }
+
+ getUsernameFromSessionStorageToken(): string {
+ const jwt: IJWTPayload = {
+ token: this.getTokenFromSessionStorage()
+ };
+ const userCred = jwt_decode<IUserCredentials>(jwt.token);
+
+ return userCred.Username;
+ }
+
+ logoutUserFromSessionStorage(): void {
+ sessionStorage.removeItem(AppConstants.SESSION_TOKEN_KEY);
+ }
+}
diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts
new file mode 100644
index 0000000..31862c4
--- /dev/null
+++ b/src/app/services/user.service.ts
@@ -0,0 +1,179 @@
+import { Injectable } from '@angular/core';
+import { Guid } from 'guid-typescript';
+import { User } from '../../models/identity/user';
+import { FormGroup } from '@angular/forms';
+import { AppConstants } from 'src/app/app-constants.module';
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { Role } from 'src/models/identity/role';
+import { Friend } from 'src/models/identity/friend';
+import { TokenService } from './token.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ getDefaultUser(): User {
+ return new User(Guid.createEmpty(), 'gosho_trapov', 'Gosho', 'Trapov', 'gotra@bg.com', AppConstants.FALLBACK_PROFILE_ICON, [], [], [], []);
+ }
+
+ /* Requests from session storage */
+
+ getUserFromSessionStorageRequest(): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.getUserRequest(userId, token);
+ }
+
+ addFriendToUserFromSessionStorageRequest(newFriendUserName: string): Observable<object> {
+ const userUserName = this._tokenService.getUsernameFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.addFriendToUserRequest(userUserName, token, newFriendUserName);
+ }
+
+ putUserFromSessionStorageRequest(updateUserFormGroup: FormGroup, userRoles: Role[], userFriends: Friend[]): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putUserRequest(userId, token, updateUserFormGroup, userRoles, userFriends);
+ }
+
+ putProfilePictureFromSessionStorageRequest(newPicture: File): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putProfilePictureRequest(userId, token, newPicture);
+ }
+
+ putBareUserFromSessionStorageRequest(user: User, password: string): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putBareUserRequest(userId, token, user, password);
+ }
+
+ deleteUserFromSessionStorageRequest(): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.deleteUserRequest(userId, token);
+ }
+
+ removeFriendFromUserFromSessionStorageRequest(friendToRemoveUserName: string): Observable<object> {
+ const userUserName = this._tokenService.getUsernameFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.removeFriendFromUserRequest(userUserName, token, friendToRemoveUserName);
+ }
+
+
+ /* User requests */
+
+ loginUserRequest(loginUserFormGroup: FormGroup): Observable<object> {
+ const body = {
+ UserName: loginUserFormGroup.get('username')?.value,
+ Password: loginUserFormGroup.get('password')?.value
+ };
+ return this._http.post(AppConstants.API_USER_LOGIN_URL, body);
+ }
+
+ registerUserRequest(registerUserFormGroup: FormGroup): Observable<object> {
+ const body = {
+ UserName: registerUserFormGroup.get('username')?.value,
+ Email: registerUserFormGroup.get('email')?.value,
+ FirstName: registerUserFormGroup.get('firstName')?.value,
+ LastName: registerUserFormGroup.get('lastName')?.value,
+ Password: registerUserFormGroup.get('password')?.value
+ };
+ return this._http.post(AppConstants.API_USER_REGISTER_URL, body);
+ }
+
+ addFriendToUserRequest(userUserName: string, authToken: string, newFriendUserName: string): Observable<object> {
+ const body = {
+ newFriendUserName: newFriendUserName
+ };
+ const options = {
+ params: new HttpParams().set('UserName', userUserName),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_USER_URL + '/AddFriend', body, options);
+ }
+
+ getUserRequest(userId: Guid, authToken: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.get(AppConstants.API_USER_URL, options);
+ }
+
+ getUserByUsernameRequest(username: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserName', username),
+ };
+ return this._http.get(AppConstants.API_USER_URL + '/GetUser', options);
+ }
+
+ putUserRequest(userId: Guid, authToken: string, updateUserFormGroup: FormGroup, userRoles: Role[], userFriends: Friend[]): Observable<object> {
+ const body = {
+ UserName: updateUserFormGroup.get('username')?.value,
+ Email: updateUserFormGroup.get('email')?.value,
+ FirstName: updateUserFormGroup.get('firstName')?.value,
+ LastName: updateUserFormGroup.get('lastName')?.value,
+ Password: updateUserFormGroup.get('password')?.value,
+ Roles: userRoles,
+ Friends: userFriends,
+ Languages: updateUserFormGroup.get('languages')?.value,
+ Technologies: updateUserFormGroup.get('technologies')?.value
+ };
+ const options = {
+ params: new HttpParams().set('Id', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_USER_URL, body, options);
+ }
+
+ putBareUserRequest(userId: Guid, authToken: string, user: User, password: string): Observable<object> {
+ const body: object = user;
+ Object.assign(body, { password: password });
+ const options = {
+ params: new HttpParams().set('Id', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_USER_URL, body, options);
+ }
+
+ putProfilePictureRequest(userId: Guid, authToken: string, newPicture: File): Observable<object> {
+ const form = new FormData();
+ form.append('picture', newPicture);
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.put(AppConstants.API_USER_URL + '/ProfilePicture', form, options);
+ }
+
+ deleteUserRequest(userId: Guid, authToken: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.delete(AppConstants.API_USER_URL, options);
+ }
+
+ removeFriendFromUserRequest(userUserName: string, authToken: string, friendToRemoveUserName: string): Observable<object> {
+ const body = {
+ friendUserNameToRemove: friendToRemoveUserName
+ };
+ const options = {
+ params: new HttpParams().set('UserName', userUserName),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.post(AppConstants.API_USER_URL + '/RemoveFriend', body, options);
+ }
+}
diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/assets/.gitkeep
diff --git a/src/assets/images/comment.png b/src/assets/images/comment.png
new file mode 100644
index 0000000..5f8e8d9
--- /dev/null
+++ b/src/assets/images/comment.png
Binary files differ
diff --git a/src/assets/images/feed/chat-pic.png b/src/assets/images/feed/chat-pic.png
new file mode 100644
index 0000000..60241fa
--- /dev/null
+++ b/src/assets/images/feed/chat-pic.png
Binary files differ
diff --git a/src/assets/images/feed/profile-pic.png b/src/assets/images/feed/profile-pic.png
new file mode 100644
index 0000000..87f67f5
--- /dev/null
+++ b/src/assets/images/feed/profile-pic.png
Binary files differ
diff --git a/src/assets/images/paper-clip.png b/src/assets/images/paper-clip.png
new file mode 100644
index 0000000..46ce0a7
--- /dev/null
+++ b/src/assets/images/paper-clip.png
Binary files differ
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
new file mode 100644
index 0000000..3612073
--- /dev/null
+++ b/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
new file mode 100644
index 0000000..7b4f817
--- /dev/null
+++ b/src/environments/environment.ts
@@ -0,0 +1,16 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
diff --git a/src/favicon.ico b/src/favicon.ico
new file mode 100644
index 0000000..997406a
--- /dev/null
+++ b/src/favicon.ico
Binary files differ
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..d488ca9
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <base href="/">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
+ <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+</head>
+
+<body class="mat-typography">
+ <app-root></app-root>
+</body>
+
+</html> \ No newline at end of file
diff --git a/src/interfaces/api-error.ts b/src/interfaces/api-error.ts
new file mode 100644
index 0000000..4dd68f3
--- /dev/null
+++ b/src/interfaces/api-error.ts
@@ -0,0 +1,6 @@
+export interface IApiError {
+ type: string;
+ title: string;
+ status: number;
+ traceId: string;
+}
diff --git a/src/interfaces/jwt-payload.ts b/src/interfaces/jwt-payload.ts
new file mode 100644
index 0000000..a2d0f0d
--- /dev/null
+++ b/src/interfaces/jwt-payload.ts
@@ -0,0 +1,3 @@
+export interface IJWTPayload {
+ token: string;
+}
diff --git a/src/interfaces/user-credentials.ts b/src/interfaces/user-credentials.ts
new file mode 100644
index 0000000..bb47540
--- /dev/null
+++ b/src/interfaces/user-credentials.ts
@@ -0,0 +1,6 @@
+import { Guid } from 'guid-typescript';
+
+export interface IUserCredentials {
+ ID: Guid;
+ Username: string;
+}
diff --git a/src/main.server.ts b/src/main.server.ts
new file mode 100644
index 0000000..10150a7
--- /dev/null
+++ b/src/main.server.ts
@@ -0,0 +1,10 @@
+import { enableProdMode } from '@angular/core';
+
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+export { AppServerModule } from './app/app.server.module';
+export { renderModule, renderModuleFactory } from '@angular/platform-server';
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..ebf5fc9
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,14 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.error(err));
+});
diff --git a/src/models/comment.ts b/src/models/comment.ts
new file mode 100644
index 0000000..0d1755f
--- /dev/null
+++ b/src/models/comment.ts
@@ -0,0 +1,70 @@
+import { Guid } from 'guid-typescript';
+
+export class Comment {
+ private _commentId: Guid;
+ private _postId: Guid;
+ private _issuerFirstName: string;
+ private _issuerLastName: string;
+ private _issuerUsername: string;
+ private _message: string;
+ private _timeCreated: Date;
+
+ constructor(commentId: Guid, postId: Guid, issuerFirstName: string, issuerLastName: string, issuerUsername: string, message: string, timeCreated: Date) {
+ this.commentId = commentId;
+ this.postId = postId;
+ this.issuerFirstName = issuerFirstName;
+ this.issuerLastName = issuerLastName;
+ this.issuerUsername = issuerUsername;
+ this.message = message;
+ this.timeCreated = timeCreated;
+ }
+
+ public get commentId(): Guid {
+ return this._commentId;
+ }
+ public set commentId(v: Guid) {
+ this._commentId = v;
+ }
+
+ public get postId(): Guid {
+ return this._postId;
+ }
+ public set postId(v: Guid) {
+ this._postId = v;
+ }
+
+ public get issuerFirstName(): string {
+ return this._issuerFirstName;
+ }
+ public set issuerFirstName(v: string) {
+ this._issuerFirstName = v;
+ }
+
+ public get issuerLastName(): string {
+ return this._issuerLastName;
+ }
+ public set issuerLastName(v: string) {
+ this._issuerLastName = v;
+ }
+
+ public get issuerUsername(): string {
+ return this._issuerUsername;
+ }
+ public set issuerUsername(v: string) {
+ this._issuerUsername = v;
+ }
+
+ public get message(): string {
+ return this._message;
+ }
+ public set message(v: string) {
+ this._message = v;
+ }
+
+ public get timeCreated(): Date {
+ return this._timeCreated;
+ }
+ public set timeCreated(v: Date) {
+ this._timeCreated = v;
+ }
+}
diff --git a/src/models/identity/friend.ts b/src/models/identity/friend.ts
new file mode 100644
index 0000000..22290cd
--- /dev/null
+++ b/src/models/identity/friend.ts
@@ -0,0 +1,3 @@
+export class Friend {
+ public userName: string;
+}
diff --git a/src/models/identity/role.ts b/src/models/identity/role.ts
new file mode 100644
index 0000000..132b0b0
--- /dev/null
+++ b/src/models/identity/role.ts
@@ -0,0 +1,3 @@
+export class Role {
+ public name: string;
+}
diff --git a/src/models/identity/user.ts b/src/models/identity/user.ts
new file mode 100644
index 0000000..e0038e0
--- /dev/null
+++ b/src/models/identity/user.ts
@@ -0,0 +1,100 @@
+import { Guid } from 'guid-typescript';
+import { Language } from '../language';
+import { Technology } from '../technology';
+import { Friend } from './friend';
+import { Role } from './role';
+
+export class User {
+ private _id : Guid;
+ private _lastName : string;
+ private _firstName : string;
+ private _userName : string;
+ private _email: string;
+ private _profilePictureURL : string;
+ private _languages: Language[];
+ private _technologies: Technology[];
+ private _roles: Role[];
+ private _friends: Friend[];
+
+ constructor(id: Guid, userName: string, firstName: string, lastName: string, email: string, profilePictureURL: string, languages: Language[], technologies: Technology[], roles: Role[], friends: Friend[]) {
+ this.id = id;
+ this.userName = userName;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.email = email;
+ this._profilePictureURL = profilePictureURL;
+ this.languages = languages;
+ this.technologies = technologies;
+ this.roles = roles;
+ }
+
+ public get id(): Guid {
+ return this._id;
+ }
+ public set id(v: Guid) {
+ this._id = v;
+ }
+
+ public get userName(): string {
+ return this._userName;
+ }
+ public set userName(v: string) {
+ this._userName = v;
+ }
+
+ public get firstName(): string {
+ return this._firstName;
+ }
+ public set firstName(v: string) {
+ this._firstName = v;
+ }
+
+ public get lastName(): string {
+ return this._lastName;
+ }
+ public set lastName(v: string) {
+ this._lastName = v;
+ }
+
+ public get email(): string {
+ return this._email;
+ }
+ public set email(v: string) {
+ this._email = v;
+ }
+
+ public get profilePictureURL(): string {
+ return this._profilePictureURL;
+ }
+ public set profilePictureURL(v: string) {
+ this._profilePictureURL = v;
+ }
+
+ public get languages(): Language[] {
+ return this._languages;
+ }
+ public set languages(v: Language[]) {
+ this._languages = v;
+ }
+
+ public get technologies(): Technology[] {
+ return this._technologies;
+ }
+ public set technologies(v: Technology[]) {
+ this._technologies = v;
+ }
+
+ public get roles(): Role[] {
+ return this._roles;
+ }
+ public set roles(v: Role[]) {
+ this._roles = v;
+ }
+
+ public get friends(): Friend[] {
+ return this._friends;
+ }
+ public set friends(v: Friend[]) {
+ this._friends = v;
+ }
+}
diff --git a/src/models/language.ts b/src/models/language.ts
new file mode 100644
index 0000000..e3aa61e
--- /dev/null
+++ b/src/models/language.ts
@@ -0,0 +1,6 @@
+import { Guid } from 'guid-typescript';
+
+export class Language {
+ public id: Guid;
+ public name: string;
+}
diff --git a/src/models/post-comment.ts b/src/models/post-comment.ts
new file mode 100644
index 0000000..5d1e346
--- /dev/null
+++ b/src/models/post-comment.ts
@@ -0,0 +1,5 @@
+import { Guid } from 'guid-typescript';
+
+export class PostComment {
+ public id: Guid;
+}
diff --git a/src/models/post.ts b/src/models/post.ts
new file mode 100644
index 0000000..8e58bea
--- /dev/null
+++ b/src/models/post.ts
@@ -0,0 +1,81 @@
+import { Guid } from 'guid-typescript';
+import { Comment } from './comment';
+import { PostComment } from './post-comment';
+
+export class Post {
+ private _postId: Guid;
+ private _creatorFirstName: string;
+ private _creatorLastName: string;
+ private _creatorUsername: string;
+ private _message: string;
+ private _timeCreated: Date;
+ private _comments: PostComment[];
+ private _fileURLs: string[];
+
+ constructor(postId: Guid, creatorFirstName: string, creatorLastName: string, creatorUsername: string, message: string, timeCreated: Date, comments: PostComment[], fileURLs: string[]) {
+ this.postId = postId;
+ this.creatorFirstName = creatorFirstName;
+ this.creatorLastName = creatorLastName;
+ this.creatorUsername = creatorUsername;
+ this.message = message;
+ this.timeCreated = timeCreated;
+ this.comments = comments;
+ this.fileURLs = fileURLs;
+ }
+
+ public get postId(): Guid {
+ return this._postId;
+ }
+ public set postId(v: Guid) {
+ this._postId = v;
+ }
+
+ public get creatorFirstName(): string {
+ return this._creatorFirstName;
+ }
+ public set creatorFirstName(v: string) {
+ this._creatorFirstName = v;
+ }
+
+ public get creatorLastName(): string {
+ return this._creatorLastName;
+ }
+ public set creatorLastName(v: string) {
+ this._creatorLastName = v;
+ }
+
+ public get creatorUsername(): string {
+ return this._creatorUsername;
+ }
+ public set creatorUsername(v: string) {
+ this._creatorUsername = v;
+ }
+
+ public get message(): string {
+ return this._message;
+ }
+ public set message(v: string) {
+ this._message = v;
+ }
+
+ public get timeCreated(): Date {
+ return this._timeCreated;
+ }
+ public set timeCreated(v: Date) {
+ this._timeCreated = v;
+ }
+
+ public get comments(): PostComment[] {
+ return this._comments;
+ }
+ public set comments(v: PostComment[]) {
+ this._comments = v;
+ }
+
+ public get fileURLs(): string[] {
+ return this._fileURLs;
+ }
+ public set fileURLs(v: string[]) {
+ this._fileURLs = v;
+ }
+}
diff --git a/src/models/technology.ts b/src/models/technology.ts
new file mode 100644
index 0000000..1869d14
--- /dev/null
+++ b/src/models/technology.ts
@@ -0,0 +1,6 @@
+import { Guid } from 'guid-typescript';
+
+export class Technology {
+ public id: Guid;
+ public name: string;
+}
diff --git a/src/polyfills.ts b/src/polyfills.ts
new file mode 100644
index 0000000..9b8f300
--- /dev/null
+++ b/src/polyfills.ts
@@ -0,0 +1,63 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ * (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/src/reset.css b/src/reset.css
new file mode 100644
index 0000000..d9ac20b
--- /dev/null
+++ b/src/reset.css
@@ -0,0 +1,52 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v5.0.1 | 20191019
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, menu, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+main, menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, main, menu, nav, section {
+ display: block;
+}
+/* HTML5 hidden-attribute fix for newer browsers */
+*[hidden] {
+ display: none;
+}
+body {
+ line-height: 1;
+}
+menu, ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..eeb93fe
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,262 @@
+/* You can add global styles to this file, and also import other style files */
+@import "./reset.css";
+
+:root {
+ --bg-color: white;
+ --focus-color: forestgreen;
+ --card-bg: white;
+ --success: forestgreen;
+ --failure: indianred;
+}
+
+html, body {
+ height: 100%;
+ margin: 0;
+}
+body {
+ font: 21px sans-serif !important;
+ background-color: var(--bg-color);
+}
+
+input:focus, button:focus {
+ outline: 0;
+}
+
+#content { /* Used for the login and register pages */
+ height: 100%;
+ max-width: 20em;
+ box-sizing: border-box;
+ border: .5em solid var(--bg-color);
+
+ margin: 0 auto;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+
+.rounded-border {
+ border: 2px solid black;
+ border-radius: .6em;
+ padding: .4em;
+}
+
+.round-image {
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.title {
+ font-size: 2em;
+ font-weight: bold;
+}
+
+.error {
+ color: red;
+}
+
+.scroll-standalone {
+ width: 100%;
+ max-height: 100%;
+ overflow-y: auto;
+}
+
+ /* Hide scrollbar for Chrome, Safari and Opera */
+.scroll-standalone::-webkit-scrollbar {
+ display: none;
+}
+
+ /* Hide scrollbar for IE, Edge and Firefox */
+.scroll-standalone {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+}
+
+.user-language, .user-technology {
+ border-radius: .4em;
+ background-color: lightgrey;
+ padding: .26em;
+ margin: .1em .2em;
+ width: fit-content;
+}
+
+/* Inputs, the type found in login and register */
+
+.input-selection {
+ position: relative;
+ margin-top: .7em;
+}
+
+ /* Don't show the placeholder when the label is on top
+ */
+.input-selection .input-field::-webkit-input-placeholder {
+ opacity: 0;
+}
+
+.input-field {
+ width: 100%;
+ background-color: var(--bg-color);
+
+ border: 0;
+ border-bottom: 1px solid grey;
+ box-sizing: border-box;
+
+ margin-bottom: .5em;
+ padding: .4em;
+ padding-left: 0;
+
+ font-size: inherit;
+}
+
+.input-field-label {
+ width: inherit;
+ height: inherit;
+ position: absolute;
+ left: 0;
+
+ margin-top: .4em;
+ color: grey;
+}
+
+ /* When hovering, typing or having typed something in an input,
+ * make the label smaller, color it and then move it up
+ */
+.input-selection:hover > .input-field-label ,
+.input-selection > input:not(:placeholder-shown) + .input-field-label ,
+.input-selection > input:focus + .input-field-label {
+ font-size: .7em;
+ color: var(--focus-color);
+ transform: translate(0, -1.2em);
+}
+
+ /* Show the placeholder, when you've hovered or
+ * focused (typing in) on the input-field
+ */
+.input-selection:hover > .input-field::-webkit-input-placeholder,
+.input-selection > .input-field:focus::-webkit-input-placeholder {
+ opacity: 1;
+}
+
+ /* Make the underline thicker and change it's and the cursors's
+ * color when hovered or focused (typing in) on the input-field
+ */
+.input-selection:hover > .input-field,
+.input-field:focus {
+ border-color: var(--focus-color) !important;
+ caret-color: var(--focus-color);
+ border-width: 2px !important;
+ margin-top: -1px !important;
+}
+
+
+/* Input errors */
+
+.input-errors {
+ margin-top: -.8em;
+ font-size: .7em;
+
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+ /* Move the errors above the input when
+ * using the site on a small screen and
+ * add some space for them above the input
+ */
+@media screen and (max-width: 350px) {
+ .input-errors {
+ margin-top: -1.8em;
+ }
+ .input-selection {
+ margin-top: 1.6em;
+ }
+}
+
+.input-errors > .error {
+ margin-left: .3em;
+}
+
+.input-field:focus ~ .input-errors > .error {
+ opacity: 1 !important;
+}
+
+.input-field:placeholder-shown ~ .input-errors > .error {
+ opacity: 0;
+}
+
+/* Submit button */
+
+.submit-btn {
+ width: 100%;
+ color: white;
+ background-color: black;
+
+ border: 2px solid black;
+ border-radius: .4em;
+ box-sizing: border-box;
+
+ font-size: .8em;
+ text-align: center;
+
+ margin-top: .5em;
+ padding: .3em;
+}
+
+.submit-btn:hover {
+ cursor: pointer;
+ color: var(--focus-color);
+ background-color: white;
+ border-color: var(--focus-color) !important;
+}
+
+.submit-btn:active {
+ transition: 0s;
+ transform: scale(.9);
+}
+
+.delete-btn:hover {
+ color: indianred;
+ border-color: indianred !important;
+}
+
+/* Form attachments (the ones that are shown while creating and editing a post) */
+
+.form-attachments {
+ display: flex;
+ flex-wrap: wrap;
+ color: gray;
+ font-size: .75em;
+ margin: 0 .3em;
+}
+
+.form-attachment {
+ border: 2px solid black;
+ border-radius: .6em;
+ margin-top: .2em;
+ margin-right: .2em;
+ padding: .2em;
+ display: flex;
+ align-items: center;
+}
+
+.form-attachment:last-child {
+ margin-right: 0;
+}
+
+.remove-form-attachment {
+ font-size: .9em;
+ color: var(--failure);
+ background-color: white;
+ border-radius: .2em;
+ margin: 0 .2em;
+ padding: .2em;
+}
+
+.remove-form-attachment:hover {
+ color: white;
+ background-color: var(--failure);
+ cursor: pointer;
+}
+
diff --git a/src/theme.scss b/src/theme.scss
new file mode 100644
index 0000000..a87ae45
--- /dev/null
+++ b/src/theme.scss
@@ -0,0 +1,14 @@
+// Import theming functions
+@import '~@angular/material/theming';
+@import './styles.css';
+@include mat-core();
+
+// Custom Angular theme
+
+// $my-custom-primary: mat-palette($mat-deep-purple);
+// $my-custom-accent: mat-palette($mat-pink, 100, 500, A100);
+// $my-custom-warn: mat-palette($mat-lime);
+
+// $my-custom-theme: mat-light-theme($my-custom-primary, $my-custom-accent, $my-custom-warn);
+
+// @include angular-material-theme($my-custom-theme); \ No newline at end of file