aboutsummaryrefslogtreecommitdiff
path: root/src/DevHive.Angular
diff options
context:
space:
mode:
authorSyndamia <kamen.d.mladenov@protonmail.com>2021-01-22 15:07:18 +0200
committerSyndamia <kamen.d.mladenov@protonmail.com>2021-01-22 15:07:53 +0200
commite161f444c64517f644613e3d4e4507c06ce108e0 (patch)
tree4c9de6f6992bb0177167f5b7e130750cc69751aa /src/DevHive.Angular
parentc8e5dac43004ca5f38b8224d69d76c8613c5eb43 (diff)
downloadDevHive-e161f444c64517f644613e3d4e4507c06ce108e0.tar
DevHive-e161f444c64517f644613e3d4e4507c06ce108e0.tar.gz
DevHive-e161f444c64517f644613e3d4e4507c06ce108e0.zip
Requests to user (in feed, login, register, profile, profile-settings) now properly wait for the data to get loaded (instead of having a hard coded timer); Substantial improvement over how these pages handle errors; Login page shows request error message
Diffstat (limited to 'src/DevHive.Angular')
-rw-r--r--src/DevHive.Angular/src/app/app-constants.module.ts2
-rw-r--r--src/DevHive.Angular/src/app/app-routing.module.ts1
-rw-r--r--src/DevHive.Angular/src/app/app.module.ts4
-rw-r--r--src/DevHive.Angular/src/app/components/feed/feed.component.ts28
-rw-r--r--src/DevHive.Angular/src/app/components/login/login.component.html2
-rw-r--r--src/DevHive.Angular/src/app/components/login/login.component.ts19
-rw-r--r--src/DevHive.Angular/src/app/components/profile-settings/profile-settings.component.ts65
-rw-r--r--src/DevHive.Angular/src/app/components/profile/profile.component.ts54
-rw-r--r--src/DevHive.Angular/src/app/components/register/register.component.ts14
-rw-r--r--src/DevHive.Angular/src/app/services/user.service.ts116
-rw-r--r--src/DevHive.Angular/src/styles.css14
11 files changed, 198 insertions, 121 deletions
diff --git a/src/DevHive.Angular/src/app/app-constants.module.ts b/src/DevHive.Angular/src/app/app-constants.module.ts
index 9ce8896..2215abd 100644
--- a/src/DevHive.Angular/src/app/app-constants.module.ts
+++ b/src/DevHive.Angular/src/app/app-constants.module.ts
@@ -4,7 +4,5 @@ export class AppConstants {
public static API_USER_LOGIN_URL = AppConstants.API_USER_URL + '/login';
public static API_USER_REGISTER_URL = AppConstants.API_USER_URL + '/register';
- public static FETCH_TIMEOUT = 500;
-
public static FALLBACK_PROFILE_ICON = 'assets/images/feed/profile-pic.png';
}
diff --git a/src/DevHive.Angular/src/app/app-routing.module.ts b/src/DevHive.Angular/src/app/app-routing.module.ts
index c511fe2..d67ad8f 100644
--- a/src/DevHive.Angular/src/app/app-routing.module.ts
+++ b/src/DevHive.Angular/src/app/app-routing.module.ts
@@ -13,6 +13,7 @@ const routes: Routes = [
{ path: 'register', component: RegisterComponent },
{ path: 'profile/:username', component: ProfileComponent },
{ path: 'profile/:username/settings', component: ProfileSettingsComponent },
+ { path: 'error', component: ErrorComponent },
{ path: '**', component: ErrorComponent }
];
diff --git a/src/DevHive.Angular/src/app/app.module.ts b/src/DevHive.Angular/src/app/app.module.ts
index 8591724..80a9583 100644
--- a/src/DevHive.Angular/src/app/app.module.ts
+++ b/src/DevHive.Angular/src/app/app.module.ts
@@ -5,6 +5,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
+import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -36,7 +37,8 @@ import { LoadingComponent } from './components/loading/loading.component';
BrowserAnimationsModule,
MatFormFieldModule,
MatInputModule,
- MatButtonModule
+ MatButtonModule,
+ HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
diff --git a/src/DevHive.Angular/src/app/components/feed/feed.component.ts b/src/DevHive.Angular/src/app/components/feed/feed.component.ts
index aa8599d..b027e5b 100644
--- a/src/DevHive.Angular/src/app/components/feed/feed.component.ts
+++ b/src/DevHive.Angular/src/app/components/feed/feed.component.ts
@@ -5,6 +5,7 @@ import { User } from 'src/models/identity/user';
import { PostComponent } from '../post/post.component';
import { UserService } from '../../services/user.service';
import { AppConstants } from 'src/app/app-constants.module';
+import {HttpErrorResponse} from '@angular/common/http';
@Component({
selector: 'app-feed',
@@ -22,6 +23,7 @@ export class FeedComponent implements OnInit {
}
ngOnInit(): void {
+ this.user = this._userService.getDefaultUser();
this.posts = [
new PostComponent(),
new PostComponent(),
@@ -30,20 +32,28 @@ export class FeedComponent implements OnInit {
];
if (sessionStorage.getItem('UserCred')) {
- // Workaround for waiting the fetch response
- // TODO: properly wait for it, before loading the page contents
- setTimeout(() => { this.user = this._userService.fetchUserFromSessionStorage(); }, AppConstants.FETCH_TIMEOUT);
- setTimeout(() => {
- this.dataArrived = true;
- if (this.user.imageUrl === '') {
- this.user.imageUrl = AppConstants.FALLBACK_PROFILE_ICON;
- }
- }, AppConstants.FETCH_TIMEOUT + 100);
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (res: object) => this.finishUserLoading(res),
+ (err: HttpErrorResponse) => this.bailOnBadToken()
+ );
} else {
this._router.navigate(['/login']);
}
}
+ private finishUserLoading(res: object): void {
+ Object.assign(this.user, res);
+ if (this.user.imageUrl === '') {
+ this.user.imageUrl = AppConstants.FALLBACK_PROFILE_ICON;
+ }
+ this.dataArrived = true;
+ }
+
+ private bailOnBadToken(): void {
+ this._userService.logoutUserFromSessionStorage();
+ this._router.navigate(['/login']);
+ }
+
goToProfile(): void {
this._router.navigate(['/profile/' + this.user.userName]);
}
diff --git a/src/DevHive.Angular/src/app/components/login/login.component.html b/src/DevHive.Angular/src/app/components/login/login.component.html
index 166799f..c38bc34 100644
--- a/src/DevHive.Angular/src/app/components/login/login.component.html
+++ b/src/DevHive.Angular/src/app/components/login/login.component.html
@@ -1,3 +1,5 @@
+<div id="error-bar">{{errorMsg}}</div>
+
<div id="content">
<div class="title">Login</div>
diff --git a/src/DevHive.Angular/src/app/components/login/login.component.ts b/src/DevHive.Angular/src/app/components/login/login.component.ts
index bde7a09..e83f4b1 100644
--- a/src/DevHive.Angular/src/app/components/login/login.component.ts
+++ b/src/DevHive.Angular/src/app/components/login/login.component.ts
@@ -1,9 +1,10 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, ErrorHandler, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControl, AbstractControl } from '@angular/forms';
import { Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { UserService } from 'src/app/services/user.service';
import {AppConstants} from 'src/app/app-constants.module';
+import {HttpErrorResponse, HttpResponse} from '@angular/common/http';
@Component({
selector: 'app-login',
@@ -12,6 +13,7 @@ import {AppConstants} from 'src/app/app-constants.module';
})
export class LoginComponent implements OnInit {
private _title = 'Login';
+ errorMsg = ''
loginUserFormGroup: FormGroup;
constructor(private _titleService: Title, private _fb: FormBuilder, private _router: Router, private _userService: UserService) {
@@ -30,8 +32,19 @@ export class LoginComponent implements OnInit {
}
onSubmit(): void {
- setTimeout(() => { this._userService.loginUser(this.loginUserFormGroup); }, AppConstants.FETCH_TIMEOUT);
- setTimeout(() => { this._router.navigate(['/']); }, AppConstants.FETCH_TIMEOUT + 100);
+ this._userService.loginUserRequest(this.loginUserFormGroup).subscribe(
+ res => this.finishLogin(res),
+ (err: HttpErrorResponse) => this.showError(err)
+ );
+ }
+
+ private finishLogin(res: object): void {
+ this._userService.setUserTokenToSessionStorage(res);
+ this._router.navigate(['/']);
+ }
+
+ private showError(error: HttpErrorResponse): void {
+ this.errorMsg = error.message;
}
onRedirectRegister(): void {
diff --git a/src/DevHive.Angular/src/app/components/profile-settings/profile-settings.component.ts b/src/DevHive.Angular/src/app/components/profile-settings/profile-settings.component.ts
index 3d7305f..b278e42 100644
--- a/src/DevHive.Angular/src/app/components/profile-settings/profile-settings.component.ts
+++ b/src/DevHive.Angular/src/app/components/profile-settings/profile-settings.component.ts
@@ -1,3 +1,4 @@
+import {HttpErrorResponse} from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import {Router} from '@angular/router';
import {AppConstants} from 'src/app/app-constants.module';
@@ -10,6 +11,7 @@ import {User} from 'src/models/identity/user';
styleUrls: ['./profile-settings.component.css']
})
export class ProfileSettingsComponent implements OnInit {
+ private _urlUsername: string;
public dataArrived = false;
public user: User;
@@ -17,34 +19,49 @@ export class ProfileSettingsComponent implements OnInit {
{ }
ngOnInit(): void {
- let username = this._router.url.substring(9);
- username = username.substring(0, username.length - 9);
+ this._urlUsername = this._router.url.substring(9)
+ this._urlUsername = this._urlUsername.substring(0, this._urlUsername.length - 9);
+ this.user = this._userService.getDefaultUser();
+
+ this._userService.getUserByUsernameRequest(this._urlUsername).subscribe(
+ (res: object) => this.finishUserLoading(res),
+ (err: HttpErrorResponse) => { this._router.navigate(['/error']); }
+ );
+ }
+
+ private finishUserLoading(res: object): void {
+ Object.assign(this.user, res);
+ if (this.user.imageUrl === '') {
+ this.user.imageUrl = AppConstants.FALLBACK_PROFILE_ICON;
+ }
if (sessionStorage.getItem('UserCred')) {
- // Workaround for waiting the fetch response
- // TODO: properly wait for it, before loading the page contents
- setTimeout(() => {
- this.user = this._userService.fetchUserFromSessionStorage();
- }, AppConstants.FETCH_TIMEOUT);
+ const userFromToken: User = this._userService.getDefaultUser();
- // After getting the user, check if we're on the profile page of the logged in user
- setTimeout(() => {
- if (this.user.userName !== username) {
- this.goToProfile();
- } else if (this.user.imageUrl === '') {
- this.user.imageUrl = AppConstants.FALLBACK_PROFILE_ICON;
- }
- }, AppConstants.FETCH_TIMEOUT + 50);
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (tokenRes: object) => {
+ Object.assign(userFromToken, tokenRes);
- setTimeout(() => {
- this.dataArrived = true;
- }, AppConstants.FETCH_TIMEOUT + 100);
+ if (userFromToken.userName === this._urlUsername) {
+ this.dataArrived = true;
+ }
+ else {
+ this.goToProfile();
+ }
+ },
+ (err: HttpErrorResponse) => this.bailOnBadToken()
+ );
}
else {
this.goToProfile();
}
}
+ private bailOnBadToken(): void {
+ this._userService.logoutUserFromSessionStorage();
+ this._router.navigate(['/login']);
+ }
+
goToProfile(): void {
this._router.navigate([this._router.url.substring(0, this._router.url.length - 9)]);
}
@@ -59,10 +76,12 @@ export class ProfileSettingsComponent implements OnInit {
}
deleteAccount(): void {
- setTimeout(() => { this._userService.deleteUserRequest(this._userService.getUserIdFromSessionStroageToken()); }, AppConstants.FETCH_TIMEOUT);
- setTimeout(() => {
- this._userService.logoutUserFromSessionStorage();
- this._router.navigate(['/login']);
- }, AppConstants.FETCH_TIMEOUT + 100);
+ this._userService.deleteUserFromSessionStorageRequest().subscribe(
+ (res: object) => {
+ this._userService.logoutUserFromSessionStorage();
+ this._router.navigate(['/login']);
+ },
+ (err: HttpErrorResponse) => console.log(err)
+ );
}
}
diff --git a/src/DevHive.Angular/src/app/components/profile/profile.component.ts b/src/DevHive.Angular/src/app/components/profile/profile.component.ts
index 9eb6e91..bfa3e3d 100644
--- a/src/DevHive.Angular/src/app/components/profile/profile.component.ts
+++ b/src/DevHive.Angular/src/app/components/profile/profile.component.ts
@@ -3,6 +3,7 @@ import { Router } from '@angular/router';
import { UserService } from 'src/app/services/user.service';
import { User } from 'src/models/identity/user';
import { AppConstants } from 'src/app/app-constants.module';
+import {HttpErrorResponse} from '@angular/common/http';
@Component({
selector: 'app-profile',
@@ -10,6 +11,7 @@ import { AppConstants } from 'src/app/app-constants.module';
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
+ private _urlUsername: string;
public loggedInUser = false;
public dataArrived = false;
public user: User;
@@ -22,34 +24,44 @@ export class ProfileComponent implements OnInit {
}
ngOnInit(): void {
- const username = this._router.url.substring(9);
+ this._urlUsername = this._router.url.substring(9);
+ this.user = this._userService.getDefaultUser();
+
+ this._userService.getUserByUsernameRequest(this._urlUsername).subscribe(
+ (res: object) => this.finishUserLoading(res),
+ (err: HttpErrorResponse) => { this._router.navigate(['/error']); }
+ );
+ }
+
+ private finishUserLoading(res: object): void {
+ Object.assign(this.user, res);
+ if (this.user.imageUrl === '') {
+ this.user.imageUrl = AppConstants.FALLBACK_PROFILE_ICON;
+ }
if (sessionStorage.getItem('UserCred')) {
- // Workaround for waiting the fetch response
- // TODO: properly wait for it, before loading the page contents
- setTimeout(() => {
- this.user = this._userService.fetchUserFromSessionStorage();
- }, AppConstants.FETCH_TIMEOUT);
+ const userFromToken: User = this._userService.getDefaultUser();
- // After getting the user, check if we're on the profile page of the logged in user
- setTimeout(() => {
- if (this.user.userName !== username) {
- this.setDefaultUser();
- } else {
- if (this.user.imageUrl === '') {
- this.user.imageUrl = AppConstants.FALLBACK_PROFILE_ICON;
- }
- this.loggedInUser = true;
- }
- }, AppConstants.FETCH_TIMEOUT + 50);
+ this._userService.getUserFromSessionStorageRequest().subscribe(
+ (tokenRes: object) => {
+ Object.assign(userFromToken, tokenRes);
+
+ if (userFromToken.userName === this._urlUsername) {
+ this.loggedInUser = true;
+ }
+ this.dataArrived = true;
+ },
+ (err: HttpErrorResponse) => this.bailOnBadToken()
+ );
}
else {
- this.setDefaultUser();
+ this.dataArrived = true;
}
+ }
- setTimeout(() => {
- this.dataArrived = true;
- }, AppConstants.FETCH_TIMEOUT + 100);
+ private bailOnBadToken(): void {
+ this._userService.logoutUserFromSessionStorage();
+ this._router.navigate(['/login']);
}
goBack(): void {
diff --git a/src/DevHive.Angular/src/app/components/register/register.component.ts b/src/DevHive.Angular/src/app/components/register/register.component.ts
index 3fdfbcd..d1d6af0 100644
--- a/src/DevHive.Angular/src/app/components/register/register.component.ts
+++ b/src/DevHive.Angular/src/app/components/register/register.component.ts
@@ -1,3 +1,4 @@
+import {HttpErrorResponse} from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
@@ -46,10 +47,21 @@ export class RegisterComponent implements OnInit {
}
onSubmit(): void {
- this._userService.registerUser(this.registerUserFormGroup);
+ this._userService.registerUserRequest(this.registerUserFormGroup).subscribe(
+ res => this.finishRegister(res),
+ (err: HttpErrorResponse) => this.showError(err)
+ );
+ }
+
+ private finishRegister(res: object): void {
+ this._userService.setUserTokenToSessionStorage(res);
this._router.navigate(['/']);
}
+ private showError(error: HttpErrorResponse): void {
+ // TODO: implement, holding out until tab-bar component is implemented
+ }
+
onRedirectRegister(): void {
this._router.navigate(['/login']);
}
diff --git a/src/DevHive.Angular/src/app/services/user.service.ts b/src/DevHive.Angular/src/app/services/user.service.ts
index 7cf574b..5bd26e9 100644
--- a/src/DevHive.Angular/src/app/services/user.service.ts
+++ b/src/DevHive.Angular/src/app/services/user.service.ts
@@ -6,96 +6,90 @@ import { IJWTPayload } from '../../interfaces/jwt-payload';
import { IUserCredentials } from '../../interfaces/user-credentials';
import { FormGroup } from '@angular/forms';
import { AppConstants } from 'src/app/app-constants.module';
+import {HttpClient, HttpErrorResponse, HttpHeaders, HttpParams} from '@angular/common/http';
+import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UserService {
- constructor() { }
+ constructor(private http: HttpClient) { }
getDefaultUser(): User {
return new User(Guid.createEmpty(), 'gosho_trapov', 'Gosho', 'Trapov', AppConstants.FALLBACK_PROFILE_ICON);
}
- getUserIdFromSessionStroageToken(): Guid {
- const jwt: IJWTPayload = JSON.parse(sessionStorage.getItem('UserCred') ?? '');
+ getUserIdFromSessionStorageToken(): Guid {
+ const jwt: IJWTPayload = { token: sessionStorage.getItem('UserCred') ?? '' };
const userCred = jwt_decode<IUserCredentials>(jwt.token);
return userCred.ID;
}
- fetchUserFromSessionStorage(): User {
+ setUserTokenToSessionStorage(response: object): void {
+ const token = JSON.stringify(response);
+ sessionStorage.setItem('UserCred', token.substr(10, token.length - 12));
+ }
+
+ getUserFromSessionStorageRequest(): Observable<object> {
// Get the token and userid from session storage
- const jwt: IJWTPayload = JSON.parse(sessionStorage.getItem('UserCred') ?? '');
+ const jwt: IJWTPayload = { token: sessionStorage.getItem('UserCred') ?? '' };
const userCred = jwt_decode<IUserCredentials>(jwt.token);
- return this.fetchUser(userCred.ID, jwt.token);
+ return this.getUserRequest(userCred.ID, jwt.token);
}
- fetchUser(userId: Guid, authToken: string): User {
- const fetchedUser: User = new User(Guid.createEmpty(), '', '', '', '');
+ deleteUserFromSessionStorageRequest(): Observable<object> {
+ // Get the token and userid from session storage
+ const jwt: IJWTPayload = { token: sessionStorage.getItem('UserCred') ?? '' };
+ const userCred = jwt_decode<IUserCredentials>(jwt.token);
- // Fetch the data and assign it to fetchedUser
- fetch(AppConstants.API_USER_URL + '?Id=' + userId, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Bearer ' + authToken
- }
- }).then(response => response.json())
- .then(data => Object.assign(fetchedUser, data));
+ return this.deleteUserRequest(userCred.ID, jwt.token);
+ }
- return fetchedUser;
+ logoutUserFromSessionStorage(): void {
+ sessionStorage.removeItem('UserCred');
}
- // TODO: make return bool when the response is an error
- loginUser(loginUserFormGroup: FormGroup): void {
- // Save the fetch reponse in the sessionStorage
- fetch(AppConstants.API_USER_LOGIN_URL, {
- method: 'POST',
- body: JSON.stringify({
- UserName: loginUserFormGroup.get('username')?.value,
- Password: loginUserFormGroup.get('password')?.value
- }),
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then(response => response.json())
- .then(data => sessionStorage.setItem('UserCred', JSON.stringify(data)));
+ loginUserRequest(loginUserFormGroup: FormGroup): Observable<object> {
+ const body = {
+ UserName: loginUserFormGroup.get('username')?.value,
+ Password: loginUserFormGroup.get('password')?.value
+ };
+ return this.http.post(AppConstants.API_USER_LOGIN_URL, body);
}
- // TODO: make return bool when the response is an error
- registerUser(registerUserFormGroup: FormGroup): void {
- // TODO: add a check for form data validity
+ registerUserRequest(registerUserFormGroup: FormGroup): Observable<object> {
+ // TODO?: add a check for form data validity
+ const body = {
+ UserName: registerUserFormGroup.get('username')?.value,
+ Email: registerUserFormGroup.get('email')?.value,
+ FirstName: registerUserFormGroup.get('firstName')?.value,
+ LastName: registerUserFormGroup.get('lastName')?.value,
+ Password: registerUserFormGroup.get('password')?.value
+ };
+ return this.http.post(AppConstants.API_USER_REGISTER_URL, body);
+ }
- // Like in login, save the fetch reponse in the sessionStorage
- fetch(AppConstants.API_USER_REGISTER_URL, {
- method: 'POST',
- body: JSON.stringify({
- UserName: registerUserFormGroup.get('username')?.value,
- Email: registerUserFormGroup.get('email')?.value,
- FirstName: registerUserFormGroup.get('firstName')?.value,
- LastName: registerUserFormGroup.get('lastName')?.value,
- Password: registerUserFormGroup.get('password')?.value
- }),
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then(response => response.json())
- .then(data => sessionStorage.setItem('UserCred', JSON.stringify(data)));
+ getUserRequest(userId: Guid, authToken: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this.http.get(AppConstants.API_USER_URL, options);
}
- logoutUserFromSessionStorage(): void {
- sessionStorage.removeItem('UserCred');
+ getUserByUsernameRequest(username: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('UserName', username),
+ };
+ return this.http.get(AppConstants.API_USER_URL + '/GetUser', options);
}
- deleteUserRequest(id: Guid): void {
- const jwt = JSON.parse(sessionStorage.getItem('UserCred') ?? '');
- fetch(AppConstants.API_USER_URL + '?Id=' + id, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Bearer ' + jwt.token
- }
- });
+ deleteUserRequest(userId: Guid, authToken: string): Observable<object> {
+ const options = {
+ params: new HttpParams().set('Id', userId.toString()),
+ headers: new HttpHeaders().set('Authorization', 'Bearer ' + authToken)
+ };
+ return this.http.delete(AppConstants.API_USER_URL, options);
}
}
diff --git a/src/DevHive.Angular/src/styles.css b/src/DevHive.Angular/src/styles.css
index 95592a1..1881272 100644
--- a/src/DevHive.Angular/src/styles.css
+++ b/src/DevHive.Angular/src/styles.css
@@ -35,6 +35,20 @@ input:focus, button:focus {
flex-direction: column;
}
+#error-bar {
+ text-align: center;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ background-color: indianred;
+ color: white;
+ padding: .2em;
+}
+
+#error-bar:empty {
+ display: none;
+}
+
.rounded-border {
border: 2px solid black;
border-radius: .6em;