aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKamen Mladenov <kamen.d.mladenov@protonmail.com>2021-04-09 19:55:59 +0300
committerGitHub <noreply@github.com>2021-04-09 19:55:59 +0300
commitf849e37ccdd6fd48f83119a3b3b65cdd8b765dc3 (patch)
tree83b88a773bb7dc053bb3aced35bce302264ec925 /src
parentbcd88af53b1a920d728ec98b45daa9ac2e2c0917 (diff)
parentc13889759d70687de6833c505221c203f65fedb8 (diff)
downloadDevHive-Angular-f849e37ccdd6fd48f83119a3b3b65cdd8b765dc3.tar
DevHive-Angular-f849e37ccdd6fd48f83119a3b3b65cdd8b765dc3.tar.gz
DevHive-Angular-f849e37ccdd6fd48f83119a3b3b65cdd8b765dc3.zip
Merge pull request #7 from Team-Kaleidoscope/devHEADv0.2main
Second Stage: Complete
Diffstat (limited to 'src')
-rw-r--r--src/app/app-constants.module.ts5
-rw-r--r--src/app/app-routing.module.ts4
-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.module.ts16
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.css45
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.html187
-rw-r--r--src/app/components/admin-panel-page/admin-panel-page.component.ts143
-rw-r--r--src/app/components/comment-page/comment-page.component.css27
-rw-r--r--src/app/components/comment-page/comment-page.component.html28
-rw-r--r--src/app/components/comment-page/comment-page.component.ts74
-rw-r--r--src/app/components/comment/comment.component.css53
-rw-r--r--src/app/components/comment/comment.component.html63
-rw-r--r--src/app/components/comment/comment.component.ts98
-rw-r--r--src/app/components/error-bar/error-bar.component.css2
-rw-r--r--src/app/components/error-bar/error-bar.component.ts18
-rw-r--r--src/app/components/feed/feed.component.css176
-rw-r--r--src/app/components/feed/feed.component.html78
-rw-r--r--src/app/components/feed/feed.component.ts31
-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.html4
-rw-r--r--src/app/components/login/login.component.css32
-rw-r--r--src/app/components/login/login.component.html38
-rw-r--r--src/app/components/login/login.component.ts23
-rw-r--r--src/app/components/navbar/navbar.component.css38
-rw-r--r--src/app/components/navbar/navbar.component.html45
-rw-r--r--src/app/components/navbar/navbar.component.ts63
-rw-r--r--src/app/components/not-found/not-found.component.html26
-rw-r--r--src/app/components/post-attachment/post-attachment.component.css56
-rw-r--r--src/app/components/post-attachment/post-attachment.component.html26
-rw-r--r--src/app/components/post-attachment/post-attachment.component.ts7
-rw-r--r--src/app/components/post-page/post-page.component.css63
-rw-r--r--src/app/components/post-page/post-page.component.html44
-rw-r--r--src/app/components/post-page/post-page.component.ts110
-rw-r--r--src/app/components/post/post.component.css127
-rw-r--r--src/app/components/post/post.component.html121
-rw-r--r--src/app/components/post/post.component.ts210
-rw-r--r--src/app/components/profile-settings/profile-settings.component.css122
-rw-r--r--src/app/components/profile-settings/profile-settings.component.html213
-rw-r--r--src/app/components/profile-settings/profile-settings.component.ts204
-rw-r--r--src/app/components/profile/profile.component.css110
-rw-r--r--src/app/components/profile/profile.component.html97
-rw-r--r--src/app/components/profile/profile.component.ts111
-rw-r--r--src/app/components/register/register.component.css40
-rw-r--r--src/app/components/register/register.component.html72
-rw-r--r--src/app/components/register/register.component.ts36
-rw-r--r--src/app/components/success-bar/success-bar.component.css1
-rw-r--r--src/app/services/comment.service.ts8
-rw-r--r--src/app/services/friend.service.ts44
-rw-r--r--src/app/services/language.service.ts4
-rw-r--r--src/app/services/post.service.ts6
-rw-r--r--src/app/services/profile-picture.service.ts32
-rw-r--r--src/app/services/rating.service.ts86
-rw-r--r--src/app/services/technology.service.ts2
-rw-r--r--src/app/services/user.service.ts54
-rw-r--r--src/assets/icons/tabler-icon-calendar-time.svg87
-rw-r--r--src/assets/icons/tabler-icon-chevron-down.svg65
-rw-r--r--src/assets/icons/tabler-icon-chevron-up.svg65
-rw-r--r--src/assets/icons/tabler-icon-edit.svg76
-rw-r--r--src/assets/icons/tabler-icon-eye-off.svg76
-rw-r--r--src/assets/icons/tabler-icon-eye.svg71
-rw-r--r--src/assets/icons/tabler-icon-file.svg69
-rw-r--r--src/assets/icons/tabler-icon-home.svg73
-rw-r--r--src/assets/icons/tabler-icon-link.svg69
-rw-r--r--src/assets/icons/tabler-icon-login.svg69
-rw-r--r--src/assets/icons/tabler-icon-logout.svg69
-rw-r--r--src/assets/icons/tabler-icon-message-2.svg79
-rw-r--r--src/assets/icons/tabler-icon-message-circle.svg86
-rw-r--r--src/assets/icons/tabler-icon-paperclip.svg65
-rw-r--r--src/assets/icons/tabler-icon-photo.svg84
-rw-r--r--src/assets/icons/tabler-icon-search.svg74
-rw-r--r--src/assets/icons/tabler-icon-settings.svg71
-rw-r--r--src/assets/icons/tabler-icon-trash.svg90
-rw-r--r--src/assets/icons/tabler-icon-trending-up.svg69
-rw-r--r--src/assets/icons/tabler-icon-user.svg71
-rw-r--r--src/assets/icons/tabler-icon-x.svg75
-rw-r--r--src/assets/images/comment.pngbin31366 -> 0 bytes
-rw-r--r--src/assets/images/feed/chat-pic.pngbin7634 -> 0 bytes
-rw-r--r--src/assets/images/feed/profile-pic.pngbin7870 -> 0 bytes
-rw-r--r--src/assets/images/paper-clip.pngbin2923 -> 0 bytes
-rw-r--r--src/interfaces/api-error.ts6
-rw-r--r--src/models/comment.model.ts (renamed from src/models/comment.ts)0
-rw-r--r--src/models/identity/friend.model.ts (renamed from src/models/identity/friend.ts)0
-rw-r--r--src/models/identity/role.model.ts (renamed from src/models/identity/role.ts)0
-rw-r--r--src/models/identity/user.model.ts (renamed from src/models/identity/user.ts)8
-rw-r--r--src/models/language.model.ts (renamed from src/models/language.ts)0
-rw-r--r--src/models/post-comment.model.ts (renamed from src/models/post-comment.ts)0
-rw-r--r--src/models/post.model.ts (renamed from src/models/post.ts)15
-rw-r--r--src/models/technology.model.ts (renamed from src/models/technology.ts)0
-rw-r--r--src/styles.css530
-rw-r--r--src/theme.scss12
95 files changed, 3532 insertions, 1988 deletions
diff --git a/src/app/app-constants.module.ts b/src/app/app-constants.module.ts
index d72af53..67091d9 100644
--- a/src/app/app-constants.module.ts
+++ b/src/app/app-constants.module.ts
@@ -2,6 +2,8 @@ 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_PROFILE_PICTURE_URL = AppConstants.BASE_API_URL + '/ProfilePicture';
+ public static API_FRIENDS_URL = AppConstants.BASE_API_URL + '/Friends';
public static API_USER_LOGIN_URL = AppConstants.API_USER_URL + '/login';
public static API_USER_REGISTER_URL = AppConstants.API_USER_URL + '/register';
@@ -9,11 +11,12 @@ export class AppConstants {
public static API_TECHNOLOGY_URL = AppConstants.BASE_API_URL + '/Technology';
public static API_POST_URL = AppConstants.BASE_API_URL + '/Post';
+ public static API_RATING_URL = AppConstants.BASE_API_URL + '/Rating';
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 FALLBACK_PROFILE_ICON = 'assets/icons/tabler-icon-user.svg';
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
index 0d83079..b319abd 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -7,8 +7,8 @@ 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';
+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 },
diff --git a/src/app/app-shell/app-shell.component.css b/src/app/app-shell/app-shell.component.css
deleted file mode 100644
index e69de29..0000000
--- a/src/app/app-shell/app-shell.component.css
+++ /dev/null
diff --git a/src/app/app-shell/app-shell.component.html b/src/app/app-shell/app-shell.component.html
deleted file mode 100644
index b5f713a..0000000
--- a/src/app/app-shell/app-shell.component.html
+++ /dev/null
@@ -1 +0,0 @@
-<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
deleted file mode 100644
index 2c4fabc..0000000
--- a/src/app/app-shell/app-shell.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index f1bca89..0000000
--- a/src/app/app-shell/app-shell.component.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-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.module.ts b/src/app/app.module.ts
index 1bf44ad..39f8ab3 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -2,10 +2,8 @@ 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 { ClipboardModule } from 'ngx-clipboard';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -22,9 +20,10 @@ import { SuccessBarComponent } from './components/success-bar/success-bar.compon
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';
+import { NavbarComponent } from './components/navbar/navbar.component';
+import { CommentPageComponent } from './components/comment-page/comment-page.component';
@NgModule({
declarations: [
@@ -42,19 +41,18 @@ import { RouterModule } from '@angular/router';
PostPageComponent,
AdminPanelPageComponent,
CommentComponent,
+ PostAttachmentComponent,
+ NavbarComponent,
CommentPageComponent,
- PostAttachmentComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
AppRoutingModule,
ReactiveFormsModule,
BrowserAnimationsModule,
- MatFormFieldModule,
- MatInputModule,
- MatButtonModule,
HttpClientModule,
- RouterModule
+ RouterModule,
+ ClipboardModule
],
providers: [],
bootstrap: [AppComponent]
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
index 1f98e20..ec7aa5d 100644
--- a/src/app/components/admin-panel-page/admin-panel-page.component.css
+++ b/src/app/components/admin-panel-page/admin-panel-page.component.css
@@ -1,42 +1,5 @@
-#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;
+.sec-info {
+ width: fit-content;
+ background-color: #424242;
+ margin: 0 0.3em 0.3em 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
index 980f12c..1074e51 100644
--- a/src/app/components/admin-panel-page/admin-panel-page.component.html
+++ b/src/app/components/admin-panel-page/admin-panel-page.component.html
@@ -1,85 +1,118 @@
-<!-- <app&#45;loading *ngIf="!dataArrived"></app&#45;loading> -->
+<app-navbar></app-navbar>
-<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>
+<app-loading *ngIf="!dataArrived"></app-loading>
- <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 }}
+<main class="scroll-standalone under-navbar centered-content flex-col" *ngIf="dataArrived">
+ <app-success-bar></app-success-bar>
+ <app-error-bar></app-error-bar>
+
+ <div class="card width-full">
+ <section>
+ <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect" (click)="toggleLanguages()">
+ ▼ Edit Languages ▼
+ </button>
+ <form class="flex-col margin-top-bot-dot3" [formGroup]="languageForm" (ngSubmit)="submitLanguages()" *ngIf="showLanguages">
+ <label class="fg-focus">
+ Create language:
+ </label>
+ <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="languageCreate" placeholder="New language name">
+
+ <label class="fg-focus">
+ Update language:
+ </label>
+ <div class="flex-row flexible-children">
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only margin-right-dot2" formControlName="updateLanguageOldName" placeholder="Old language name">
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="updateLanguageNewName" placeholder="New language 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 }}
+ <label class="fg-focus">
+ Delete language:
+ </label>
+ <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteLanguageName" placeholder="Language name">
+
+ <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit">
+ Modify languages
+ </button>
+ <section class="border-faded-slim padding-dot4 margin-top-dot4">
+ <div class="none-message" *ngIf="availableLanguages.length === 0">
+ No languages in database!
+ </div>
+ <div *ngIf="availableLanguages.length > 0">
+ Available languages:
+ </div>
+ <div class="flex-row margin-top-dot4">
+ <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let lang of availableLanguages">
+ {{ lang.name }}
+ </div>
+ </div>
+ </section>
+ </form>
+ </section>
+ <section>
+ <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleTechnologies()">
+ ▼ Edit Technologies ▼
+ </button>
+ <form class="flex-col margin-top-bot-dot3" [formGroup]="technologyForm" (ngSubmit)="submitTechnologies()" *ngIf="showTechnologies">
+ <label class="fg-focus">
+ Create technology:
+ </label>
+ <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="technologyCreate" placeholder="New technology name">
+
+ <label class="fg-focus">
+ Update technology:
+ </label>
+ <div class="flex-row flexible-children">
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only margin-right-dot2" formControlName="updateTechnologyOldName" placeholder="Old technology name">
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="updateTechnologyNewName" placeholder="New technology 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>
+ <label class="fg-focus">
+ Delete technology:
+ </label>
+ <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteTechnologyName" placeholder="Technology name">
+
+ <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit">
+ Modify technology
+ </button>
+ <section class="border-faded-slim padding-dot4 margin-top-dot4">
+ <div class="none-message" *ngIf="availableTechnologies.length === 0">
+ No technologies in database!
+ </div>
+ <div *ngIf="availableTechnologies.length > 0">
+ Available technologies:
+ </div>
+ <div class="flex-row margin-top-dot4">
+ <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let tech of availableTechnologies">
+ {{ tech.name }}
+ </div>
+ </div>
+ </section>
+ </form>
+ </section>
+ <section>
+ <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleDeletions()">
+ ▼ Deletions ▼
+ </button>
+ <form class="flex-col margin-top-bot-dot3" [formGroup]="deleteForm" (ngSubmit)="submitDelete()" *ngIf="showDeletions">
+ <label class="fg-focus">
+ Delete user by Id:
+ </label>
+ <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteUser" placeholder="User Id">
+
+ <label class="fg-focus">
+ Delete post by Id:
+ </label>
+ <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deletePost" placeholder="Post Id">
+
+ <label class="fg-focus">
+ Delete comment by Id:
+ </label>
+ <input type="text" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="deleteComment" placeholder="Comment Id">
- <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>
+ <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit">
+ Delete
+ </button>
+ </form>
+ </section>
</div>
-</div>
+</main>
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
index 99c0721..d1070b5 100644
--- a/src/app/components/admin-panel-page/admin-panel-page.component.ts
+++ b/src/app/components/admin-panel-page/admin-panel-page.component.ts
@@ -11,9 +11,9 @@ 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 { User } from 'src/models/identity/user.model';
+import { Language } from 'src/models/language.model';
+import { Technology } from 'src/models/technology.model';
import { ErrorBarComponent } from '../error-bar/error-bar.component';
import { SuccessBarComponent } from '../success-bar/success-bar.component';
@@ -46,17 +46,17 @@ export class AdminPanelPageComponent implements OnInit {
return;
}
- this._userService.getUserFromSessionStorageRequest().subscribe(
- (result: object) => {
+ this._userService.getUserFromSessionStorageRequest().subscribe({
+ next: (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) => {
+ error: () => {
this._router.navigate(['/login']);
}
- );
+ });
this.languageForm = this._fb.group({
languageCreate: new FormControl(''),
@@ -65,9 +65,11 @@ export class AdminPanelPageComponent implements OnInit {
deleteLanguageName: new FormControl('')
});
- this.languageForm.valueChanges.subscribe(() => {
- this._successBar?.hideMsg();
- this._errorBar?.hideError();
+ this.languageForm.valueChanges.subscribe({
+ next: () => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ }
});
this.technologyForm = this._fb.group({
@@ -77,9 +79,11 @@ export class AdminPanelPageComponent implements OnInit {
deleteTechnologyName: new FormControl('')
});
- this.technologyForm.valueChanges.subscribe(() => {
- this._successBar?.hideMsg();
- this._errorBar?.hideError();
+ this.technologyForm.valueChanges.subscribe({
+ next: () => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ }
});
this.deleteForm = this._fb.group({
@@ -88,15 +92,35 @@ export class AdminPanelPageComponent implements OnInit {
deleteComment: new FormControl('')
});
- this.deleteForm.valueChanges.subscribe(() => {
- this._successBar?.hideMsg();
- this._errorBar?.hideError();
+ this.deleteForm.valueChanges.subscribe({
+ next: () => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ }
});
this.loadAvailableLanguages();
this.loadAvailableTechnologies();
}
+ private loadAvailableLanguages(): void {
+ this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe({
+ next: (result: object) => {
+ this.availableLanguages = result as Language[];
+ this.dataArrived = true;
+ }
+ });
+ }
+
+ private loadAvailableTechnologies(): void {
+ this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe({
+ next: (result: object) => {
+ this.availableTechnologies = result as Technology[];
+ this.dataArrived = true;
+ }
+ });
+ }
+
// Navigation
backToProfile(): void {
@@ -128,14 +152,14 @@ export class AdminPanelPageComponent implements OnInit {
const languageCreate: string = this.languageForm.get('languageCreate')?.value;
if (languageCreate !== '' && languageCreate !== null) {
- this._languageService.createLanguageWithSessionStorageRequest(languageCreate.trim()).subscribe(
- (result: object) => {
+ this._languageService.createLanguageWithSessionStorageRequest(languageCreate.trim()).subscribe({
+ next: () => {
this.languageModifiedSuccess('Successfully updated languages!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -146,14 +170,14 @@ export class AdminPanelPageComponent implements OnInit {
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._languageService.putLanguageWithSessionStorageRequest(langId, updateLanguageNewName.trim()).subscribe({
+ next: () => {
this.languageModifiedSuccess('Successfully updated languages!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -163,14 +187,14 @@ export class AdminPanelPageComponent implements OnInit {
if (deleteLanguageName !== '' && deleteLanguageName !== null) {
const langId = this.availableLanguages.filter(x => x.name === deleteLanguageName.trim())[0].id;
- this._languageService.deleteLanguageWithSessionStorageRequest(langId).subscribe(
- (result: object) => {
+ this._languageService.deleteLanguageWithSessionStorageRequest(langId).subscribe({
+ next: () => {
this.languageModifiedSuccess('Successfully deleted language!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -180,13 +204,6 @@ export class AdminPanelPageComponent implements OnInit {
this.loadAvailableLanguages();
}
- private loadAvailableLanguages(): void {
- this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe(
- (result: object) => {
- this.availableLanguages = result as Language[];
- }
- );
- }
// Technology modifying
@@ -204,14 +221,14 @@ export class AdminPanelPageComponent implements OnInit {
const technologyCreate: string = this.technologyForm.get('technologyCreate')?.value;
if (technologyCreate !== '' && technologyCreate !== null) {
- this._technologyService.createTechnologyWithSessionStorageRequest(technologyCreate.trim()).subscribe(
- (result: object) => {
+ this._technologyService.createTechnologyWithSessionStorageRequest(technologyCreate.trim()).subscribe({
+ next: () => {
this.technologyModifiedSuccess('Successfully updated technologies!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -222,14 +239,14 @@ export class AdminPanelPageComponent implements OnInit {
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._technologyService.putTechnologyWithSessionStorageRequest(techId, updateTechnologyNewName.trim()).subscribe({
+ next: () => {
this.technologyModifiedSuccess('Successfully updated technologies!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -239,14 +256,14 @@ export class AdminPanelPageComponent implements OnInit {
if (deleteTechnologyName !== '' && deleteTechnologyName !== null) {
const techId = this.availableTechnologies.filter(x => x.name === deleteTechnologyName.trim())[0].id;
- this._technologyService.deleteTechnologyWithSessionStorageRequest(techId).subscribe(
- (result: object) => {
+ this._technologyService.deleteTechnologyWithSessionStorageRequest(techId).subscribe({
+ next: () => {
this.technologyModifiedSuccess('Successfully deleted technology!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -256,14 +273,6 @@ export class AdminPanelPageComponent implements OnInit {
this.loadAvailableTechnologies();
}
- private loadAvailableTechnologies(): void {
- this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe(
- (result: object) => {
- this.availableTechnologies = result as Technology[];
- }
- );
- }
-
// Deletions
toggleDeletions(): void {
@@ -282,14 +291,14 @@ export class AdminPanelPageComponent implements OnInit {
if (deleteUser !== '' && deleteUser !== null) {
const userId: Guid = Guid.parse(deleteUser);
- this._userService.deleteUserRequest(userId, this._tokenService.getTokenFromSessionStorage()).subscribe(
- (result: object) => {
+ this._userService.deleteUserRequest(userId, this._tokenService.getTokenFromSessionStorage()).subscribe({
+ next: () => {
this.deletionSuccess('Successfully deleted user!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -299,14 +308,14 @@ export class AdminPanelPageComponent implements OnInit {
if (deletePost !== '' && deletePost !== null) {
const postId: Guid = Guid.parse(deletePost);
- this._postService.deletePostRequest(postId, this._tokenService.getTokenFromSessionStorage()).subscribe(
- (result: object) => {
+ this._postService.deletePostRequest(postId, this._tokenService.getTokenFromSessionStorage()).subscribe({
+ next: () => {
this.deletionSuccess('Successfully deleted user!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
@@ -316,14 +325,14 @@ export class AdminPanelPageComponent implements OnInit {
if (deleteComment !== '' && deleteComment !== null) {
const commentId: Guid = Guid.parse(deleteComment);
- this._commentService.deleteCommentWithSessionStorage(commentId).subscribe(
- (result: object) => {
+ this._commentService.deleteCommentWithSessionStorage(commentId).subscribe({
+ next: () => {
this.deletionSuccess('Successfully deleted comment!');
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
}
diff --git a/src/app/components/comment-page/comment-page.component.css b/src/app/components/comment-page/comment-page.component.css
index b886bc1..e69de29 100644
--- a/src/app/components/comment-page/comment-page.component.css
+++ b/src/app/components/comment-page/comment-page.component.css
@@ -1,27 +0,0 @@
-#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
index 2d110d6..8a2ffe1 100644
--- a/src/app/components/comment-page/comment-page.component.html
+++ b/src/app/components/comment-page/comment-page.component.html
@@ -1,14 +1,18 @@
-<app-loading *ngIf="!loaded"></app-loading>
+<app-navbar></app-navbar>
-<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>
+<app-loading *ngIf="!dataArrived"></app-loading>
+
+<main class="centered-content scroll-standalone under-navbar flex-col" *ngIf="dataArrived">
+ <app-post [paramId]="postId.toString()"></app-post>
+ <hr class="card-hr">
+ <section class="card flex-col width-full margin-0-top">
+ <button class="fg-focus border-faded-slim padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="goToPostPage()">
+ Show all comments
+ </button>
+ </section>
+ <div class="text-centered">
+ ...
</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>
+
+ <app-comment [paramId]="commentId.toString()"></app-comment>
+</main>
diff --git a/src/app/components/comment-page/comment-page.component.ts b/src/app/components/comment-page/comment-page.component.ts
index bd4cfe5..4281e1c 100644
--- a/src/app/components/comment-page/comment-page.component.ts
+++ b/src/app/components/comment-page/comment-page.component.ts
@@ -1,12 +1,8 @@
-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',
@@ -15,77 +11,31 @@ import { Comment } from 'src/models/comment';
})
export class CommentPageComponent implements OnInit {
private _title = 'Comment';
- public loaded = false;
- public loggedIn = false;
- public editable = false;
- public editingComment = false;
+ public dataArrived = false;
+ public postId: Guid;
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){
+ constructor(private _titleService: Title, private _router: Router, private _commentService: CommentService) {
this._titleService.setTitle(this._title);
}
ngOnInit(): void {
- this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== '';
this.commentId = Guid.parse(this._router.url.substring(9));
+ this.postId = Guid.createEmpty();
- // 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;
+ this._commentService.getCommentRequest(this.commentId).subscribe({
+ next: (result: object) => {
+ this.postId = Object.values(result)[1];
+
+ this.dataArrived = true;
},
- (err: HttpErrorResponse) => {
+ error: () => {
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]);
+ goToPostPage(): void {
+ this._router.navigate(['/post/' + this.postId]);
}
}
diff --git a/src/app/components/comment/comment.component.css b/src/app/components/comment/comment.component.css
index d82f10e..48d9174 100644
--- a/src/app/components/comment/comment.component.css
+++ b/src/app/components/comment/comment.component.css
@@ -1,59 +1,30 @@
-.comment {
- display: flex;
- width: 100%;
-
- margin: .5em auto;
+.left-pane {
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;
+.author-picture {
+ width: 2.2em;
+ height: 2.2em;
}
/* Content */
.content {
- flex: 1;
+ padding: 0 var(--card-padding);
}
.message {
- margin: .3em 0;
- word-break: break-all;
+ word-break: break-word;
}
-.timestamp {
- font-size: .5em;
- color: gray;
+.comment-details {
+ margin-bottom: calc(var(--card-padding) / 1.5);
+ padding: 0.2em 0;
}
-.message:hover, .timestamp:hover {
- cursor: pointer;
+.comment-details > * {
+ margin-left: 1.1em;
}
+
diff --git a/src/app/components/comment/comment.component.html b/src/app/components/comment/comment.component.html
index 718e25c..9403ea2 100644
--- a/src/app/components/comment/comment.component.html
+++ b/src/app/components/comment/comment.component.html
@@ -1,23 +1,46 @@
<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()">
+<section class="card flex-row" [hidden]="loaded" (mouseleave)="resetShareBtn()">
+ <aside class="left-pane">
+ <img class="author-picture round-image hover-half-opacity" [src]="user.profilePictureURL" (click)="goToAuthorProfile()">
+ </aside>
+ <main class="content flexible">
+ <summary class="font-size-dot8 text-vertical-middle hover-half-opacity" (click)="goToAuthorProfile()">
+ <span>
+ {{ user.firstName }} {{ user.lastName }}
+ </span>
+ <span class="fg-faded">
+ @{{ user.userName }}
+ </span>
+ </summary>
+ <form [formGroup]="editCommentFormGroup" *ngIf="editingComment" (ngSubmit)="editComment()">
+ <textarea class="textarea-new-msg width-full border-faded-slim border-bottom-only padding-dot2 margin-bot-dot5" rows="1" formControlName="newCommentMessage" placeholder="What's on your mind?"></textarea>
+ <button type="submit" class="border-faded-slim width-full padding-dot2 lighter-hover click-effect border-radius-dot3 margin-bot-dot5">
+ Update Comment
+ </button>
+ </form>
+ <article class="message margin-top-bot-dot2" *ngIf="!editingComment">
{{ comment.message }}
- </div>
- <div class="timestamp" (click)="goToCommentPage()">
- {{ timeCreated }}
- </div>
- </div>
-</div>
+ </article>
+ <section class="comment-details flex-row flex-justify-end font-size-dot7 border-faded-slim border-bottom-only">
+ <time class="flex-row flex-center-align-items">
+ <img class="height-font" src="/assets/icons/tabler-icon-calendar-time.svg">&nbsp;
+ <span>
+ {{ timeCreated }}
+ </span>
+ </time>
+ </section>
+ <section class="flex-row justify-children-center align-children-center">
+ <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="toggleEditing()">
+ <img src="/assets/icons/tabler-icon-edit.svg">
+ </button>
+ <button #share ngxClipboard [cbContent]="getPostLink()" class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="showCopiedMessage()">
+ <img src="/assets/icons/tabler-icon-link.svg">
+ &nbsp;Share
+ </button>
+ <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="deleteComment()">
+ <img src="/assets/icons/tabler-icon-trash.svg">
+ </button>
+ </section>
+ </main>
+</section>
diff --git a/src/app/components/comment/comment.component.ts b/src/app/components/comment/comment.component.ts
index 5076769..2a54f92 100644
--- a/src/app/components/comment/comment.component.ts
+++ b/src/app/components/comment/comment.component.ts
@@ -1,54 +1,126 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
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 { UserService } from 'src/app/services/user.service';
-import { Comment } from 'src/models/comment';
-import { User } from 'src/models/identity/user';
+import { Comment } from 'src/models/comment.model';
+import { User } from 'src/models/identity/user.model';
@Component({
selector: 'app-comment',
templateUrl: './comment.component.html',
styleUrls: ['./comment.component.css']
})
-export class CommentComponent implements OnInit {
+export class CommentComponent implements OnInit, AfterViewInit {
public loaded = false;
+ public loggedInAuthor = false;
+ public editingComment = false;
public user: User;
public comment: Comment;
public timeCreated: string;
@Input() paramId: string;
+ public editCommentFormGroup: FormGroup;
+ @ViewChild('share') shareBtn: ElementRef;
+ private _defaultShareBtnInnerHTML: string;
+ private _linkShared = false;
- constructor(private _router: Router, private _commentService: CommentService, private _userService: UserService)
+ constructor(private _router: Router, private _commentService: CommentService, private _userService: UserService, private _tokenService: TokenService, private _fb: FormBuilder, private _elem: ElementRef, private _renderer: Renderer2)
{ }
ngOnInit(): void {
this.comment = this._commentService.getDefaultComment();
this.user = this._userService.getDefaultUser();
- this._commentService.getCommentRequest(Guid.parse(this.paramId)).subscribe(
- (result: object) => {
+ this._commentService.getCommentRequest(Guid.parse(this.paramId)).subscribe({
+ next: (result: object) => {
Object.assign(this.comment, result);
this.timeCreated = new Date(this.comment.timeCreated).toLocaleString('en-GB');
this.loadUser();
}
- );
+ });
+
+ this.editCommentFormGroup = this._fb.group({
+ newCommentMessage: new FormControl('')
+ });
}
private loadUser(): void {
- this._userService.getUserByUsernameRequest(this.comment.issuerUsername).subscribe(
- (result: object) => {
+ this._userService.getUserByUsernameRequest(this.comment.issuerUsername).subscribe({
+ next: (result: object) => {
Object.assign(this.user, result);
+
+ if (this._tokenService.getTokenFromSessionStorage() !== '') {
+ this.loggedInAuthor = this._tokenService.getUsernameFromSessionStorageToken() === this.comment.issuerUsername;
+ this.editCommentFormGroup.get('newCommentMessage')?.setValue(this.comment.message);
+ }
+
this.loaded = true;
}
- );
+ });
+ }
+
+ ngAfterViewInit(): void {
+ this._defaultShareBtnInnerHTML = this.shareBtn.nativeElement.innerHTML;
}
goToAuthorProfile(): void {
this._router.navigate(['/profile/' + this.comment.issuerUsername]);
}
- goToCommentPage(): void {
- this._router.navigate(['/comment/' + this.comment.commentId]);
+ toggleEditing(): void {
+ this.editingComment = !this.editingComment;
+ }
+
+ editComment(): void {
+ if (this._tokenService.getTokenFromSessionStorage() === '') {
+ this._router.navigate(['/login']);
+ return;
+ }
+
+ if (this.editingComment) {
+ const newMessage = this.editCommentFormGroup.get('newCommentMessage')?.value;
+
+ if (newMessage !== '' && newMessage !== this.comment.message) {
+ this._commentService.putCommentWithSessionStorageRequest(Guid.parse(this.paramId), this.comment.postId, newMessage).subscribe({
+ next: () => {
+ this.reloadPage();
+ }
+ });
+ }
+ }
+ this.editingComment = !this.editingComment;
+ }
+
+ deleteComment(): void {
+ this._commentService.deleteCommentWithSessionStorage(Guid.parse(this.paramId)).subscribe({
+ next: () => {
+ this.reloadPage();
+ }
+ });
+ }
+
+ private reloadPage(): void {
+ this._router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this._router.onSameUrlNavigation = 'reload';
+ this._router.navigate([this._router.url]);
+ }
+
+ resetShareBtn(): void {
+ if (this._linkShared) {
+ this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', this._defaultShareBtnInnerHTML);
+ this._linkShared = false;
+ }
+ }
+
+ showCopiedMessage(): void {
+ this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', 'Link copied to clipboard!');
+ this._linkShared = true;
+ }
+
+ getPostLink(): string {
+ return location.origin + '/comment/' + this.paramId;
}
}
diff --git a/src/app/components/error-bar/error-bar.component.css b/src/app/components/error-bar/error-bar.component.css
index 8f8edd9..880f205 100644
--- a/src/app/components/error-bar/error-bar.component.css
+++ b/src/app/components/error-bar/error-bar.component.css
@@ -5,6 +5,8 @@
color: white;
padding: .2em;
text-align: center;
+ word-wrap: break-word;
+ white-space: pre-wrap;
}
#error-bar:empty {
diff --git a/src/app/components/error-bar/error-bar.component.ts b/src/app/components/error-bar/error-bar.component.ts
index 111bac8..277552b 100644
--- a/src/app/components/error-bar/error-bar.component.ts
+++ b/src/app/components/error-bar/error-bar.component.ts
@@ -1,6 +1,5 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
-import { IApiError } from 'src/interfaces/api-error';
@Component({
selector: 'app-error-bar',
@@ -18,14 +17,15 @@ export class ErrorBarComponent implements OnInit {
}
showError(error: HttpErrorResponse): void {
- const test: IApiError = {
- type: '',
- title: 'Error!',
- status: 0,
- traceId: ''
- };
- Object.assign(test, error.error);
- this.errorMsg = test.title;
+ this.errorMsg = '';
+
+ const errors: string[][] = Object.values(Object.values(error.error)[0] as any);
+
+ for (const errorArr of errors) {
+ for (const errorMsg of errorArr) {
+ this.errorMsg += errorMsg + '\n';
+ }
+ }
}
hideError(): void {
diff --git a/src/app/components/feed/feed.component.css b/src/app/components/feed/feed.component.css
index cb155c6..295a379 100644
--- a/src/app/components/feed/feed.component.css
+++ b/src/app/components/feed/feed.component.css
@@ -1,179 +1,27 @@
-#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 */
+section:empty {
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;
+#create-post {
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;
+ margin-top: 1em;
}
-#file-upload:hover {
- cursor: pointer;
+#attachments-btns img, .file-button > input {
+ height: 1.4em;
+ width: 1.4em;
}
-#file-upload::-webkit-file-upload-button {
- visibility: hidden;
+.file-button {
+ position: relative;
}
-#attachment-img {
- height: 1.5em;
- width: 1.5em;
+.file-button > img {
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 */
+.file-button > input {
+ font-size: inherit;
+ color: transparent;
}
diff --git a/src/app/components/feed/feed.component.html b/src/app/components/feed/feed.component.html
index 1a03dcc..4832456 100644
--- a/src/app/components/feed/feed.component.html
+++ b/src/app/components/feed/feed.component.html
@@ -1,52 +1,38 @@
+<app-navbar></app-navbar>
+
<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 }}
+<main class="centered-content scroll-standalone under-navbar flex-col flex-justify-start" *ngIf="dataArrived">
+ <form id="create-post" class="card flex-col" [formGroup]="createPostFormGroup" (ngSubmit)="createPost()">
+ <textarea class="textarea-new-msg border-faded-slim border-bottom-only padding-dot2" rows="1" formControlName="newPostMessage" placeholder="What's on your mind?"></textarea>
+ <section class="flex-row flex-justify-start align-children-center padding-top-bot-dot5">
+ <div class="file-button hover-half-opacity click-effect">
+ <img src="/assets/icons/tabler-icon-paperclip.svg">
+ <input type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple>
</div>
- <div id="profile-bar-username">
- @{{ user.userName }}
+ </section>
+ <section class="flex-row bot-padding-dot6ger">
+ <div *ngFor="let file of files" class="form-attachment border-faded-slim flexible flex-row flex-no-wrap flex-center-align-items padding-dot2 margin-top-bot-dot2">
+ <div class="flexible">
+ {{ file.name ? file.name : 'Attachment' }}
+ </div>
+ <div class="flex-col hover-half-opacity border-radius-dot2 click-effect" (click)="removeAttachment(file.name)">
+ <img src="/assets/icons/tabler-icon-x.svg">
+ </div>
</div>
+ </section>
+ <button class="border-faded-slim padding-dot2 lighter-hover click-effect border-radius-dot3">
+ Post
+ </button>
+ </form>
+ <hr class="card-hr">
+ <section id="posts" (scroll)="onScroll($event)">
+ <div class="text-centered" *ngIf="posts.length === 0">
+ None of your friends have posted anything yet!<br>
+ Try refreshing your page!
</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 *ngFor="let friendPost of posts" class="post">
+ <app-post [paramId]="friendPost.postId.toString()"></app-post>
</div>
- </div>
-</div>
+ </section>
+</main>
diff --git a/src/app/components/feed/feed.component.ts b/src/app/components/feed/feed.component.ts
index b412b3c..4100e41 100644
--- a/src/app/components/feed/feed.component.ts
+++ b/src/app/components/feed/feed.component.ts
@@ -1,12 +1,11 @@
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 { User } from 'src/models/identity/user.model';
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 { Post } from 'src/models/post.model';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { PostService } from 'src/app/services/post.service';
import { TokenService } from 'src/app/services/token.service';
@@ -42,7 +41,7 @@ export class FeedComponent implements OnInit {
this.files = [];
const now = new Date();
- now.setHours(now.getHours() + 2); // accounting for eastern european timezone
+ now.setHours(now.getHours() + 3); // accounting for eastern european timezone
this._timeLoaded = now.toISOString();
this.createPostFormGroup = this._fb.group({
@@ -50,27 +49,27 @@ export class FeedComponent implements OnInit {
fileUpload: new FormControl('')
});
- this._userService.getUserFromSessionStorageRequest().subscribe(
- (res: object) => {
+ this._userService.getUserFromSessionStorageRequest().subscribe({
+ next: (res: object) => {
Object.assign(this.user, res);
this.loadFeed();
},
- (err: HttpErrorResponse) => {
+ error: () => {
this.logout();
}
- );
+ });
}
private loadFeed(): void {
- this._feedService.getUserFeedFromSessionStorageRequest(this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe(
- (result: object) => {
+ this._feedService.getUserFeedFromSessionStorageRequest(this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe({
+ next: (result: object) => {
this.posts.push(...Object.values(result)[0]);
this.finishUserLoading();
},
- (err) => {
+ error: () => {
this.finishUserLoading();
}
- );
+ });
}
private finishUserLoading(): void {
@@ -103,14 +102,14 @@ export class FeedComponent implements OnInit {
const postMessage = this.createPostFormGroup.get('newPostMessage')?.value;
this.dataArrived = false;
- this._postService.createPostWithSessionStorageRequest(postMessage, this.files).subscribe(
- (result: object) => {
+ this._postService.createPostWithSessionStorageRequest(postMessage, this.files).subscribe({
+ next: () => {
this.goToProfile();
},
- (err: HttpErrorResponse) => {
+ error: () => {
this.dataArrived = true;
}
- );
+ });
}
onScroll(event: any): void {
diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.css b/src/app/components/kaleidoscope/kaleidoscope.component.css
deleted file mode 100644
index e69de29..0000000
--- a/src/app/components/kaleidoscope/kaleidoscope.component.css
+++ /dev/null
diff --git a/src/app/components/kaleidoscope/kaleidoscope.component.html b/src/app/components/kaleidoscope/kaleidoscope.component.html
deleted file mode 100644
index 7392a21..0000000
--- a/src/app/components/kaleidoscope/kaleidoscope.component.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<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
deleted file mode 100644
index 1c5cda1..0000000
--- a/src/app/components/kaleidoscope/kaleidoscope.component.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-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.html b/src/app/components/loading/loading.component.html
index 8440f4e..bdc795b 100644
--- a/src/app/components/loading/loading.component.html
+++ b/src/app/components/loading/loading.component.html
@@ -1,3 +1,3 @@
-<div id="content">
+<main id="content">
Loading...
-</div>
+</main>
diff --git a/src/app/components/login/login.component.css b/src/app/components/login/login.component.css
index 766522e..e69de29 100644
--- a/src/app/components/login/login.component.css
+++ b/src/app/components/login/login.component.css
@@ -1,32 +0,0 @@
-* {
- 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
index 13f9bbf..95075c9 100644
--- a/src/app/components/login/login.component.html
+++ b/src/app/components/login/login.component.html
@@ -1,30 +1,28 @@
-<div id="content">
- <div class="title">Login</div>
+<main class="centered-content scroll-standalone flex-col flex-center-align-items flex-justify-center height-full">
+ <summary class="title card width-full margin-0 padding-dot2">Login</summary>
- <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>
+ <app-error-bar class="width-full margin-top-dot4"></app-error-bar>
+ <form class="width-full card padding-dot6" [formGroup]="loginUserFormGroup" (ngSubmit)="onSubmit()">
+ <section class="input-selection width-full">
+ <input type="text" placeholder="Username" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="username" required>
+ <label class="fancy-input-label width-full">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>
-
+ </section>
+ <section class="input-selection">
+ <input [type]="showingPassword ? 'text' : 'password'" placeholder="Password" class="fancy-input width-full border-faded-slim border-bottom-only padding-right-1dot5" formControlName="password" required>
+ <label class="fancy-input-label width-full">Password</label>
+ <button type="button" class="show-password-button hover-half-opacity click-effect" (click)="toggleShowPassword()">
+ <img [src]="showingPassword ? '/assets/icons/tabler-icon-eye-off.svg' : '/assets/icons/tabler-icon-eye.svg'">
+ </button>
<div class="input-errors">
<label *ngIf="loginUserFormGroup.get('password')?.errors?.required" class="error">*Required</label>
</div>
- </div>
+ </section>
- <hr>
- <button class="submit-btn" type="submit">Submit</button>
- <app-error-bar></app-error-bar>
+ <button class="border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full" type="submit">Submit</button>
</form>
- <button class="submit-btn redirect-to-register" (click)="onRedirectRegister()">New to DevHive? Register instead</button>
-</div>
+ <button class="fg-focus border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full margin-top-dot4" (click)="onRedirectRegister()">New to DevHive? Register instead</button>
+</main>
diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts
index c3fb79c..a0ce730 100644
--- a/src/app/components/login/login.component.ts
+++ b/src/app/components/login/login.component.ts
@@ -1,5 +1,5 @@
import { Component, OnInit, ViewChild } from '@angular/core';
-import { FormGroup, FormBuilder, Validators, FormControl, AbstractControl } from '@angular/forms';
+import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms';
import { Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { UserService } from 'src/app/services/user.service';
@@ -16,6 +16,7 @@ export class LoginComponent implements OnInit {
@ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent;
private _title = 'Login';
public loginUserFormGroup: FormGroup;
+ public showingPassword = false;
constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) {
this._titleService.setTitle(this._title);
@@ -32,28 +33,24 @@ export class LoginComponent implements OnInit {
});
}
+ toggleShowPassword(): void {
+ this.showingPassword = !this.showingPassword;
+ }
+
onSubmit(): void {
this._errorBar.hideError();
- this._userService.loginUserRequest(this.loginUserFormGroup).subscribe(
- (res: object) => {
+ this._userService.loginUserRequest(this.loginUserFormGroup).subscribe({
+ next: (res: object) => {
this._tokenService.setUserTokenToSessionStorage(res);
this._router.navigate(['/']);
},
- (err: HttpErrorResponse) => {
+ error: (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/navbar/navbar.component.css b/src/app/components/navbar/navbar.component.css
new file mode 100644
index 0000000..d02e928
--- /dev/null
+++ b/src/app/components/navbar/navbar.component.css
@@ -0,0 +1,38 @@
+#navbar {
+ height: var(--navbar-height);
+ width: 100%;
+ background-color: var(--card-bg);
+}
+
+#nav-contents img {
+ height: 1.9em;
+ width: 1.9em;
+}
+
+@media screen and (max-width: 30rem) {
+ #nav-username {
+ display: none;
+ }
+
+ #navbar {
+ font-size: 0.8em;
+ }
+}
+
+#nav-profile-picture {
+ padding: 0.1em;
+}
+
+#nav-profile-picture img {
+ height: 1.8em;
+ width: 1.8em;
+}
+
+.nav-item {
+ margin-left: 0.2em;
+ padding: 0 0.1em;
+}
+
+.nav-item:first-child {
+ margin-left: 0;
+}
diff --git a/src/app/components/navbar/navbar.component.html b/src/app/components/navbar/navbar.component.html
new file mode 100644
index 0000000..d05f6e1
--- /dev/null
+++ b/src/app/components/navbar/navbar.component.html
@@ -0,0 +1,45 @@
+<nav id="navbar">
+ <div id="nav-contents" class="centered-content flex-row padding-dot3 flex-center-align-items">
+ <div class="nav-item border-radius-dot2 flex-row flex-center-align-items light-hover hover-half-opacity click-effect" (click)="goToProfile()" *ngIf="loggedIn">
+ <div id="nav-profile-picture" class="flex-col">
+ <img class="round-image" [src]="user.profilePictureURL">
+ </div>
+ <div id="nav-username" class="font-size-dot9 flex-col">
+ <div class="padding-dot2">
+ @{{ user.userName }}
+ </div>
+ </div>
+ </div>
+ <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" (click)="goToFeed()" *ngIf="loggedIn">
+ <img src="/assets/icons/tabler-icon-home.svg">
+ </div>
+ <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" style="display: none">
+ <!-- Trending functionality isn't implemented yet! -->
+ <img src="/assets/icons/tabler-icon-trending-up.svg">
+ </div>
+ <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" style="display: none">
+ <!-- Chat functionality isn't implemented yet! -->
+ <img src="/assets/icons/tabler-icon-message-circle.svg">
+ </div>
+ <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" style="display: none">
+ <!-- Search functionality isn't implemented yet! -->
+ <img src="/assets/icons/tabler-icon-search.svg">
+ </div>
+ <div class="flexible">
+ <!-- This element serves as a spacer -->
+ </div>
+ <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" (click)="goToSettings()" *ngIf="loggedIn">
+ <img src="/assets/icons/tabler-icon-settings.svg">
+ </div>
+ <div class="nav-item flex-col border-radius-dot2 light-hover hover-half-opacity click-effect" (click)="logout()" *ngIf="loggedIn">
+ <img src="/assets/icons/tabler-icon-logout.svg">
+ </div>
+ <div class="nav-item flex-row flex-center-align-items border-radius-dot2 light-hover hover-half-opacity click-effect side-padding-dot3" (click)="goToLogin()" *ngIf="!loggedIn">
+ Login&nbsp;
+ <img src="/assets/icons/tabler-icon-login.svg">
+ </div>
+ <div class="flexible" *ngIf="!loggedIn">
+ <!-- This element serves as a spacer -->
+ </div>
+ </div>
+</nav>
diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts
new file mode 100644
index 0000000..14b8f52
--- /dev/null
+++ b/src/app/components/navbar/navbar.component.ts
@@ -0,0 +1,63 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { TokenService } from 'src/app/services/token.service';
+import { UserService } from 'src/app/services/user.service';
+import { User } from 'src/models/identity/user.model';
+
+@Component({
+ selector: 'app-navbar',
+ templateUrl: './navbar.component.html',
+ styleUrls: ['./navbar.component.css']
+})
+export class NavbarComponent implements OnInit {
+ public user: User;
+ public loggedIn: Boolean;
+
+ constructor(private _router: Router, private _userService: UserService, private _tokenService: TokenService)
+ { }
+
+ ngOnInit(): void {
+ this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== '';
+
+ this.user = this._userService.getDefaultUser();
+
+ if (this.loggedIn) {
+ this._userService.getUserFromSessionStorageRequest().subscribe({
+ next: (res: object) => {
+ Object.assign(this.user, res);
+ },
+ });
+ }
+ }
+
+ goToProfile(): void {
+ // Properly reload the page
+ // Needed because if you're on someone's profile and go to yours, angular won't refresh the page (with your info)
+ this._router.routeReuseStrategy.shouldReuseRoute = () => false;
+ this._router.onSameUrlNavigation = 'reload';
+
+ this._router.navigate(['/profile/' + this.user.userName]);
+ }
+
+ goToFeed(): void {
+ if (this.loggedIn) {
+ this._router.navigate(['/']);
+ }
+ else {
+ this.goToLogin();
+ }
+ }
+
+ goToSettings(): void {
+ this._router.navigate(['/profile/' + this.user.userName + '/settings']);
+ }
+
+ logout(): void {
+ this._tokenService.logoutUserFromSessionStorage();
+ this.goToLogin();
+ }
+
+ goToLogin(): void {
+ this._router.navigate(['/login']);
+ }
+}
diff --git a/src/app/components/not-found/not-found.component.html b/src/app/components/not-found/not-found.component.html
index 8394810..4d1165d 100644
--- a/src/app/components/not-found/not-found.component.html
+++ b/src/app/components/not-found/not-found.component.html
@@ -1,10 +1,18 @@
-<div id="content">
- <div class="title">
- Page not found!
+<main class="centered-content flex-col flex-center-align-items flex-justify-center height-full">
+ <div class="card width-full padding-dot6">
+ <summary class="title">
+ Page not found!
+ </summary>
+ <hr class="card-hr">
+ <nav class="flex-row flexible-children padding-dot2">
+ <button class="flex-row flex-justify-center flex-center-align-items border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 margin-right-dot2" (click)="backToFeed()">
+ <img src="/assets/icons/tabler-icon-home.svg">
+ &nbsp;Back to feed
+ </button>
+ <button class="flex-row flex-justify-center flex-center-align-items border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" (click)="backToLogin()">
+ <img src="/assets/icons/tabler-icon-login.svg">
+ &nbsp;Back to login
+ </button>
+ </nav>
</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>
+</main>
diff --git a/src/app/components/post-attachment/post-attachment.component.css b/src/app/components/post-attachment/post-attachment.component.css
index 572cc99..6cdab0b 100644
--- a/src/app/components/post-attachment/post-attachment.component.css
+++ b/src/app/components/post-attachment/post-attachment.component.css
@@ -1,32 +1,3 @@
-/* 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 {
@@ -36,9 +7,6 @@
width: 100vw;
height: 100vh;
background-color: #000000AF;
- display: flex;
- justify-content: center;
- align-items: center;
z-index: 2;
}
@@ -49,27 +17,7 @@
.attachment-download {
max-width: 420px !important;
- margin: 0 .4em;
+ background-color: var(--card-bg);
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;
+ color: inherit;
}
diff --git a/src/app/components/post-attachment/post-attachment.component.html b/src/app/components/post-attachment/post-attachment.component.html
index 4d381d1..e5e4b5a 100644
--- a/src/app/components/post-attachment/post-attachment.component.html
+++ b/src/app/components/post-attachment/post-attachment.component.html
@@ -1,18 +1,16 @@
-<div class="attachment">
- <div class="clickable" (click)="toggleShowFull()">
- <div class="attachment-name">
- {{ fileName }}
- </div>
- <div class="attachment-type">
- {{ fileType }}
- </div>
+<figure class="form-attachment border-faded-slim flexible flex-row flex-no-wrap padding-dot2 hover-half-opacity margin-top-bot-dot2" (click)="toggleShowFull()">
+ <div class="flex-col flex-justify-center margin-right-dot2" [ngSwitch]="fileType">
+ <img *ngSwitchCase="'img'" src="/assets/icons/tabler-icon-photo.svg">
+ <img *ngSwitchDefault src="/assets/icons/tabler-icon-file.svg">
</div>
-</div>
+ <summary class="flex-col flex-justify-center">
+ {{ fileName }}
+ </summary>
+</figure>
-<div class="show-full-attachment" *ngIf="showFull" (click)="toggleShowFull()">
+<div class="show-full-attachment flex-row flex-justify-center flex-center-align-items" *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>
+ <a class="attachment-download border-faded-slim padding-dot4 hover-half-opacity click-effect border-radius-dot3" *ngIf="!isImage" href="{{paramURL}}">
+ Download attachment
+ </a>
</div>
diff --git a/src/app/components/post-attachment/post-attachment.component.ts b/src/app/components/post-attachment/post-attachment.component.ts
index 1d00def..1aeca37 100644
--- a/src/app/components/post-attachment/post-attachment.component.ts
+++ b/src/app/components/post-attachment/post-attachment.component.ts
@@ -18,7 +18,12 @@ export class PostAttachmentComponent implements OnInit {
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';
+ if (this.fileType === 'img') {
+ this.fileName = this.paramURL.match(/(?!\/)+?[^\/]+?(?=\.)/g)?.pop() ?? 'Attachment';
+ }
+ else {
+ this.fileName = this.paramURL.match(/[^\/]+?$/g)?.pop() ?? 'Attachment';
+ }
}
toggleShowFull(): void {
diff --git a/src/app/components/post-page/post-page.component.css b/src/app/components/post-page/post-page.component.css
index 3eec851..7405240 100644
--- a/src/app/components/post-page/post-page.component.css
+++ b/src/app/components/post-page/post-page.component.css
@@ -1,62 +1,3 @@
-#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;
+main {
+ padding-top: 0.5em;
}
diff --git a/src/app/components/post-page/post-page.component.html b/src/app/components/post-page/post-page.component.html
index 8665865..cfca6c0 100644
--- a/src/app/components/post-page/post-page.component.html
+++ b/src/app/components/post-page/post-page.component.html
@@ -1,37 +1,23 @@
+<app-navbar></app-navbar>
+
<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>
+<main class="centered-content scroll-standalone under-navbar flex-col" *ngIf="dataArrived">
<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 class="card flex-col width-full margin-0-top" [formGroup]="addCommentFormGroup" (ngSubmit)="addComment()">
+ <textarea rows="1" placeholder="Add a new comment" class="textarea-new-msg border-faded-slim border-bottom-only padding-dot2 margin-bot-dot5" formControlName="newComment"></textarea>
+ <button class="border-faded-slim padding-dot2 lighter-hover click-effect border-radius-dot3" type="submit">
+ Add Comment
+ </button>
</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>
+ <hr class="card-hr">
+ <section>
+ <div class="text-centered" *ngIf="post?.comments?.length === 0">
+ Nobody has comented on this post yet!<br>
+ Try refreshing the page!
</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>
+ </section>
+</main>
diff --git a/src/app/components/post-page/post-page.component.ts b/src/app/components/post-page/post-page.component.ts
index 413ff80..0babfdf 100644
--- a/src/app/components/post-page/post-page.component.ts
+++ b/src/app/components/post-page/post-page.component.ts
@@ -1,4 +1,3 @@
-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';
@@ -7,8 +6,7 @@ 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';
+import { Post } from 'src/models/post.model';
@Component({
selector: 'app-post-page',
@@ -19,47 +17,30 @@ 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){
+ constructor(private _titleService: Title, private _router: Router, private _fb: FormBuilder, private _tokenService: TokenService, private _postService: PostService, private _commentService: CommentService) {
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._postService.getPostRequest(this.postId).subscribe({
+ next: (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;
- }
+
+ this.dataArrived = true;
},
- (err: HttpErrorResponse) => {
+ error: () => {
this._router.navigate(['/not-found']);
}
- );
-
- this.editPostFormGroup = this._fb.group({
- newPostMessage: new FormControl(''),
- fileUpload: new FormControl('')
});
this.addCommentFormGroup = this._fb.group({
@@ -67,68 +48,6 @@ export class PostPageComponent implements OnInit {
});
}
- 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']);
@@ -137,23 +56,14 @@ export class PostPageComponent implements OnInit {
const newComment = this.addCommentFormGroup.get('newComment')?.value;
if (newComment !== '' && newComment !== null) {
- this._commentService.createCommentWithSessionStorageRequest(this.postId, newComment).subscribe(
- (result: object) => {
- this.editPostFormGroup.reset();
+ this._commentService.createCommentWithSessionStorageRequest(this.postId, newComment).subscribe({
+ next: () => {
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';
diff --git a/src/app/components/post/post.component.css b/src/app/components/post/post.component.css
index 1b88c7d..1015564 100644
--- a/src/app/components/post/post.component.css
+++ b/src/app/components/post/post.component.css
@@ -1,134 +1,55 @@
-.post {
- display: flex;
- width: 98%;
-
- margin: .5em auto;
+.left-pane {
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 {
+.author-picture {
width: 2.2em;
height: 2.2em;
- margin-right: .2em;
-}
-
-.author-info > .handle {
- font-size: .9em;
- color: gray;
}
/* Content */
.content {
- flex: 1;
+ padding: 0 var(--card-padding);
}
.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;
+ word-break: break-word;
}
-.comment-count > img {
- height: .8em;
+.post-details {
+ margin-bottom: calc(var(--card-padding) / 1.5);
+ padding: 0.2em 0;
}
-.message:hover, .timestamp:hover {
- cursor: pointer;
+.post-details > * {
+ margin-left: 1.1em;
}
-/* Rating */
-
-/* Temporary, until ratings are implemented fully */
-.rating {
- display: none !important;
+.rating img {
+ height: 1.2em;
+ width: 1.2em;
}
-.rating {
- display: flex;
- flex-direction: column;
- align-items: center;
- min-height: 4.4em;
- margin: auto -.1em auto 0;
-}
+/* Edit */
-.score {
- flex: 1;
- display: flex;
- align-items: center;
+#attachments-btns img, .file-button > input {
+ height: 1.4em;
+ width: 1.4em;
}
-
-.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;
+.file-button {
+ position: relative;
}
-.attachments:empty {
- display: none;
+.file-button > img {
+ position: absolute;
+ pointer-events: none;
}
-.attachments > * {
- flex: 1;
+.file-button > input {
+ font-size: inherit;
+ color: transparent;
}
diff --git a/src/app/components/post/post.component.html b/src/app/components/post/post.component.html
index bc0d84a..0c1cce2 100644
--- a/src/app/components/post/post.component.html
+++ b/src/app/components/post/post.component.html
@@ -1,49 +1,88 @@
<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 }}
+<section class="card flex-row" [hidden]="loaded" (mouseleave)="resetShareBtn()">
+ <aside class="left-pane">
+ <img class="author-picture round-image hover-half-opacity" [src]="user.profilePictureURL" (click)="goToAuthorProfile()">
+ </aside>
+ <main class="content flexible">
+ <summary class="font-size-dot8 text-vertical-middle hover-half-opacity" (click)="goToAuthorProfile()">
+ <span>
+ {{ user.firstName }} {{ user.lastName }}
+ </span>
+ <span class="fg-faded">
+ @{{ user.userName }}
+ </span>
+ </summary>
+ <article class="message margin-top-bot-dot2" *ngIf="!editingPost">
+ {{ post.message }}
+ </article>
+ <section class="flex-row flexible-children" *ngIf="!editingPost">
+ <figure *ngFor="let fileURL of post.fileURLs">
+ <app-post-attachment [paramURL]="fileURL"></app-post-attachment>
+ </figure>
+ </section>
+ <form [formGroup]="editPostFormGroup" *ngIf="editingPost">
+ <textarea class="textarea-new-msg width-full border-faded-slim border-bottom-only padding-dot2" rows="1" formControlName="newPostMessage" placeholder="What's on your mind?"></textarea>
+ <section class="flex-row flex-justify-start align-children-center top-bot-padding-dot6ger">
+ <div class="file-button hover-half-opacity click-effect">
+ <img src="/assets/icons/tabler-icon-paperclip.svg">
+ <input type="file" formControlName="fileUpload" (change)="onFileUpload($event)" multiple>
</div>
- <div class="handle">
- @{{ user.userName }}
+ </section>
+ </form>
+ <section class="flex-row bot-padding-dot6ger" *ngIf="editingPost">
+ <div *ngFor="let file of files" class="form-attachment border-faded-slim flexible flex-row flex-no-wrap flex-center-align-items padding-dot2 margin-top-bot-dot2">
+ <div class="flexible">
+ {{ file.name ? file.name : 'Attachment' }}
+ </div>
+ <div class="flex-col hover-half-opacity border-radius-dot2 click-effect" (click)="removeAttachment(file.name)">
+ <img src="/assets/icons/tabler-icon-x.svg">
</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">
- ᐃ
+ </section>
+ <button class="border-faded-slim width-full padding-dot2 lighter-hover click-effect border-radius-dot3 margin-bot-dot5" *ngIf="editingPost" (click)="editPost()">
+ Update Post
+ </button>
+ <section class="post-details flex-row flex-justify-end font-size-dot7 border-faded-slim border-bottom-only">
+ <time class="flex-row flex-center-align-items">
+ <img class="height-font" src="/assets/icons/tabler-icon-calendar-time.svg">&nbsp;
+ <span>
+ {{ timeCreated }}
+ </span>
+ </time>
+ <summary class="flex-row flex-center-align-items">
+ <img class="height-font" src="/assets/icons/tabler-icon-message-2.svg">&nbsp;
+ <span>
+ {{ post.comments.length }}
+ </span>
+ </summary>
+ </section>
+ <section class="flex-row justify-children-center align-children-center">
+ <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="toggleEditing()">
+ <img src="/assets/icons/tabler-icon-edit.svg">
+ </button>
+ <button class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="goToPostPage()">
+ <img src="/assets/icons/tabler-icon-message-2.svg">
+ &nbsp;Comment
+ </button>
+ <button #share ngxClipboard [cbContent]="getPostLink()" class="flexible padding-dot2 lighter-hover click-effect border-radius-dot3" (click)="showCopiedMessage()">
+ <img src="/assets/icons/tabler-icon-link.svg">
+ &nbsp;Share
+ </button>
+ <button class="padding-dot2 lighter-hover click-effect border-radius-dot3" *ngIf="loggedInAuthor" (click)="deletePost()">
+ <img src="/assets/icons/tabler-icon-trash.svg">
+ </button>
+ </section>
+ </main>
+ <aside class="rating flex-col flex-center-align-items">
+ <button #upvote class="flex-col lighter-hover border-radius-dot2 click-effect" (click)="votePost(true)">
+ <img src="/assets/icons/tabler-icon-chevron-up.svg">
</button>
- <div class="score">
+ <summary class="top-bot-padding-dot2">
{{ votesNumber }}
- </div>
- <button class="vote">
- ᐁ
+ </summary>
+ <button #downvote class="flex-col lighter-hover border-radius-dot2 click-effect" (click)="votePost(false)">
+ <img src="/assets/icons/tabler-icon-chevron-down.svg">
</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>
+ </aside>
+</section>
diff --git a/src/app/components/post/post.component.ts b/src/app/components/post/post.component.ts
index 387f56f..e8ba430 100644
--- a/src/app/components/post/post.component.ts
+++ b/src/app/components/post/post.component.ts
@@ -1,50 +1,123 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { Guid } from 'guid-typescript';
+import { CloudinaryService } from 'src/app/services/cloudinary.service';
import { PostService } from 'src/app/services/post.service';
+import { RatingService } from 'src/app/services/rating.service';
import { UserService } from 'src/app/services/user.service';
-import { User } from 'src/models/identity/user';
-import { Post } from 'src/models/post';
+import { User } from 'src/models/identity/user.model';
+import { Post } from 'src/models/post.model';
+import { TokenService } from '../../services/token.service';
@Component({
selector: 'app-post',
templateUrl: './post.component.html',
styleUrls: ['./post.component.css'],
})
-export class PostComponent implements OnInit {
+export class PostComponent implements OnInit, AfterViewInit {
public loaded = false;
public user: User;
public post: Post;
public votesNumber: number;
public timeCreated: string;
@Input() paramId: string;
+ @ViewChild('upvote') upvoteBtn: ElementRef;
+ @ViewChild('downvote') downvoteBtn: ElementRef;
+ @ViewChild('share') shareBtn: ElementRef;
+ private _defaultShareBtnInnerHTML: string;
+ private _linkShared = false; // For optimisation purposes
+ public loggedIn = false;
+ public loggedInAuthor = false;
+ public editingPost = false;
+ public files: File[];
+ public editPostFormGroup: FormGroup;
- constructor(private _postService: PostService, private _userService: UserService, private _router: Router)
+ constructor(private _postService: PostService, private _ratingServe: RatingService, private _userService: UserService, private _router: Router, private _tokenService: TokenService, private _cloudinaryService: CloudinaryService, private _fb: FormBuilder, private _elem: ElementRef, private _renderer: Renderer2)
{ }
ngOnInit(): void {
+ this.loggedIn = this._tokenService.getTokenFromSessionStorage() !== '';
+
this.post = this._postService.getDefaultPost();
this.user = this._userService.getDefaultUser();
+ this.files = [];
- this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe(
- (result: object) => {
+ this._postService.getPostRequest(Guid.parse(this.paramId)).subscribe({
+ next: (result: object) => {
Object.assign(this.post, result);
+
this.post.fileURLs = Object.values(result)[7];
- this.votesNumber = 23;
+ this.votesNumber = this.post.currentRating;
this.timeCreated = new Date(this.post.timeCreated).toLocaleString('en-GB');
+
this.loadUser();
}
- );
+ });
+
+ this.editPostFormGroup = this._fb.group({
+ newPostMessage: new FormControl(''),
+ fileUpload: new FormControl('')
+ });
}
private loadUser(): void {
- this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe(
- (result: object) => {
+ this._userService.getUserByUsernameRequest(this.post.creatorUsername).subscribe({
+ next: (result: object) => {
Object.assign(this.user, result);
+
+ if (this.loggedIn) {
+ this.loggedInAuthor = this._tokenService.getUsernameFromSessionStorageToken() === this.post.creatorUsername;
+ this.editPostFormGroup.get('newPostMessage')?.setValue(this.post.message);
+
+ if (this.post.fileURLs.length > 0) {
+ this.loadFiles();
+ return;
+ }
+ }
+
this.loaded = true;
}
- );
+ });
+ }
+
+ private loadFiles(): void {
+ for (const fileURL of this.post.fileURLs) {
+ this._cloudinaryService.getFileRequest(fileURL).subscribe({
+ next: (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.loaded = true;
+ }
+ }
+ });
+ }
+ }
+
+ ngAfterViewInit(): void {
+ if (this.loggedIn) {
+ this._ratingServe.getRatingByUserAndPostWithSessionStorageRequest(Guid.parse(this.paramId)).subscribe({
+ next: (x: object) => {
+ if (!x) {
+ return;
+ }
+
+ const isLike: boolean = Object.values(x)[3];
+
+ this.changeColorOfVoteButton(isLike, !isLike);
+ }
+ });
+ }
+
+ this._defaultShareBtnInnerHTML = this.shareBtn.nativeElement.innerHTML;
}
goToAuthorProfile(): void {
@@ -54,4 +127,117 @@ export class PostComponent implements OnInit {
goToPostPage(): void {
this._router.navigate(['/post/' + this.post.postId]);
}
+
+ toggleEditing(): void {
+ this.editingPost = !this.editingPost;
+ }
+
+ 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 {
+ const newMessage = this.editPostFormGroup.get('newPostMessage')?.value;
+
+ if (newMessage !== '') {
+ this._postService.putPostWithSessionStorageRequest(Guid.parse(this.paramId), newMessage, this.files).subscribe({
+ next: () => {
+ this.reloadPage();
+ }
+ });
+ this.loaded = false;
+ }
+ }
+
+ deletePost(): void {
+ this._postService.deletePostWithSessionStorage(Guid.parse(this.paramId)).subscribe({
+ next: () => {
+ 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]);
+ }
+
+ votePost(isLike: boolean): void {
+ if (!this.loggedIn) {
+ this._router.navigate(['/login']);
+ return;
+ }
+
+ this._ratingServe.getRatingByUserAndPostWithSessionStorageRequest(Guid.parse(this.paramId)).subscribe({
+ next: (x: object) => {
+ if (x == null) { // checks if result is null or undefined
+ this.createRating(isLike);
+
+ this.changeColorOfVoteButton(isLike, !isLike);
+ }
+ else if (Object.values(x)[3] === isLike) {
+ this.deleteRating(Object.values(x)[0], isLike);
+
+ this.changeColorOfVoteButton(false, false);
+ }
+ else {
+ this.putRating(isLike);
+
+ this.changeColorOfVoteButton(isLike, !isLike);
+ }
+ }
+ });
+ }
+
+ private createRating(isLike: boolean): void {
+ this._ratingServe.createRatingWithSessionStorageRequest(Guid.parse(this.paramId), isLike).subscribe({
+ next: () => {
+ this.votesNumber += -1 + Number(isLike) * 2;
+ }
+ });
+ }
+
+ private putRating(isLike: boolean): void {
+ this._ratingServe.putRatingWithSessionStorageRequest(Guid.parse(this.paramId), isLike).subscribe({
+ next: () => {
+ // when false -2 + 0 wjen true -2 + 4
+ this.votesNumber += -2 + Number(isLike) * 4;
+ }
+ });
+ }
+
+ private deleteRating(ratingId: string, isLike: boolean): void {
+ this._ratingServe.deleteRatingFromSessionStorageRequest(Guid.parse(ratingId)).subscribe({
+ next: () => {
+ this.votesNumber += 1 - Number(isLike) * 2;
+ }
+ });
+ }
+
+ private changeColorOfVoteButton(isUpvoted: boolean, isDownvoted: boolean): void {
+ this._renderer.setStyle(this.upvoteBtn.nativeElement, 'backgroundColor', (isUpvoted) ? 'var(--upvote-highlight)' : 'inherit');
+ this._renderer.setStyle(this.downvoteBtn.nativeElement, 'backgroundColor', (isDownvoted) ? 'var(--downvote-highlight)' : 'inherit');
+ }
+
+ resetShareBtn(): void {
+ if (this._linkShared) {
+ this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', this._defaultShareBtnInnerHTML);
+ this._linkShared = false;
+ }
+ }
+
+ showCopiedMessage(): void {
+ this._renderer.setProperty(this.shareBtn.nativeElement, 'innerHTML', 'Link copied to clipboard!');
+ this._linkShared = true;
+ }
+
+ getPostLink(): string {
+ return location.origin + '/post/' + this.paramId;
+ }
}
diff --git a/src/app/components/profile-settings/profile-settings.component.css b/src/app/components/profile-settings/profile-settings.component.css
index 1c07d9f..a8451c2 100644
--- a/src/app/components/profile-settings/profile-settings.component.css
+++ b/src/app/components/profile-settings/profile-settings.component.css
@@ -1,124 +1,10 @@
-* {
- 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;
+.sec-info {
+ width: fit-content;
+ background-color: #424242;
+ margin: 0 0.3em 0.3em 0;
}
diff --git a/src/app/components/profile-settings/profile-settings.component.html b/src/app/components/profile-settings/profile-settings.component.html
index 502697d..1471859 100644
--- a/src/app/components/profile-settings/profile-settings.component.html
+++ b/src/app/components/profile-settings/profile-settings.component.html
@@ -1,116 +1,149 @@
+<app-navbar></app-navbar>
+
<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>
+<main class="scroll-standalone under-navbar centered-content flex-col">
+ <section class="card width-full" *ngIf="this.isAdminUser">
+ <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" (click)="goToAdminPanel()">
+ Go to admin panel
+ </button>
+ </section>
+ <form class="flex-row card width-full font-size-dot9 margin-top-bot-dot7" [formGroup]="updateProfilePictureFormGroup" (ngSubmit)="updateProfilePicture()">
+ <img id="profile-picture" class="round-image" [src]="user.profilePictureURL">
+ <section class="flexible flex-col flex-center-align-items flex-justify-center padding-side-font">
+ <input class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="file" accept="image/*" formControlName="fileUpload" (change)="onFileUpload($event)">
+ <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 margin-top-dot4" type="submit" *ngIf="newProfilePicture.size > 0">
+ Update profile picture
+ </button>
+ </section>
+ </form>
+ <form class="flex-col card width-full padding-dot6" [formGroup]="updateUserFormGroup" (ngSubmit)="onSubmit()">
+ <section class="flex-col">
+ <div class="flex-row">
+ <label class="flexible fg-focus">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>
+ <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>
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="firstName" required>
+ </section>
+ <section class="flex-col">
+ <div class="flex-row">
+ <label class="flexible fg-focus">Last Name</label>
- <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>
+ <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>
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="lastName" required>
+ </section>
+ <section class="flex-col">
+ <div class="flex-row">
+ <label class="flexible fg-focus">Username</label>
- <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>
+ <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>
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="username" required>
+ </section>
+ <section class="flex-col">
+ <div class="flex-row">
+ <label class="flexible fg-focus">Email</label>
- <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>
+ <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 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>
+ <input type="text" class="fancy-input border-faded-slim border-bottom-only" formControlName="email" required>
+ </section>
+ <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleLanguages()">
+ ▼ Edit Languages ▼
+ </button>
+ <section class="flex-row flexible-children border-faded-slim padding-dot3 margin-top-dot5" *ngIf="showLanguages">
+ <section class="padding-right-1">
+ <div class="none-message" *ngIf="chosenLanguages.length === 0">
+ You haven't chosen any languages!
</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 class="border-faded-slim border-bottom-only" *ngIf="chosenLanguages.length > 0">
+ Chosen languages:
+ </div>
+ <div class="flex-row margin-top-dot4">
+ <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let lang of chosenLanguages" (click)="langClick(lang.name)">
+ {{ lang.name }}
</div>
</div>
- Available languages:
- <div id="all-languages">
- <div class="user-language" *ngFor="let lang of availableLanguages">
+ </section>
+ <section>
+ <div class="none-message" *ngIf="availableLanguages.length === 0">
+ No other languages available!
+ </div>
+ <div class="border-faded-slim border-bottom-only" *ngIf="availableLanguages.length > 0">
+ Available languages:
+ </div>
+ <div class="flex-row margin-top-dot4">
+ <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let lang of availableLanguages" (click)="langClick(lang.name)">
{{ 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>
+ </section>
+ </section>
+ <button type="button" class="fg-focus width-full border-faded-slim padding-dot3 lighter-hover click-effect margin-top-dot4" (click)="toggleTechnologies()">
+ ▼ Edit Technologies ▼
+ </button>
+ <section class="flex-row flexible-children border-faded-slim padding-dot3 margin-top-dot5" *ngIf="showTechnologies">
+ <section class="padding-right-1">
+ <div class="none-message" *ngIf="chosenTechnologies.length === 0">
+ You haven't chosen any technologies!
+ </div>
+ <div class="border-faded-slim border-bottom-only" *ngIf="chosenTechnologies.length > 0">
+ Chosen technologies:
+ </div>
+ <div class="flex-row margin-top-dot4">
+ <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let tech of chosenTechnologies" (click)="techClick(tech.name)">
+ {{ tech.name }}
</div>
</div>
- Available technologies:
- <div id="all-technologies">
- <div class="user-technology" *ngFor="let tech of availableTechnologies">
+ </section>
+ <section>
+ <div class="none-message" *ngIf="availableTechnologies.length === 0">
+ No other technologies available!
+ </div>
+ <div class="border-faded-slim border-bottom-only" *ngIf="availableTechnologies.length > 0">
+ Available technologies:
+ </div>
+ <div class="flex-row margin-top-dot4">
+ <div class="sec-info border-radius-dot5r padding-dot2 hover-half-opacity click-effect" *ngFor="let tech of availableTechnologies" (click)="techClick(tech.name)">
{{ tech.name }}
</div>
</div>
- </div>
+ </section>
+ </section>
+ <section class="flex-col input-selection">
+ <div class="flex-row">
+ <label class="flexible fg-focus">Current Password</label>
- <button id="update-profile-btn" class="submit-btn" type="submit">Update profile</button>
+ <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>
+ <input [type]="showCurrentPassword ? 'text' : 'password'" class="fancy-input border-faded-slim border-bottom-only padding-right-1dot5" formControlName="password" required>
+ <button type="button" class="show-password-button hover-half-opacity click-effect" (click)="toggleShowPassword(0)">
+ <img [src]="showCurrentPassword ? '/assets/icons/tabler-icon-eye-off.svg' : '/assets/icons/tabler-icon-eye.svg'">
+ </button>
+ </section>
+ <section class="margin-top-bot-dot3">
<app-success-bar></app-success-bar>
<app-error-bar></app-error-bar>
- </form>
- <hr>
- <div id="confirm-delete" *ngIf="deleteAccountConfirm">
+ </section>
+ <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3" type="submit">
+ Update profile
+ </button>
+ </form>
+ <section class="card width-full">
+ <div class="margin-bot-dot5 text-centered fg-error" *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>
+ <button class="width-full border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 fg-error" (click)="deleteAccount()">
+ Delete account
+ </button>
+ </section>
+ </main>
+
diff --git a/src/app/components/profile-settings/profile-settings.component.ts b/src/app/components/profile-settings/profile-settings.component.ts
index a484665..3d6e491 100644
--- a/src/app/components/profile-settings/profile-settings.component.ts
+++ b/src/app/components/profile-settings/profile-settings.component.ts
@@ -6,14 +6,15 @@ 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 { User } from 'src/models/identity/user.model';
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 { Language } from 'src/models/language.model';
+import { Technology } from 'src/models/technology.model';
import { TokenService } from 'src/app/services/token.service';
import { Title } from '@angular/platform-browser';
import { AppConstants } from 'src/app/app-constants.module';
+import { ProfilePictureService } from 'src/app/services/profile-picture.service';
@Component({
selector: 'app-profile-settings',
@@ -34,10 +35,13 @@ export class ProfileSettingsComponent implements OnInit {
public updateProfilePictureFormGroup: FormGroup;
public newProfilePicture: File;
public user: User;
+ public chosenLanguages: Language[];
+ public chosenTechnologies: Technology[];
public availableLanguages: Language[];
public availableTechnologies: Technology[];
+ public showCurrentPassword = false;
- 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) {
+ constructor(private _titleService: Title, private _router: Router, private _userService: UserService, private _profilePictureService: ProfilePictureService, private _languageService: LanguageService, private _technologyService: TechnologyService, private _tokenService: TokenService, private _fb: FormBuilder, private _location: Location) {
this._titleService.setTitle(this._title);
}
@@ -50,38 +54,40 @@ export class ProfileSettingsComponent implements OnInit {
this.availableTechnologies = [];
this.newProfilePicture = new File([], '');
- this._userService.getUserByUsernameRequest(this._urlUsername).subscribe(
- (res: object) => {
+ // Initializing forms with blank (default) values
+ this.updateUserFormGroup = this._fb.group({
+ firstName: new FormControl(''),
+ lastName: new FormControl(''),
+ username: new FormControl(''),
+ email: new FormControl(''),
+ password: new FormControl(''),
+ });
+ this.updateProfilePictureFormGroup = this._fb.group({
+ fileUpload: new FormControl('')
+ });
+
+ this._userService.getUserByUsernameRequest(this._urlUsername).subscribe({
+ next: (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) => {
+ error: () => {
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) => {
+ this._userService.getUserFromSessionStorageRequest().subscribe({
+ next: (tokenRes: object) => {
Object.assign(userFromToken, tokenRes);
if (userFromToken.userName === this._urlUsername) {
+ this.loadUserSecondaryInfo();
this.initForms();
this.dataArrived = true;
}
@@ -89,16 +95,53 @@ export class ProfileSettingsComponent implements OnInit {
this.goToProfile();
}
},
- (err: HttpErrorResponse) => {
+ error: () => {
this.logout();
}
- );
+ });
}
else {
this.goToProfile();
}
}
+ private loadUserSecondaryInfo(): void {
+ // Load languages and tehnologies of user
+ this._languageService.getFullLanguagesFromIncomplete(this.user.languages).then(
+ (result) => {
+ this.chosenLanguages = result as Language[];
+ this.loadAvailableLanguages();
+ }
+ );
+
+ this._technologyService.getFullTechnologiesFromIncomplete(this.user.technologies).then(
+ (result) => {
+ this.chosenTechnologies = result as Technology[];
+ this.loadAvailableTechnologies();
+ }
+ );
+ }
+
+ private loadAvailableLanguages(): void {
+ this._languageService.getAllLanguagesWithSessionStorageRequest().subscribe({
+ next: (result: object) => {
+ const allAvailable = result as Language[];
+ // Remove the chosen languages from all of the avaiable ones
+ this.availableLanguages = allAvailable.filter(a => !this.user.languages.some(l => l.name === a.name));
+ }
+ });
+ }
+
+ private loadAvailableTechnologies(): void {
+ this._technologyService.getAllTechnologiesWithSessionStorageRequest().subscribe({
+ next: (result: object) => {
+ const allAvailable = result as Technology[];
+ // Remove the chosen technologies from all of the avaiable ones
+ this.availableTechnologies = allAvailable.filter(a => !this.user.technologies.some(t => t.name === a.name));
+ }
+ });
+ }
+
private initForms(): void {
this.updateUserFormGroup = this._fb.group({
firstName: new FormControl(this.user.firstName, [
@@ -122,52 +165,17 @@ export class ProfileSettingsComponent implements OnInit {
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(' '));
- });
+ this.updateUserFormGroup.valueChanges.subscribe({
+ next: () => {
+ this._successBar?.hideMsg();
+ this._errorBar?.hideError();
+ }
});
}
@@ -180,11 +188,11 @@ export class ProfileSettingsComponent implements OnInit {
return;
}
- this._userService.putProfilePictureFromSessionStorageRequest(this.newProfilePicture).subscribe(
- (result: object) => {
+ this._profilePictureService.putPictureWithSessionStorageRequest(this.newProfilePicture).subscribe({
+ next: () => {
this.reloadPage();
}
- );
+ });
this.dataArrived = false;
}
@@ -195,14 +203,20 @@ export class ProfileSettingsComponent implements OnInit {
this.patchLanguagesControl();
this.patchTechnologiesControl();
- this._userService.putUserFromSessionStorageRequest(this.updateUserFormGroup, this.user.roles, this.user.friends).subscribe(
- (result: object) => {
+ this._userService.putUserFromSessionStorageRequest(this.updateUserFormGroup, this.chosenLanguages, this.chosenTechnologies, this.user.roles, this.user.friends).subscribe({
+ next: () => {
this._successBar.showMsg('Profile updated successfully!');
+
+ // "Reload" page when changing username
+ const newUsername = this.updateUserFormGroup.get('username')?.value;
+ if (newUsername !== this._urlUsername) {
+ this._router.navigate(['/profile/' + newUsername + '/settings']);
+ }
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
}
private patchLanguagesControl(): void {
@@ -261,17 +275,47 @@ export class ProfileSettingsComponent implements OnInit {
}
}
+ langClick(name: string): void {
+ if (this.chosenLanguages.some(c => c.name === name)) {
+ const index = this.chosenLanguages.findIndex(t => t.name === name);
+
+ this.availableLanguages.push(this.chosenLanguages[index]);
+ this.chosenLanguages.splice(index, 1);
+ }
+ else {
+ const index = this.availableLanguages.findIndex(t => t.name === name);
+
+ this.chosenLanguages.push(this.availableLanguages[index]);
+ this.availableLanguages.splice(index, 1);
+ }
+ }
+
+ techClick(name: string): void {
+ if (this.chosenTechnologies.some(c => c.name === name)) {
+ const index = this.chosenTechnologies.findIndex(t => t.name === name);
+
+ this.availableTechnologies.push(this.chosenTechnologies[index]);
+ this.chosenTechnologies.splice(index, 1);
+ }
+ else {
+ const index = this.availableTechnologies.findIndex(t => t.name === name);
+
+ this.chosenTechnologies.push(this.availableTechnologies[index]);
+ this.availableTechnologies.splice(index, 1);
+ }
+ }
+
goToProfile(): void {
this._router.navigate([this._router.url.substring(0, this._router.url.length - 9)]);
}
- navigateToAdminPanel(): void {
+ goToAdminPanel(): void {
this._router.navigate(['/admin-panel']);
}
logout(): void {
this._tokenService.logoutUserFromSessionStorage();
- this.goToProfile();
+ this._router.navigate(['/login']);
}
toggleLanguages(): void {
@@ -284,14 +328,14 @@ export class ProfileSettingsComponent implements OnInit {
deleteAccount(): void {
if (this.deleteAccountConfirm) {
- this._userService.deleteUserFromSessionStorageRequest().subscribe(
- (res: object) => {
+ this._userService.deleteUserFromSessionStorageRequest().subscribe({
+ next: () => {
this.logout();
},
- (err: HttpErrorResponse) => {
+ error: (err: HttpErrorResponse) => {
this._errorBar.showError(err);
}
- );
+ });
this.dataArrived = false;
}
else {
@@ -304,4 +348,10 @@ export class ProfileSettingsComponent implements OnInit {
this._router.onSameUrlNavigation = 'reload';
this._router.navigate([this._router.url]);
}
+
+ toggleShowPassword(index: number): void {
+ switch (index) {
+ case 0: this.showCurrentPassword = !this.showCurrentPassword;
+ }
+ }
}
diff --git a/src/app/components/profile/profile.component.css b/src/app/components/profile/profile.component.css
index ebcd406..c7e112f 100644
--- a/src/app/components/profile/profile.component.css
+++ b/src/app/components/profile/profile.component.css
@@ -1,105 +1,27 @@
-* {
- box-sizing: border-box;
+#user-info {
+ font-size: 1.3em;
}
-#content {
- max-width: 22em;
- justify-content: start;
+#profile-picture {
+ height: 5rem;
+ width: 5rem;
}
-hr {
- width: calc(100% - 1em);
- color: black;
- border: 1px solid black;
+.sec-info-card {
+ padding-bottom: calc(var(--card-padding) - 0.3em);
}
-form {
- width: 100%;
+.sec-info-title {
+ padding-bottom: 0.2em;
+ margin-bottom: 0.3em;
}
-/* 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;
+.none-message {
+ margin-bottom: 0.3em;
}
-#posts {
- width: 100%;
- height: 100%;
+.sec-info {
+ width: fit-content;
+ background-color: #424242;
+ margin: 0 0.3em 0.3em 0;
}
diff --git a/src/app/components/profile/profile.component.html b/src/app/components/profile/profile.component.html
index 0e5f633..c6fdee6 100644
--- a/src/app/components/profile/profile.component.html
+++ b/src/app/components/profile/profile.component.html
@@ -1,60 +1,57 @@
+<app-navbar></app-navbar>
+
<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>
+<main class="scroll-standalone under-navbar flex-col centered-content" *ngIf="dataArrived" (scroll)="onScroll($event)">
+ <section id="user-info" class="card flex-row width-full flex-justify-center">
+ <div>
+ <img id="profile-picture" class="round-image" [src]="user.profilePictureURL">
</div>
- <div class="secondary-info rounded-border">
- Languages:
- <div *ngFor="let lang of user.languages">
- <div class="user-language">
- {{ lang.name }}
- </div>
+ <div class="flexible flex-col flex-center-align-items flex-justify-center side-padding-dot3">
+ <div class="text-centered">
+ {{ user.firstName }} {{ user.lastName }}
</div>
- <div *ngIf="user.languages.length === 0">
- &nbsp;None
+ <div>
+ @{{ user.userName }}
</div>
+ <button id="add-friend" class="border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 font-size-dot8 margin-top-dot4" (click)="modifyFriend()" *ngIf="!isTheLoggedInUser && isUserLoggedIn">
+ {{ friendOfUser ? 'Unfriend' : 'Add friend' }}
+ </button>
</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>
+ </section>
+ <section class="card sec-info-card flex-col width-full">
+ <div class="sec-info-title border-faded-slim border-bottom-only">
+ Languages
+ </div>
+ <div class="none-message" *ngIf="user.languages.length === 0">
+ None
</div>
- <hr>
- <div id="posts">
- <div id="no-posts" *ngIf="userPosts.length === 0">
- {{ user.firstName }} {{ user.lastName }} hasn't posted anything yet!
+ <div class="flex-row">
+ <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let lang of user.languages">
+ {{ lang.name }}
</div>
- <div *ngFor="let userPost of userPosts">
- <app-post [paramId]="userPost.postId.toString()"></app-post>
+ </div>
+ </section>
+ <section class="card sec-info-card flex-col width-full">
+ <div class="sec-info-title border-faded-slim border-bottom-only">
+ Technologies
+ </div>
+ <div class="none-message" *ngIf="user.technologies.length === 0">
+ None
+ </div>
+ <div class="flex-row">
+ <div class="sec-info border-radius-dot5r padding-dot2" *ngFor="let tech of user.technologies">
+ {{ tech.name }}
</div>
</div>
- </div>
-</div>
+ </section>
+ <hr class="card-hr">
+ <section>
+ <div class="text-centered" *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>
+ </section>
+</main>
diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts
index bbf8585..f9f64b5 100644
--- a/src/app/components/profile/profile.component.ts
+++ b/src/app/components/profile/profile.component.ts
@@ -1,18 +1,16 @@
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 { User } from 'src/models/identity/user.model';
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 { Post } from 'src/models/post.model';
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';
+import { FriendService } from 'src/app/services/friend.service';
@Component({
selector: 'app-profile',
@@ -29,44 +27,34 @@ export class ProfileComponent implements OnInit {
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) {
+ constructor(private _titleService: Title, private _router: Router, private _userService: UserService, private _friendService: FriendService, 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
+ now.setHours(now.getHours() + 3); // 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) => {
+ this._userService.getUserByUsernameRequest(this._urlUsername).subscribe({
+ next: (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) => {
+ error: () => {
this._router.navigate(['/not-found']);
}
- );
+ });
}
private loadLanguages(): void {
@@ -96,17 +84,17 @@ export class ProfileComponent implements OnInit {
}
private loadPosts(): void {
- this._feedService.getUserPostsRequest(this.user.userName, this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe(
- (result: object) => {
+ this._feedService.getUserPostsRequest(this.user.userName, this._currentPage++, this._timeLoaded, AppConstants.PAGE_SIZE).subscribe({
+ next: (result: object) => {
const resultArr: Post[] = Object.values(result)[0];
this.userPosts.push(...resultArr);
this.finishUserLoading();
},
- (err: HttpErrorResponse) => {
+ error: () => {
this._currentPage = -1;
this.finishUserLoading();
}
- );
+ });
}
private finishUserLoading(): void {
@@ -114,8 +102,8 @@ export class ProfileComponent implements OnInit {
this.isUserLoggedIn = true;
const userFromToken: User = this._userService.getDefaultUser();
- this._userService.getUserFromSessionStorageRequest().subscribe(
- (tokenRes: object) => {
+ this._userService.getUserFromSessionStorageRequest().subscribe({
+ next: (tokenRes: object) => {
Object.assign(userFromToken, tokenRes);
if (userFromToken.friends.map(x => x.userName).includes(this._urlUsername)) {
@@ -126,28 +114,16 @@ export class ProfileComponent implements OnInit {
}
this.dataArrived = true;
},
- (err: HttpErrorResponse) => {
+ error: () => {
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();
@@ -158,34 +134,35 @@ export class ProfileComponent implements OnInit {
}
modifyFriend(): void {
- if (this.updatingFriendship) {
- this.dataArrived = false;
-
- this._userService.getUserFromSessionStorageRequest().subscribe(
- (result: object) => {
- const loggedInUser: User = result as User;
+ this.dataArrived = false;
+ if (this.friendOfUser) {
+ this.removeFriendFromLoggedInUser();
+ }
+ else {
+ this.addFriendToLoggedInUser();
+ }
+ }
- 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);
- }
+ private addFriendToLoggedInUser(): void {
+ this._friendService.postFriendWithSessionStorageRequest(this.user.userName).subscribe({
+ next: () => {
+ this.reloadPage();
+ },
+ error: () => {
+ this._router.navigate(['/']);
+ }
+ });
+ }
- this._userService.putBareUserFromSessionStorageRequest(loggedInUser, this.updateFrienship.get('password')?.value).subscribe(
- (resultUpdate: object) => {
- this.reloadPage();
- },
- (err: HttpErrorResponse) => {
- this._router.navigate(['/']);
- }
- );
- }
- );
- }
- this.updatingFriendship = !this.updatingFriendship;
+ private removeFriendFromLoggedInUser(): void {
+ this._friendService.deleteFriendWithSessionStorageRequest(this.user.userName).subscribe({
+ next: () => {
+ this.reloadPage();
+ },
+ error: () => {
+ this._router.navigate(['/']);
+ }
+ });
}
onScroll(event: any): void {
diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css
index 93d8006..e69de29 100644
--- a/src/app/components/register/register.component.css
+++ b/src/app/components/register/register.component.css
@@ -1,40 +0,0 @@
-/* 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
index 4e67e0e..d8fa7c4 100644
--- a/src/app/components/register/register.component.html
+++ b/src/app/components/register/register.component.html
@@ -1,65 +1,57 @@
-<div id="content">
- <div class="title">Register</div>
+<main class="centered-content scroll-standalone height-full flex-col flex-center-align-items flex-justify-center">
+ <summary class="title card width-full margin-0 padding-dot2">Register</summary>
- <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>
+ <app-error-bar class="width-full margin-top-dot4"></app-error-bar>
+ <form class="width-full card padding-dot6" [formGroup]="registerUserFormGroup" (ngSubmit)="onSubmit()">
+ <section class="input-selection width-full">
+ <input type="text" placeholder="First Name" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="firstName" required>
+ <label class="fancy-input-label width-full">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>
-
+ </section>
+ <section class="input-selection">
+ <input type="text" placeholder="Last Name" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="lastName" required>
+ <label class="fancy-input-label width-full">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>
-
+ </section>
+ <section class="input-selection">
+ <input type="text" placeholder="Username" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="username" required>
+ <label class="fancy-input-label width-full">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>
-
+ </section>
+ <section class="input-selection">
+ <input type="text" placeholder="Email" class="fancy-input width-full border-faded-slim border-bottom-only" formControlName="email" required>
+ <label class="fancy-input-label width-full">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>
-
+ </section>
+ <section class="input-selection">
+ <input [type]="showingPassword ? 'text' : 'password'" placeholder="Password" class="fancy-input width-full border-faded-slim border-bottom-only padding-right-1dot5" formControlName="password" required>
+ <label class="fancy-input-label width-full">Password</label>
+ <button type="button" class="show-password-button hover-half-opacity click-effect" (click)="toggleShowPassword()">
+ <img [src]="showingPassword ? '/assets/icons/tabler-icon-eye-off.svg' : '/assets/icons/tabler-icon-eye.svg'">
+ </button>
<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>
+ </section>
- <hr>
- <button class="submit-btn" type="submit">Submit</button>
- <app-error-bar></app-error-bar>
+ <button class="border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full" type="submit">Submit</button>
</form>
- <button class="submit-btn redirect-to-login" (click)="onRedirectLogin()">Already have an account? Login here</button>
-</div>
+ <button class="fg-focus border-faded-slim padding-dot3 lighter-hover click-effect border-radius-dot3 width-full margin-top-dot4" (click)="onRedirectLogin()">
+ Already have an account? Login here
+ </button>
+</main>
diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts
index 36eaa55..6d84e11 100644
--- a/src/app/components/register/register.component.ts
+++ b/src/app/components/register/register.component.ts
@@ -1,6 +1,6 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit, ViewChild } from '@angular/core';
-import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { 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';
@@ -16,6 +16,7 @@ export class RegisterComponent implements OnInit {
@ViewChild(ErrorBarComponent) private _errorBar: ErrorBarComponent;
private _title = 'Register';
public registerUserFormGroup: FormGroup;
+ public showingPassword = false;
constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService, private _tokenService: TokenService) {
this._titleService.setTitle(this._title);
@@ -49,38 +50,23 @@ export class RegisterComponent implements OnInit {
// this.registerUserFormGroup.valueChanges.subscribe(console.log);
}
+ toggleShowPassword(): void {
+ this.showingPassword = !this.showingPassword;
+ }
+
onSubmit(): void {
- this._userService.registerUserRequest(this.registerUserFormGroup).subscribe(
- res => {
+ this._userService.registerUserRequest(this.registerUserFormGroup).subscribe({
+ next: (res: object) => {
this._tokenService.setUserTokenToSessionStorage(res);
this._router.navigate(['/']);
},
- (err: HttpErrorResponse) => {
+ error: (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
index bee634d..32fe099 100644
--- a/src/app/components/success-bar/success-bar.component.css
+++ b/src/app/components/success-bar/success-bar.component.css
@@ -4,6 +4,7 @@
color: white;
padding: .2em;
text-align: center;
+ box-sizing: border-box;
}
#success-bar:empty {
diff --git a/src/app/services/comment.service.ts b/src/app/services/comment.service.ts
index c9dbf35..9839b01 100644
--- a/src/app/services/comment.service.ts
+++ b/src/app/services/comment.service.ts
@@ -2,7 +2,7 @@ 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 { Comment } from 'src/models/comment.model';
import { AppConstants } from '../app-constants.module';
import { TokenService } from './token.service';
@@ -14,7 +14,7 @@ export class CommentService {
{ }
getDefaultComment(): Comment {
- return new Comment(Guid.createEmpty(), Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date());
+ return new Comment(Guid.createEmpty(), Guid.createEmpty(), '', '', '', '', new Date());
}
/* Requests from session storage */
@@ -55,7 +55,7 @@ export class CommentService {
getCommentRequest(id: Guid): Observable<object> {
const options = {
- params: new HttpParams().set('Id', id.toString())
+ params: new HttpParams().set('CommentId', id.toString())
};
return this._http.get(AppConstants.API_COMMENT_URL, options);
}
@@ -75,7 +75,7 @@ export class CommentService {
deleteCommentRequest(commentId: Guid, authToken: string): Observable<object> {
const options = {
- params: new HttpParams().set('Id', commentId.toString()),
+ params: new HttpParams().set('CommentId', commentId.toString()),
headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
};
return this._http.delete(AppConstants.API_COMMENT_URL, options);
diff --git a/src/app/services/friend.service.ts b/src/app/services/friend.service.ts
new file mode 100644
index 0000000..6821606
--- /dev/null
+++ b/src/app/services/friend.service.ts
@@ -0,0 +1,44 @@
+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 FriendService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ postFriendWithSessionStorageRequest(friendUsername: string): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.postFriendRequest(userId, token, friendUsername);
+ }
+
+ deleteFriendWithSessionStorageRequest(friendUsername: string): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.deleteFriendRequest(userId, token, friendUsername);
+ }
+
+ postFriendRequest(userId: Guid, authToken: string, friendUsername: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()).set('FriendUsername', friendUsername),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.post(AppConstants.API_FRIENDS_URL, {}, options);
+ }
+
+ deleteFriendRequest(userId: Guid, authToken: string, friendUsername: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()).set('FriendUsername', friendUsername),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this._http.delete(AppConstants.API_FRIENDS_URL, options);
+ }
+}
diff --git a/src/app/services/language.service.ts b/src/app/services/language.service.ts
index 15e241f..ec368da 100644
--- a/src/app/services/language.service.ts
+++ b/src/app/services/language.service.ts
@@ -2,7 +2,7 @@ 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 { Language } from 'src/models/language.model';
import { AppConstants } from '../app-constants.module';
import { TokenService } from './token.service';
@@ -105,7 +105,7 @@ export class LanguageService {
deleteLanguageRequest(authToken: string, langId: Guid): Observable<object> {
const options = {
- params: new HttpParams().set('Id', langId.toString()),
+ params: new HttpParams().set('LanguageId', 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
index 7b2a539..1a6e021 100644
--- a/src/app/services/post.service.ts
+++ b/src/app/services/post.service.ts
@@ -3,7 +3,7 @@ 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 { Post } from 'src/models/post.model';
import { AppConstants } from '../app-constants.module';
import { TokenService } from './token.service';
@@ -15,7 +15,7 @@ export class PostService {
{ }
getDefaultPost(): Post {
- return new Post(Guid.createEmpty(), 'Gosho', 'Trapov', 'gosho_trapov', 'Your opinion on my idea?', new Date(), [], []);
+ return new Post(Guid.createEmpty(), '', '', '', '', new Date(), [], [], 0);
}
/* Requests from session storage */
@@ -78,7 +78,7 @@ export class PostService {
deletePostRequest(postId: Guid, authToken: string): Observable<object> {
const options = {
- params: new HttpParams().set('Id', postId.toString()),
+ params: new HttpParams().set('PostId', postId.toString()),
headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
};
return this._http.delete(AppConstants.API_POST_URL, options);
diff --git a/src/app/services/profile-picture.service.ts b/src/app/services/profile-picture.service.ts
new file mode 100644
index 0000000..8b9d0a3
--- /dev/null
+++ b/src/app/services/profile-picture.service.ts
@@ -0,0 +1,32 @@
+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 ProfilePictureService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ putPictureWithSessionStorageRequest(newPicture: File): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putRatingRequest(userId, token, newPicture);
+ }
+
+ putRatingRequest(userId: Guid, authToken: string, newPicture: File): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ const form = new FormData();
+ form.append('picture', newPicture);
+
+ return this._http.put(AppConstants.API_PROFILE_PICTURE_URL, form, options);
+ }
+}
diff --git a/src/app/services/rating.service.ts b/src/app/services/rating.service.ts
new file mode 100644
index 0000000..be35a81
--- /dev/null
+++ b/src/app/services/rating.service.ts
@@ -0,0 +1,86 @@
+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 RatingService {
+ constructor(private _http: HttpClient, private _tokenService: TokenService)
+ { }
+
+ createRatingWithSessionStorageRequest(postId: Guid, isLike: boolean): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.createRatingRequest(userId, token, postId, isLike);
+ }
+
+ putRatingWithSessionStorageRequest(postId: Guid, isLike: boolean): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.putRatingRequest(userId, token, postId, isLike);
+ }
+
+ getRatingByUserAndPostWithSessionStorageRequest(postId: Guid): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.getRatingByUserAndPostRequest(userId, token, postId);
+ }
+
+ deleteRatingFromSessionStorageRequest(ratingId: Guid): Observable<object> {
+ const userId = this._tokenService.getUserIdFromSessionStorageToken();
+ const token = this._tokenService.getTokenFromSessionStorage();
+
+ return this.deleteRatingRequest(userId, token, ratingId);
+ }
+
+ createRatingRequest(userId: Guid, authToken: string, postId: Guid, isLike: boolean): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ const body = {
+ postId: postId.toString(),
+ isLike: isLike
+ };
+
+ return this._http.post(AppConstants.API_RATING_URL, body, options);
+ }
+
+ putRatingRequest(userId: Guid, authToken: string, postId: Guid, isLike: boolean): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()).set('PostId', postId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ const body = {
+ isLike: isLike
+ };
+
+ return this._http.put(AppConstants.API_RATING_URL, body, options);
+ }
+
+ getRatingByUserAndPostRequest(userId: Guid, authToken: string, postId: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()).set('PostId', postId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+
+ return this._http.get(AppConstants.API_RATING_URL + '/GetByUserAndPost', options);
+ }
+
+ deleteRatingRequest(userId: Guid, authToken: string, ratingId: Guid): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserId', userId.toString()).set('RatingId', ratingId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+
+ return this._http.delete(AppConstants.API_RATING_URL, options);
+ }
+}
diff --git a/src/app/services/technology.service.ts b/src/app/services/technology.service.ts
index dbdc039..fcc3d4c 100644
--- a/src/app/services/technology.service.ts
+++ b/src/app/services/technology.service.ts
@@ -2,7 +2,7 @@ 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 { Technology } from 'src/models/technology.model';
import { AppConstants } from '../app-constants.module';
import { TokenService } from './token.service';
diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts
index 31862c4..690fff5 100644
--- a/src/app/services/user.service.ts
+++ b/src/app/services/user.service.ts
@@ -1,13 +1,15 @@
import { Injectable } from '@angular/core';
import { Guid } from 'guid-typescript';
-import { User } from '../../models/identity/user';
+import { User } from '../../models/identity/user.model';
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 { Role } from 'src/models/identity/role.model';
+import { Friend } from 'src/models/identity/friend.model';
import { TokenService } from './token.service';
+import { Language } from 'src/models/language.model';
+import { Technology } from 'src/models/technology.model';
@Injectable({
providedIn: 'root'
@@ -17,7 +19,7 @@ export class UserService {
{ }
getDefaultUser(): User {
- return new User(Guid.createEmpty(), 'gosho_trapov', 'Gosho', 'Trapov', 'gotra@bg.com', AppConstants.FALLBACK_PROFILE_ICON, [], [], [], []);
+ return new User(Guid.createEmpty(), '', '', '', '', AppConstants.FALLBACK_PROFILE_ICON, [], [], [], []);
}
/* Requests from session storage */
@@ -36,25 +38,11 @@ export class UserService {
return this.addFriendToUserRequest(userUserName, token, newFriendUserName);
}
- putUserFromSessionStorageRequest(updateUserFormGroup: FormGroup, userRoles: Role[], userFriends: Friend[]): Observable<object> {
+ putUserFromSessionStorageRequest(updateUserFormGroup: FormGroup, languages: Language[], technologies: Technology[], 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);
+ return this.putUserRequest(userId, token, updateUserFormGroup, languages, technologies, userRoles, userFriends);
}
deleteUserFromSessionStorageRequest(): Observable<object> {
@@ -119,7 +107,7 @@ export class UserService {
return this._http.get(AppConstants.API_USER_URL + '/GetUser', options);
}
- putUserRequest(userId: Guid, authToken: string, updateUserFormGroup: FormGroup, userRoles: Role[], userFriends: Friend[]): Observable<object> {
+ putUserRequest(userId: Guid, authToken: string, updateUserFormGroup: FormGroup, languages: Language[], technologies: Technology[], userRoles: Role[], userFriends: Friend[]): Observable<object> {
const body = {
UserName: updateUserFormGroup.get('username')?.value,
Email: updateUserFormGroup.get('email')?.value,
@@ -128,19 +116,9 @@ export class UserService {
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)
+ Languages: languages,
+ Technologies: technologies
};
- 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)
@@ -148,16 +126,6 @@ export class UserService {
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()),
diff --git a/src/assets/icons/tabler-icon-calendar-time.svg b/src/assets/icons/tabler-icon-calendar-time.svg
new file mode 100644
index 0000000..3bb3a3e
--- /dev/null
+++ b/src/assets/icons/tabler-icon-calendar-time.svg
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-calendar-time"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg16"
+ sodipodi:docname="tabler-icon-calendar-time.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata22">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs20" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview18"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg16" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="M 11.795,21 H 5 A 2,2 0 0 1 3,19 V 7 A 2,2 0 0 1 5,5 h 12 a 2,2 0 0 1 2,2 v 4"
+ id="path4"
+ style="stroke:#ffeede" />
+ <circle
+ cx="18"
+ cy="18"
+ r="4"
+ id="circle6"
+ style="stroke:#ffeede" />
+ <path
+ d="M 15,3 V 7"
+ id="path8"
+ style="stroke:#ffeede" />
+ <path
+ d="M 7,3 V 7"
+ id="path10"
+ style="stroke:#ffeede" />
+ <path
+ d="M 3,11 H 19"
+ id="path12"
+ style="stroke:#ffeede" />
+ <path
+ d="M 18,16.496 V 18 l 1,1"
+ id="path14"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-chevron-down.svg b/src/assets/icons/tabler-icon-chevron-down.svg
new file mode 100644
index 0000000..0971874
--- /dev/null
+++ b/src/assets/icons/tabler-icon-chevron-down.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-chevron-down"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg6"
+ sodipodi:docname="tabler-icon-chevron-down.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata12">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs10" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="750"
+ inkscape:window-height="480"
+ id="namedview8"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg6" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <polyline
+ points="6 9 12 15 18 9"
+ id="polyline4"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-chevron-up.svg b/src/assets/icons/tabler-icon-chevron-up.svg
new file mode 100644
index 0000000..20d694a
--- /dev/null
+++ b/src/assets/icons/tabler-icon-chevron-up.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-chevron-up"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg6"
+ sodipodi:docname="tabler-icon-chevron-up.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata12">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs10" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="737"
+ inkscape:window-height="480"
+ id="namedview8"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg6" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <polyline
+ points="6 15 12 9 18 15"
+ id="polyline4"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-edit.svg b/src/assets/icons/tabler-icon-edit.svg
new file mode 100644
index 0000000..e12e396
--- /dev/null
+++ b/src/assets/icons/tabler-icon-edit.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-edit"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg10"
+ sodipodi:docname="tabler-icon-edit.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata16">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs14" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview12"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg10" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="M 9,7 H 6 A 2,2 0 0 0 4,9 v 9 a 2,2 0 0 0 2,2 h 9 a 2,2 0 0 0 2,-2 v -3"
+ id="path4"
+ style="stroke:#ffeede" />
+ <path
+ d="m 9,15 h 3 l 8.5,-8.5 a 2.1213203,2.1213203 0 0 0 -3,-3 L 9,12 v 3"
+ id="path6"
+ style="stroke:#ffeede" />
+ <line
+ x1="16"
+ y1="5"
+ x2="19"
+ y2="8"
+ id="line8"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-eye-off.svg b/src/assets/icons/tabler-icon-eye-off.svg
new file mode 100644
index 0000000..e1ed7f9
--- /dev/null
+++ b/src/assets/icons/tabler-icon-eye-off.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-eye-off"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg10"
+ sodipodi:docname="tabler-icon-eye-off.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata16">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs14" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview12"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg10" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <line
+ x1="3"
+ y1="3"
+ x2="21"
+ y2="21"
+ id="line4"
+ style="stroke:#ffeede" />
+ <path
+ d="m 10.584,10.587 a 2.0004052,2.0004052 0 0 0 2.828,2.83"
+ id="path6"
+ style="stroke:#ffeede" />
+ <path
+ d="M 9.363,5.365 A 9.466,9.466 0 0 1 12,5 c 4,0 7.333,2.333 10,7 -0.778,1.361 -1.612,2.524 -2.503,3.488 m -2.14,1.861 C 15.726,18.449 13.942,19 12,19 8,19 4.667,16.667 2,12 3.369,9.605 4.913,7.825 6.632,6.659"
+ id="path8"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-eye.svg b/src/assets/icons/tabler-icon-eye.svg
new file mode 100644
index 0000000..7384d99
--- /dev/null
+++ b/src/assets/icons/tabler-icon-eye.svg
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-eye"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-eye.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <circle
+ cx="12"
+ cy="12"
+ r="2"
+ id="circle4"
+ style="stroke:#ffeede" />
+ <path
+ d="M 22,12 C 19.333,16.667 16,19 12,19 8,19 4.667,16.667 2,12 4.667,7.333 8,5 12,5 c 4,0 7.333,2.333 10,7"
+ id="path6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-file.svg b/src/assets/icons/tabler-icon-file.svg
new file mode 100644
index 0000000..312c9f4
--- /dev/null
+++ b/src/assets/icons/tabler-icon-file.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-file"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-file.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="m 14,3 v 4 a 1,1 0 0 0 1,1 h 4"
+ id="path4"
+ style="stroke:#ffeede" />
+ <path
+ d="M 17,21 H 7 A 2,2 0 0 1 5,19 V 5 A 2,2 0 0 1 7,3 h 7 l 5,5 v 11 a 2,2 0 0 1 -2,2 z"
+ id="path6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-home.svg b/src/assets/icons/tabler-icon-home.svg
new file mode 100644
index 0000000..3578ddb
--- /dev/null
+++ b/src/assets/icons/tabler-icon-home.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-home"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg10"
+ sodipodi:docname="tabler-icon-home.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata16">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs14" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview12"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg10" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <polyline
+ points="5 12 3 12 12 3 21 12 19 12"
+ id="polyline4"
+ style="stroke:#ffeede" />
+ <path
+ d="m 5,12 v 7 a 2,2 0 0 0 2,2 h 10 a 2,2 0 0 0 2,-2 v -7"
+ id="path6"
+ style="stroke:#ffeede" />
+ <path
+ d="m 9,21 v -6 a 2,2 0 0 1 2,-2 h 2 a 2,2 0 0 1 2,2 v 6"
+ id="path8"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-link.svg b/src/assets/icons/tabler-icon-link.svg
new file mode 100644
index 0000000..a9f9110
--- /dev/null
+++ b/src/assets/icons/tabler-icon-link.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-link"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-link.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="737"
+ inkscape:window-height="480"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="m 10,14 a 3.5,3.5 0 0 0 5,0 l 4,-4 A 3.5355339,3.5355339 0 0 0 14,5 l -0.5,0.5"
+ id="path4"
+ style="stroke:#ffeede" />
+ <path
+ d="m 14,10 a 3.5,3.5 0 0 0 -5,0 l -4,4 a 3.5355339,3.5355339 0 0 0 5,5 l 0.5,-0.5"
+ id="path6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-login.svg b/src/assets/icons/tabler-icon-login.svg
new file mode 100644
index 0000000..4f62892
--- /dev/null
+++ b/src/assets/icons/tabler-icon-login.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-login"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-login.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="M 14,8 V 6 A 2,2 0 0 0 12,4 H 5 A 2,2 0 0 0 3,6 v 12 a 2,2 0 0 0 2,2 h 7 a 2,2 0 0 0 2,-2 v -2"
+ id="path4"
+ style="stroke:#ffeede" />
+ <path
+ d="M 20,12 H 7 l 3,-3 m 0,6 -3,-3"
+ id="path6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-logout.svg b/src/assets/icons/tabler-icon-logout.svg
new file mode 100644
index 0000000..c311628
--- /dev/null
+++ b/src/assets/icons/tabler-icon-logout.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-logout"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-logout.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="M 14,8 V 6 A 2,2 0 0 0 12,4 H 5 A 2,2 0 0 0 3,6 v 12 a 2,2 0 0 0 2,2 h 7 a 2,2 0 0 0 2,-2 v -2"
+ id="path4"
+ style="stroke:#ffeede" />
+ <path
+ d="M 7,12 H 21 L 18,9 m 0,6 3,-3"
+ id="path6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-message-2.svg b/src/assets/icons/tabler-icon-message-2.svg
new file mode 100644
index 0000000..6a9d20c
--- /dev/null
+++ b/src/assets/icons/tabler-icon-message-2.svg
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-message-2"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg10"
+ sodipodi:docname="tabler-icon-message-2.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata16">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs14" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview12"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg10" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="M 12,20 9,17 H 7 A 3,3 0 0 1 4,14 V 8 A 3,3 0 0 1 7,5 h 10 a 3,3 0 0 1 3,3 v 6 a 3,3 0 0 1 -3,3 h -2 l -3,3"
+ id="path4"
+ style="stroke:#ffeede" />
+ <line
+ x1="8"
+ y1="9"
+ x2="16"
+ y2="9"
+ id="line6"
+ style="stroke:#ffeede" />
+ <line
+ x1="8"
+ y1="13"
+ x2="14"
+ y2="13"
+ id="line8"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-message-circle.svg b/src/assets/icons/tabler-icon-message-circle.svg
new file mode 100644
index 0000000..fc999bd
--- /dev/null
+++ b/src/assets/icons/tabler-icon-message-circle.svg
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-message-circle"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg12"
+ sodipodi:docname="tabler-icon-message-circle.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata18">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs16" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg12" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="M 3,20 4.3,16.1 A 9,8 0 1 1 7.7,19 L 3,20"
+ id="path4"
+ style="stroke:#ffeede" />
+ <line
+ x1="12"
+ y1="12"
+ x2="12"
+ y2="12.01"
+ id="line6"
+ style="stroke:#ffeede" />
+ <line
+ x1="8"
+ y1="12"
+ x2="8"
+ y2="12.01"
+ id="line8"
+ style="stroke:#ffeede" />
+ <line
+ x1="16"
+ y1="12"
+ x2="16"
+ y2="12.01"
+ id="line10"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-paperclip.svg b/src/assets/icons/tabler-icon-paperclip.svg
new file mode 100644
index 0000000..955e6fb
--- /dev/null
+++ b/src/assets/icons/tabler-icon-paperclip.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-paperclip"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg6"
+ sodipodi:docname="tabler-icon-paperclip.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata12">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs10" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="737"
+ inkscape:window-height="480"
+ id="namedview8"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg6" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="m 15,7 -6.5,6.5 a 2.1213203,2.1213203 0 0 0 3,3 L 18,10 A 4.2426407,4.2426407 0 0 0 12,4 l -6.5,6.5 a 6.363961,6.363961 0 0 0 9,9 L 21,13"
+ id="path4"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-photo.svg b/src/assets/icons/tabler-icon-photo.svg
new file mode 100644
index 0000000..eddf3cc
--- /dev/null
+++ b/src/assets/icons/tabler-icon-photo.svg
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-photo"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg12"
+ sodipodi:docname="tabler-icon-photo.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata18">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs16" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg12" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <line
+ x1="15"
+ y1="8"
+ x2="15.01"
+ y2="8"
+ id="line4"
+ style="stroke:#ffeede" />
+ <rect
+ x="4"
+ y="4"
+ width="16"
+ height="16"
+ rx="3"
+ id="rect6"
+ style="stroke:#ffeede" />
+ <path
+ d="m 4,15 4,-4 a 3,5 0 0 1 3,0 l 5,5"
+ id="path8"
+ style="stroke:#ffeede" />
+ <path
+ d="m 14,14 1,-1 a 3,5 0 0 1 3,0 l 2,2"
+ id="path10"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-search.svg b/src/assets/icons/tabler-icon-search.svg
new file mode 100644
index 0000000..abbd480
--- /dev/null
+++ b/src/assets/icons/tabler-icon-search.svg
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-search"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-search.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <circle
+ cx="10"
+ cy="10"
+ r="7"
+ id="circle4"
+ style="stroke:#ffeede" />
+ <line
+ x1="21"
+ y1="21"
+ x2="15"
+ y2="15"
+ id="line6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-settings.svg b/src/assets/icons/tabler-icon-settings.svg
new file mode 100644
index 0000000..c133334
--- /dev/null
+++ b/src/assets/icons/tabler-icon-settings.svg
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-settings"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-settings.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <path
+ d="m 10.325,4.317 c 0.426,-1.756 2.924,-1.756 3.35,0 a 1.724,1.724 0 0 0 2.573,1.066 c 1.543,-0.94 3.31,0.826 2.37,2.37 a 1.724,1.724 0 0 0 1.065,2.572 c 1.756,0.426 1.756,2.924 0,3.35 a 1.724,1.724 0 0 0 -1.066,2.573 c 0.94,1.543 -0.826,3.31 -2.37,2.37 a 1.724,1.724 0 0 0 -2.572,1.065 c -0.426,1.756 -2.924,1.756 -3.35,0 A 1.724,1.724 0 0 0 7.752,18.617 c -1.543,0.94 -3.31,-0.826 -2.37,-2.37 A 1.724,1.724 0 0 0 4.317,13.675 c -1.756,-0.426 -1.756,-2.924 0,-3.35 A 1.724,1.724 0 0 0 5.383,7.752 c -0.94,-1.543 0.826,-3.31 2.37,-2.37 1,0.608 2.296,0.07 2.572,-1.065 z"
+ id="path4"
+ style="stroke:#ffeede" />
+ <circle
+ cx="12"
+ cy="12"
+ r="3"
+ id="circle6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-trash.svg b/src/assets/icons/tabler-icon-trash.svg
new file mode 100644
index 0000000..eee68f7
--- /dev/null
+++ b/src/assets/icons/tabler-icon-trash.svg
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-trash"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg14"
+ sodipodi:docname="tabler-icon-trash.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata20">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs18" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview16"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg14" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <line
+ x1="4"
+ y1="7"
+ x2="20"
+ y2="7"
+ id="line4"
+ style="stroke:#ffeede" />
+ <line
+ x1="10"
+ y1="11"
+ x2="10"
+ y2="17"
+ id="line6"
+ style="stroke:#ffeede" />
+ <line
+ x1="14"
+ y1="11"
+ x2="14"
+ y2="17"
+ id="line8"
+ style="stroke:#ffeede" />
+ <path
+ d="m 5,7 1,12 a 2,2 0 0 0 2,2 h 8 a 2,2 0 0 0 2,-2 L 19,7"
+ id="path10"
+ style="stroke:#ffeede" />
+ <path
+ d="M 9,7 V 4 a 1,1 0 0 1 1,-1 h 4 a 1,1 0 0 1 1,1 v 3"
+ id="path12"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-trending-up.svg b/src/assets/icons/tabler-icon-trending-up.svg
new file mode 100644
index 0000000..c40ee92
--- /dev/null
+++ b/src/assets/icons/tabler-icon-trending-up.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-trending-up"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-trending-up.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <polyline
+ points="3 17 9 11 13 15 21 7"
+ id="polyline4"
+ style="stroke:#ffeede" />
+ <polyline
+ points="14 7 21 7 21 14"
+ id="polyline6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-user.svg b/src/assets/icons/tabler-icon-user.svg
new file mode 100644
index 0000000..37969ec
--- /dev/null
+++ b/src/assets/icons/tabler-icon-user.svg
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-user"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-user.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1053"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <circle
+ cx="12"
+ cy="7"
+ r="4"
+ id="circle4"
+ style="stroke:#ffeede" />
+ <path
+ d="m 6,21 v -2 a 4,4 0 0 1 4,-4 h 4 a 4,4 0 0 1 4,4 v 2"
+ id="path6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/icons/tabler-icon-x.svg b/src/assets/icons/tabler-icon-x.svg
new file mode 100644
index 0000000..28725c8
--- /dev/null
+++ b/src/assets/icons/tabler-icon-x.svg
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ class="icon icon-tabler icon-tabler-x"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ fill="none"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="tabler-icon-x.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="737"
+ inkscape:window-height="480"
+ id="namedview10"
+ showgrid="false"
+ inkscape:zoom="37.541667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg8" />
+ <path
+ stroke="none"
+ d="M0 0h24v24H0z"
+ fill="none"
+ id="path2" />
+ <line
+ x1="18"
+ y1="6"
+ x2="6"
+ y2="18"
+ id="line4"
+ style="stroke:#ffeede" />
+ <line
+ x1="6"
+ y1="6"
+ x2="18"
+ y2="18"
+ id="line6"
+ style="stroke:#ffeede" />
+</svg>
diff --git a/src/assets/images/comment.png b/src/assets/images/comment.png
deleted file mode 100644
index 5f8e8d9..0000000
--- a/src/assets/images/comment.png
+++ /dev/null
Binary files differ
diff --git a/src/assets/images/feed/chat-pic.png b/src/assets/images/feed/chat-pic.png
deleted file mode 100644
index 60241fa..0000000
--- a/src/assets/images/feed/chat-pic.png
+++ /dev/null
Binary files differ
diff --git a/src/assets/images/feed/profile-pic.png b/src/assets/images/feed/profile-pic.png
deleted file mode 100644
index 87f67f5..0000000
--- a/src/assets/images/feed/profile-pic.png
+++ /dev/null
Binary files differ
diff --git a/src/assets/images/paper-clip.png b/src/assets/images/paper-clip.png
deleted file mode 100644
index 46ce0a7..0000000
--- a/src/assets/images/paper-clip.png
+++ /dev/null
Binary files differ
diff --git a/src/interfaces/api-error.ts b/src/interfaces/api-error.ts
deleted file mode 100644
index 4dd68f3..0000000
--- a/src/interfaces/api-error.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface IApiError {
- type: string;
- title: string;
- status: number;
- traceId: string;
-}
diff --git a/src/models/comment.ts b/src/models/comment.model.ts
index 0d1755f..0d1755f 100644
--- a/src/models/comment.ts
+++ b/src/models/comment.model.ts
diff --git a/src/models/identity/friend.ts b/src/models/identity/friend.model.ts
index 22290cd..22290cd 100644
--- a/src/models/identity/friend.ts
+++ b/src/models/identity/friend.model.ts
diff --git a/src/models/identity/role.ts b/src/models/identity/role.model.ts
index 132b0b0..132b0b0 100644
--- a/src/models/identity/role.ts
+++ b/src/models/identity/role.model.ts
diff --git a/src/models/identity/user.ts b/src/models/identity/user.model.ts
index e0038e0..e2f54a4 100644
--- a/src/models/identity/user.ts
+++ b/src/models/identity/user.model.ts
@@ -1,8 +1,8 @@
import { Guid } from 'guid-typescript';
-import { Language } from '../language';
-import { Technology } from '../technology';
-import { Friend } from './friend';
-import { Role } from './role';
+import { Language } from '../language.model';
+import { Technology } from '../technology.model';
+import { Friend } from './friend.model';
+import { Role } from './role.model';
export class User {
private _id : Guid;
diff --git a/src/models/language.ts b/src/models/language.model.ts
index e3aa61e..e3aa61e 100644
--- a/src/models/language.ts
+++ b/src/models/language.model.ts
diff --git a/src/models/post-comment.ts b/src/models/post-comment.model.ts
index 5d1e346..5d1e346 100644
--- a/src/models/post-comment.ts
+++ b/src/models/post-comment.model.ts
diff --git a/src/models/post.ts b/src/models/post.model.ts
index 8e58bea..9751c26 100644
--- a/src/models/post.ts
+++ b/src/models/post.model.ts
@@ -1,6 +1,6 @@
import { Guid } from 'guid-typescript';
-import { Comment } from './comment';
-import { PostComment } from './post-comment';
+import { Comment } from './comment.model';
+import { PostComment } from './post-comment.model';
export class Post {
private _postId: Guid;
@@ -11,8 +11,9 @@ export class Post {
private _timeCreated: Date;
private _comments: PostComment[];
private _fileURLs: string[];
+ private _currentRating: number;
- constructor(postId: Guid, creatorFirstName: string, creatorLastName: string, creatorUsername: string, message: string, timeCreated: Date, comments: PostComment[], fileURLs: string[]) {
+ constructor(postId: Guid, creatorFirstName: string, creatorLastName: string, creatorUsername: string, message: string, timeCreated: Date, comments: PostComment[], fileURLs: string[], currentRating: number) {
this.postId = postId;
this.creatorFirstName = creatorFirstName;
this.creatorLastName = creatorLastName;
@@ -21,6 +22,7 @@ export class Post {
this.timeCreated = timeCreated;
this.comments = comments;
this.fileURLs = fileURLs;
+ this.currentRating = currentRating;
}
public get postId(): Guid {
@@ -78,4 +80,11 @@ export class Post {
public set fileURLs(v: string[]) {
this._fileURLs = v;
}
+
+ public get currentRating(): number {
+ return this._currentRating;
+ }
+ public set currentRating(v: number) {
+ this._currentRating = v;
+ }
}
diff --git a/src/models/technology.ts b/src/models/technology.model.ts
index 1869d14..1869d14 100644
--- a/src/models/technology.ts
+++ b/src/models/technology.model.ts
diff --git a/src/styles.css b/src/styles.css
index eeb93fe..c9a4f25 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -2,44 +2,62 @@
@import "./reset.css";
:root {
- --bg-color: white;
- --focus-color: forestgreen;
- --card-bg: white;
- --success: forestgreen;
+ --max-width: 40rem;
+ --bg-color: #18191a;
+ --fg-color: #ffeede;
+ --focus-color: #669908;
+ --card-bg: #303030;
+ --card-padding: 0.5em;
+ --success: #669908;
--failure: indianred;
+ --faded-color: #696969;
+ --navbar-height: 2.6em;
+ --upvote-highlight: #999908;
+ --downvote-highlight: #c35b5b;
}
-html, body {
- height: 100%;
- margin: 0;
+html,
+body {
+ height: 100%;
+ margin: 0;
}
body {
font: 21px sans-serif !important;
+ color: var(--fg-color);
background-color: var(--bg-color);
}
-input:focus, button:focus {
- outline: 0;
+button, textarea, input {
+ color: inherit;
+ background: inherit;
+ background-color: inherit;
+ font: inherit;
+ padding: 0;
+ border: none;
}
-#content { /* Used for the login and register pages */
- height: 100%;
- max-width: 20em;
- box-sizing: border-box;
- border: .5em solid var(--bg-color);
+input:focus,
+button:focus,
+textarea:focus {
+ outline: 0;
+}
- margin: 0 auto;
+ /* We can't do ...::-webkit-file-upload-button, ...::file-selector-button {...}
+ * for some reason, so they have to be separated (some browsers break)
+ */
+input[type=file]::-webkit-file-upload-button {
+ visibility: hidden;
+}
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: column;
+input[type=file]::file-selector-button {
+ visibility: hidden;
}
+
.rounded-border {
border: 2px solid black;
- border-radius: .6em;
- padding: .4em;
+ border-radius: 0.6em;
+ padding: 0.4em;
}
.round-image {
@@ -50,6 +68,7 @@ input:focus, button:focus {
.title {
font-size: 2em;
font-weight: bold;
+ text-align: center;
}
.error {
@@ -60,101 +79,405 @@ input:focus, button:focus {
width: 100%;
max-height: 100%;
overflow-y: auto;
+ flex-wrap: nowrap !important;
}
- /* Hide scrollbar for Chrome, Safari and Opera */
+/* Hide scrollbar for Chrome, Safari and Opera */
.scroll-standalone::-webkit-scrollbar {
display: none;
}
- /* Hide scrollbar for IE, Edge and Firefox */
+/* Hide scrollbar for IE, Edge and Firefox */
.scroll-standalone {
- -ms-overflow-style: none; /* IE and Edge */
- scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
}
-.user-language, .user-technology {
- border-radius: .4em;
+.user-language,
+.user-technology {
+ border-radius: 0.4em;
background-color: lightgrey;
- padding: .26em;
- margin: .1em .2em;
+ padding: 0.26em;
+ margin: 0.1em 0.2em;
width: fit-content;
}
-/* Inputs, the type found in login and register */
+/* Cards */
-.input-selection {
+.card {
+ margin: 0.5em auto;
+ box-sizing: border-box;
+ padding: var(--card-padding);
+ background-color: var(--card-bg);
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;
+@media screen and (max-width: 30rem) {
+ .card {
+ font-size: 0.8em;
+ }
+
+ .card button {
+ font-size: 0.9em;
+ }
+
+ .title {
+ font-size: 1.5em;
+ }
+
+ .input-selection:first-child {
+ margin-top: 1.2em !important;
+ }
+
+ :not(card) button {
+ font-size: 0.7em;
+ }
+}
+
+.card-hr {
+ width: calc(100% - 1em);
+ border: 1px solid var(--fg-color);
+ background-color: var(--fg-color);
+}
+
+/* General flex */
+
+.flex-row, .flex-col {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.flex-no-wrap {
+ flex-wrap: nowrap;
+}
+
+.flex-row {
+ flex-direction: row;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.flexible, .flexible-children > * {
+ flex: 1;
+}
+
+.flex-center-align-items {
+ align-items: center;
+}
+
+.flex-justify-start {
+ justify-content: flex-start;
+}
+
+.flex-justify-end {
+ justify-content: flex-end;
+}
+
+.flex-justify-center {
+ justify-content: center;
+}
+
+.align-children-center > * {
+ display: flex;
+ align-items: center;
+}
+
+.justify-children-center > * {
+ display: flex;
+ justify-content: center;
+}
+
+/* General font sizes */
+
+.font-size-dot9 {
+ font-size: 0.9em;
+}
+
+.font-size-dot8 {
+ font-size: 0.8em;
+}
+
+.font-size-dot7 {
+ font-size: 0.7em;
+}
+
+/* General text */
+
+.text-centered {
+ text-align: center;
+}
+
+.text-vertical-middle {
+ vertical-align: middle;
+}
+
+/* General border */
+
+.border-faded-slim {
+ border: 1px solid var(--faded-color);
+}
+
+.border-radius-dot5r, .card {
+ border-radius: 0.5rem;
+}
+
+.border-radius-dot3 {
+ border-radius: 0.3em;
+}
+
+.border-radius-dot2 {
+ border-radius: 0.2em;
+}
+
+.border-bottom-only {
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
+/* General colors */
+
+.fg-faded {
+ color: var(--faded-color);
+}
+
+.fg-focus {
+ color: var(--focus-color);
+ border-color: var(--focus-color);
+}
+
+.fg-error {
+ color: var(--failure);
+ border-color: var(--failure);
+}
+
+/* General padding */
+
+.padding-dot6 {
+ padding: 0.6em;
+}
+
+.padding-dot5 {
+ padding: 0.5em;
+}
+
+.padding-dot4 {
+ padding: 0.4em;
+}
+
+.padding-dot3 {
+ padding: 0.3em;
+}
+
+.padding-dot2 {
+ padding: 0.2em;
+}
+
+.padding-dot1 {
+ padding: 0.1em;
+}
+
+.padding-right-1 {
+ padding-right: 1em !important;
+}
+
+.padding-right-1dot5 {
+ padding-right: 1.5em !important;
}
-.input-field {
+.padding-side-font {
+ padding-right: 1em;
+ padding-left: 1em;
+}
+
+.padding-side-dot3 {
+ padding-right: 0.3em;
+ padding-left: 0.3em;
+}
+
+.padding-bot-dot5 {
+ padding-bottom: 0.5em;
+}
+
+.padding-top-bot-dot5 {
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.padding-top-bot-dot2 {
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+}
+
+/* Margin */
+
+.margin-0 {
+ margin: 0;
+}
+
+.margin-top-0 {
+ margin-top: 0;
+}
+
+.margin-top-dot4 {
+ margin-top: 0.4em;
+}
+
+.margin-top-dot5 {
+ margin-top: 0.5em;
+}
+
+.margin-bot-dot5 {
+ margin-bottom: 0.5em;
+}
+
+.margin-top-bot-dot7 {
+ margin-top: 0.7em;
+ margin-bottom: 0.7em;
+}
+
+.margin-top-bot-dot3 {
+ margin-top: 0.3em;
+ margin-bottom: 0.3em;
+}
+
+.margin-top-bot-dot2 {
+ margin-top: 0.2em;
+ margin-bottom: 0.2em;
+}
+
+.margin-right-dot2 {
+ margin-right: 0.2em;
+}
+
+/* Effects */
+
+.lighter-hover:hover, .light-hover:hover, .hover-half-opacity:hover {
+ cursor: pointer;
+}
+
+.lighter-hover:hover {
+ background: #424242;
+}
+
+.light-hover:hover {
+ background: #696969;
+}
+
+.hover-half-opacity:hover {
+ opacity: 0.5;
+}
+
+.click-effect:active {
+ transform: scale(0.9);
+}
+
+/* Size */
+
+.height-full {
+ height: 100%;
+}
+
+.width-full {
width: 100%;
- background-color: var(--bg-color);
+}
+
+.height-font {
+ height: 1em;
+}
+
+/* Misc */
- border: 0;
- border-bottom: 1px solid grey;
+.centered-content {
+ max-width: var(--max-width);
+ margin: 0 auto;
box-sizing: border-box;
+}
- margin-bottom: .5em;
- padding: .4em;
- padding-left: 0;
+.under-navbar {
+ height: calc(100% - var(--navbar-height)) !important;
+}
+
+/* Inputs, the type found in login and register */
+
+.textarea-new-msg {
+ min-height: 2em;
+ resize: vertical;
+ box-sizing: border-box;
+}
- font-size: inherit;
+.fancy-input {
+ box-sizing: border-box;
+ margin-bottom: 0.5em;
+ padding: 0.4em;
+ padding-left: 0;
}
-.input-field-label {
+.fancy-input-label {
width: inherit;
height: inherit;
position: absolute;
left: 0;
- margin-top: .4em;
+ margin-top: 0.4em;
+ transition: 0.2s;
+ scale: 1em;
+}
+
+.input-selection {
+ position: relative;
+ margin-top: 0.5em;
+}
+
+.input-selection input::placeholder {
+ /* Don't show the placeholder when the label is on top */
+ opacity: 0;
color: grey;
}
+.input-selection:hover > input::placeholder,
+.input-selection > input:focus::placeholder {
+ /* When hovering on the input, or when having clicked on it,
+ * show the placeholder
+ */
+ opacity: 1;
+}
+
+.input-selection:hover > label,
+.input-selection > input:not(:placeholder-shown) + label,
+.input-selection > input:focus + label {
/* 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;
+ font-size: 0.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 {
+.input-selection:hover > input,
+.input-selection > input:focus {
+ /* When hovering or clicked on it, make it the focus color */
border-color: var(--focus-color) !important;
caret-color: var(--focus-color);
- border-width: 2px !important;
- margin-top: -1px !important;
+ /* When hovering or clicked on it, make the bottom border wider (taller) */
+ border-width: 2px;
+ padding-bottom: calc(0.4em - 1px);
}
+.show-password-button {
+ position: absolute;
+ right: 0.2em;
+ bottom: 1.1em;
+ height: 1em;
+}
/* Input errors */
.input-errors {
- margin-top: -.8em;
- font-size: .7em;
+ margin-top: -0.8em;
+ font-size: 0.7em;
position: absolute;
right: 0;
@@ -175,7 +498,7 @@ input:focus, button:focus {
}
.input-errors > .error {
- margin-left: .3em;
+ margin-left: 0.3em;
}
.input-field:focus ~ .input-errors > .error {
@@ -186,77 +509,8 @@ input:focus, button:focus {
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;
+ background-color: #424242;
}
-
-.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
index a87ae45..0194061 100644
--- a/src/theme.scss
+++ b/src/theme.scss
@@ -1,14 +1,2 @@
// 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