Skip to content

Commit

Permalink
add support for stackoverflow autocompletion when adding new bookmark
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrian Matei committed Dec 9, 2019
1 parent 72cf57a commit 1c471a3
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 65 deletions.
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "bookmarks.dev",
"version": "8.5.0",
"version": "8.6.0",
"license": "MIT",
"scripts": {
"ng": "ng",
Expand Down
1 change: 1 addition & 0 deletions src/app/core/model/bookmark.ts
Expand Up @@ -17,4 +17,5 @@ export interface Bookmark {
starredBy?: string[];
likes?: number;
youtubeVideoId?: string;
stackoverflowQuestionId?: string;
}
1 change: 1 addition & 0 deletions src/app/core/model/webpage-data.ts
@@ -1,5 +1,6 @@
export interface WebpageData {
title: string;
metaDescription: string;
tags: string[];
publishedOn?: Date;
}
172 changes: 116 additions & 56 deletions src/app/personal/create/create-personal-bookmark.component.ts
@@ -1,27 +1,28 @@
import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Bookmark } from '../../core/model/bookmark';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { MarkdownService } from '../markdown.service';
import { KeycloakService } from 'keycloak-angular';
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { MatAutocompleteSelectedEvent, MatChipInputEvent } from '@angular/material';
import { Observable, throwError as observableThrowError } from 'rxjs';
import { languages } from '../../shared/language-options';
import { tagsValidator } from '../../shared/tags-validation.directive';
import { PublicBookmarksStore } from '../../public/bookmarks/store/public-bookmarks-store.service';
import { PublicBookmarksService } from '../../public/bookmarks/public-bookmarks.service';
import { descriptionSizeValidator } from '../../shared/description-size-validation.directive';
import { RateBookmarkRequest, RatingActionType } from '../../core/model/rate-bookmark.request';
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { PersonalBookmarksService } from '../../core/personal-bookmarks.service';
import { UserDataStore } from '../../core/user/userdata.store';
import { Logger } from '../../core/logger.service';
import { Router } from '@angular/router';
import { ErrorService } from '../../core/error/error.service';
import { UserDataService } from '../../core/user-data.service';
import { UserInfoStore } from '../../core/user/user-info.store';
import { SuggestedTagsStore } from '../../core/user/suggested-tags.store';
import {debounceTime, distinctUntilChanged, map, startWith} from 'rxjs/operators';
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {Bookmark} from '../../core/model/bookmark';
import {FormArray, FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
import {MarkdownService} from '../markdown.service';
import {KeycloakService} from 'keycloak-angular';
import {COMMA, ENTER, SPACE} from '@angular/cdk/keycodes';
import {MatAutocompleteSelectedEvent, MatChipInputEvent} from '@angular/material';
import {Observable, throwError as observableThrowError} from 'rxjs';
import {languages} from '../../shared/language-options';
import {tagsValidator} from '../../shared/tags-validation.directive';
import {PublicBookmarksStore} from '../../public/bookmarks/store/public-bookmarks-store.service';
import {PublicBookmarksService} from '../../public/bookmarks/public-bookmarks.service';
import {descriptionSizeValidator} from '../../shared/description-size-validation.directive';
import {RateBookmarkRequest, RatingActionType} from '../../core/model/rate-bookmark.request';
import {HttpErrorResponse, HttpResponse} from '@angular/common/http';
import {PersonalBookmarksService} from '../../core/personal-bookmarks.service';
import {UserDataStore} from '../../core/user/userdata.store';
import {Logger} from '../../core/logger.service';
import {Router} from '@angular/router';
import {ErrorService} from '../../core/error/error.service';
import {UserDataService} from '../../core/user-data.service';
import {UserInfoStore} from '../../core/user/user-info.store';
import {SuggestedTagsStore} from '../../core/user/suggested-tags.store';
import {WebpageData} from '../../core/model/webpage-data';

@Component({
selector: 'app-new-personal-bookmark-form',
Expand Down Expand Up @@ -70,20 +71,20 @@ export class CreatePersonalBookmarkComponent implements OnInit {
private router: Router,
private errorService: ErrorService
) {
this.userInfoStore.getUserInfo$().subscribe(userInfo => {
this.userId = userInfo.sub;

this.suggestedTagsStore.getSuggestedTags$(this.userId).subscribe(tags => {
this.autocompleteTags = tags.sort();

this.filteredTags = this.tagsControl.valueChanges.pipe(
startWith(null),
map((tag: string | null) => {
return tag ? this.filter(tag) : this.autocompleteTags.slice();
})
);
});
this.userInfoStore.getUserInfo$().subscribe(userInfo => {
this.userId = userInfo.sub;

this.suggestedTagsStore.getSuggestedTags$(this.userId).subscribe(tags => {
this.autocompleteTags = tags.sort();

this.filteredTags = this.tagsControl.valueChanges.pipe(
startWith(null),
map((tag: string | null) => {
return tag ? this.filter(tag) : this.autocompleteTags.slice();
})
);
});
});
}

ngOnInit(): void {
Expand All @@ -100,20 +101,21 @@ export class CreatePersonalBookmarkComponent implements OnInit {
description: ['', descriptionSizeValidator],
shared: false,
language: 'en',
youtubeVideoId: null
youtubeVideoId: null,
stackoverflowQuestionId: null,
});

this.bookmarkForm.get('location').valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged(), )
.subscribe(location => {
this.personalBookmarksService.getPersonalBookmarkByLocation(this.userId, location).subscribe(httpResponse => {
if (httpResponse.status === 200) {
this.personalBookmarkPresent = true;
} else {
this.getScrapeData(location);
}
},
if (httpResponse.status === 200) {
this.personalBookmarkPresent = true;
} else {
this.getScrapeData(location);
}
},
(errorResponse: HttpErrorResponse) => {
if (errorResponse.status === 404) {
this.getScrapeData(location);
Expand All @@ -128,33 +130,86 @@ export class CreatePersonalBookmarkComponent implements OnInit {
const youtubeVideoId = this.getYoutubeVideoId(location);
if (youtubeVideoId) {
this.bookmarkForm.get('youtubeVideoId').patchValue(youtubeVideoId, {emitEvent: false});
this.publicBookmarksService.getYoutubeVideoData(youtubeVideoId).subscribe((webpageData: WebpageData) => {
this.patchFormAttributesWithScrapedData(webpageData);
},
error => {
console.error(`Problems when scraping data for youtube id ${youtubeVideoId}`, error);
this.updateFormWithScrapingDataFromLocation(location);
});
} else {
const stackoverflowQuestionId = this.getStackoverflowQuestionId(location);
if (stackoverflowQuestionId) {
this.bookmarkForm.get('stackoverflowQuestionId').patchValue(stackoverflowQuestionId, {emitEvent: false});
this.publicBookmarksService.getStackoverflowQuestionData(stackoverflowQuestionId).subscribe((webpageData: WebpageData) => {
this.patchFormAttributesWithScrapedData(webpageData);
},
error => {
console.error(`Problems when scraping data for stackoverflow id ${stackoverflowQuestionId}`, error);
this.updateFormWithScrapingDataFromLocation(location);
});
} else {
this.updateFormWithScrapingDataFromLocation(location);
}
}
this.publicBookmarksService.getScrapingData(location, youtubeVideoId).subscribe(response => {
if (response) {
this.bookmarkForm.get('name').patchValue(response.title, {emitEvent: false});
if (response.publishedOn) {
this.bookmarkForm.get('publishedOn').patchValue(response.publishedOn, {emitEvent: false});
}
this.bookmarkForm.get('description').patchValue(response.metaDescription, {emitEvent: false});
}

private patchFormAttributesWithScrapedData(webpageData) {
if (webpageData.title) {
this.bookmarkForm.get('name').patchValue(webpageData.title, {emitEvent: false});
}
if (webpageData.publishedOn) {
this.bookmarkForm.get('publishedOn').patchValue(webpageData.publishedOn, {emitEvent: false});
}
if (webpageData.metaDescription) {
this.bookmarkForm.get('description').patchValue(webpageData.metaDescription, {emitEvent: false});
}
if (webpageData.tags) {
for (let i = 0; i < webpageData.tags.length; i++) {
const formTags = this.bookmarkForm.get('tags') as FormArray;
formTags.push(this.formBuilder.control(webpageData.tags[i]));
}
});

this.tagsControl.setValue(null);
this.tags.markAsDirty();
}
}

private getYoutubeVideoId(bookmarkUrl): string {
private updateFormWithScrapingDataFromLocation(location) {
this.publicBookmarksService.getScrapingData(location).subscribe((webpageData: WebpageData) => {
this.patchFormAttributesWithScrapedData(webpageData);
},
error => {
console.error(`Problems when scraping data for locaation ${location}`, error);
});
}

private getYoutubeVideoId(bookmarkUrl): string {
let youtubeVideoId = null;
if ( bookmarkUrl.startsWith('https://youtu.be/') ) {
if (bookmarkUrl.startsWith('https://youtu.be/')) {
youtubeVideoId = bookmarkUrl.split('/').pop();
} else if ( bookmarkUrl.startsWith('https://www.youtube.com/watch') ) {
} else if (bookmarkUrl.startsWith('https://www.youtube.com/watch')) {
youtubeVideoId = bookmarkUrl.split('v=')[1];
const ampersandPosition = youtubeVideoId.indexOf('&');
if ( ampersandPosition !== -1 ) {
if (ampersandPosition !== -1) {
youtubeVideoId = youtubeVideoId.substring(0, ampersandPosition);
}
}

return youtubeVideoId;
};


private getStackoverflowQuestionId(location: string) {
let stackoverflowQuestionId = null;
const regExpMatchArray = location.match(/stackoverflow\.com\/questions\/(\d+)/);
if (regExpMatchArray) {
stackoverflowQuestionId = regExpMatchArray[1];
}

return stackoverflowQuestionId;
}

add(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
Expand Down Expand Up @@ -215,6 +270,10 @@ export class CreatePersonalBookmarkComponent implements OnInit {
newBookmark.youtubeVideoId = bookmark.youtubeVideoId;
}

if (bookmark.stackoverflowQuestionId) {
newBookmark.stackoverflowQuestionId = bookmark.stackoverflowQuestionId;
}

this.personalBookmarksService.createBookmark(this.userId, newBookmark)
.subscribe(
res => {
Expand Down Expand Up @@ -292,6 +351,7 @@ export class CreatePersonalBookmarkComponent implements OnInit {
get description() {
return this.bookmarkForm.get('description');
}

}


24 changes: 16 additions & 8 deletions src/app/public/bookmarks/public-bookmarks.service.ts
Expand Up @@ -29,15 +29,23 @@ export class PublicBookmarksService {
return this.httpClient.get<Bookmark[]>(this.publicBookmarksApiBaseUrl, {params: params});
}

getScrapingData(location: string, youtubeVideoId: string): Observable<WebpageData> {
let params;
if (youtubeVideoId) {
params = new HttpParams()
.set('youtubeVideoId', youtubeVideoId)
} else {
params = new HttpParams()
getScrapingData(location: string): Observable<WebpageData> {
const params = new HttpParams()
.set('location', location);
}
return this.httpClient
.get<WebpageData>(`${this.publicBookmarksApiBaseUrl}/scrape`, {params: params});
}

getYoutubeVideoData(youtubeVideoId: string) {
const params = new HttpParams()
.set('youtubeVideoId', youtubeVideoId)
return this.httpClient
.get<WebpageData>(`${this.publicBookmarksApiBaseUrl}/scrape`, {params: params});
}

getStackoverflowQuestionData(stackoverflowQuestionId: string) {
const params = new HttpParams()
.set('stackoverflowQuestionId', stackoverflowQuestionId)
return this.httpClient
.get<WebpageData>(`${this.publicBookmarksApiBaseUrl}/scrape`, {params: params});
}
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/async-bookmark-list.component.html
Expand Up @@ -5,6 +5,7 @@
<div class="titles">
<h5 class="card-title">
<i *ngIf="bookmark.youtubeVideoId" class="fab fa-youtube youtube-icon" (click)="playYoutubeVideo(bookmark)" title="Play video"></i>
<i *ngIf="bookmark.stackoverflowQuestionId" class="fab fa-stack-overflow stackoverflow-icon"></i>
<a href="{{bookmark.location}}" target="_blank" [innerHtml]="bookmark.name | highlight: queryText" (click)="onBookmarkLinkClick(bookmark)"></a>
<sup class="external-link-hint"><i class="fas fa-external-link-alt"></i></sup>
</h5>
Expand Down
4 changes: 4 additions & 0 deletions src/app/shared/async-bookmark-list.component.scss
Expand Up @@ -44,3 +44,7 @@
margin-right: 0.5rem;
cursor: pointer;
}

.stackoverflow-icon {
margin-right: 0.5rem;
}

0 comments on commit 1c471a3

Please sign in to comment.