Переглянути джерело

Update to use two separate routes for thread and user search; update frontend search box to use api

sbkwgh 6 роки тому
батько
коміт
226ca986c7
2 змінених файлів з 152 додано та 46 видалено
  1. 75 17
      frontend/src/components/SearchBox.vue
  2. 77 29
      routes/search.js

+ 75 - 17
frontend/src/components/SearchBox.vue

@@ -14,6 +14,8 @@
 				:placeholder='placeholder || "Search this forum"'
 				v-model='searchField'
 				
+				ref='input'
+
 				@focus='setShowResults'
 				@input='setShowResults'
 				@keydown='setKeyHighlight'
@@ -43,7 +45,7 @@
 					@mouseover='highlightIndex = getHighlightIndex("threads header")'
 				>
 					<span class='fa fa-fw fa-search'></span>
-					Search all threads containing '<strong>{{searchField}}</strong>'
+					Search threads for '<strong>{{searchField}}</strong>'
 				</div>
 				<div
 					class='search_box__results__thread'
@@ -53,9 +55,10 @@
 					v-for='(thread, index) in threads'
 					ref='threads'
 					@mouseover='highlightIndex = getHighlightIndex("threads", index)'
+					@click='goToSearch'
 				>
-					<div class='search_box__results__title'>{{thread.title}}</div>
-					<div class='search_box__results__content'>{{thread.content}}</div>
+					<div class='search_box__results__title'>{{thread.name}}</div>
+					<div class='search_box__results__content'>{{thread.Posts[0].content | stripTags | truncate(140) }}</div>
 				</div>
 			</template>
 
@@ -70,7 +73,7 @@
 					@mouseover='highlightIndex = getHighlightIndex("users header")'
 				>
 					<span class='fa fa-fw fa-search'></span>
-					Search all users beginning '<strong>{{searchField}}</strong>'
+					Search users containing '<strong>{{searchField}}</strong>'
 				</div>
 				<div
 					class='search_box__results__user'
@@ -80,12 +83,19 @@
 					v-for='(user, index) in users'
 					ref='users'
 					@mouseover='highlightIndex = getHighlightIndex("users", index)'
+					@click='goToSearch'
 				>
 					<avatar-icon size='tiny' :user='user'></avatar-icon>
 					<div class='search_box__results__title'>{{user.username}}</div>
 				</div>
 			</template>
 
+			<div class='search_box__results__message' v-if='!threads.length && !users.length && !loading'>
+				No users or threads found for '<strong>{{searchField}}</strong>'
+			</div>
+			<div class='search_box__results__message' v-if='loading'>
+				Loading...
+			</div>
 		</div>
 	</div>
 </template>
@@ -93,6 +103,8 @@
 <script>
 	import AvatarIcon from './AvatarIcon';
 
+	import AjaxErrorHandler from '../assets/js/errorHandler'
+
 	export default {
 		name: 'SearchBox',
 		props: ['placeholder', 'header-bar'],
@@ -101,15 +113,12 @@
 			return {
 				searchField: '',
 				showResults: false,
+				loading: false,
 
 				highlightIndex: null,
 
-				threads: [
-					{title: 'Thread', content: 'Body content here 123' },
-					{ title: 'Some other', content: 'Loremp ipsum dolor sit amet' },
-					{ title: 'What??', content: 'testtestt esttesttesttes ttestt esttest' }
-				],
-				users: [{ username: 'Username' }, { username: 'username' }, { username: 'username' }]
+				threads: [],
+				users: []
 			}
 		},
 		computed: {
@@ -127,17 +136,28 @@
 			setShowResults () {
 				//Return if results should not show
 				if(!this.headerBar) return;
-				
+
 				this.showResults = !!this.searchField.trim().length;
-				if(!this.showResults) this.resetResultsBox();
+				if(this.showResults) {
+					this.getResults();
+				} else {
+					this.resetResultsBox();
+				}
 			},
 			resetResultsBox () {
 				//Return if results should not show
 				if(!this.headerBar) return;
 
 				this.showResults = false;
-				this.highlightIndex = null;
-				this.$refs.results.scrollTop = 0;
+				
+				//These changes alter ui within the box
+				//therefore wait until transition completed
+				setTimeout(() => {
+					this.highlightIndex = null;
+					this.$refs.results.scrollTop = 0;
+					this.threads = [];
+					this.users = [];
+				}, 200);
 			},
 			//Produces a 'global' highlight index from the
 			//relative index of each array group, dependent on
@@ -233,9 +253,42 @@
 					this.showResults = false;
 					this.$router.push("/search/" + encodeURIComponent(this.searchField));
 				} else {
-					//Do something
+					let { group, index } = this.getGroupFromIndex(this.highlightIndex);
+					if(group === 'users') {
+						this.$router.push('/user/' + this.users[index].username);
+					} else if (group === 'threads') {
+						let thread = this.threads[index];
+						this.$router.push('/thread/' + thread.slug + '/' + thread.id);
+					}
+
 					this.resetResultsBox();
 				}
+
+				this.$refs.input.blur();
+			},
+			getResults () {
+				let q = this.searchField.trim();
+				if(!q.length) return;
+
+				this.loading = true;
+				this.threads = [];
+				this.users = [];
+
+				this.axios
+					.get('/api/v1/search/thread?q=' + q)
+					.then(res => {
+						this.threads = res.data.threads;
+						this.loading = false;
+					})
+					.catch(AjaxErrorHandler(this.$store));
+
+				this.axios
+					.get('/api/v1/search/user?q=' + q)
+					.then(res => {
+						this.users = res.data.users;
+						this.loading = false;
+					})
+					.catch(AjaxErrorHandler(this.$store));
 			}
 		},
 		mounted () {
@@ -307,7 +360,7 @@
 			right: 0;
 			transform: translateY(-0.25rem);
 			transition: opacity 0.2s, transform 0.2s;
-			width: 150%;
+			width: 100%;
 
 			@at-root #{&}--show {
 				opacity: 1;
@@ -326,7 +379,7 @@
 				position: sticky;
 
 				@at-root #{&}--divider {
-					border-top: thin solid $color__gray--primary;
+					border-top: thin solid $color__gray--darker;
 				}
 			}
 		
@@ -379,6 +432,11 @@
 				color: $color__text--secondary;
 				font-size: 0.85rem;
 			}
+
+			@at-root #{&}__message {
+				cursor: default;
+				padding: 1rem;
+			}
 		}
 	}
 

+ 77 - 29
routes/search.js

@@ -1,52 +1,100 @@
 let express = require('express')
 let router = express.Router()
 
-let { Post, Thread, Sequelize } = require('../models')
+let { Post, Thread, User, Sequelize } = require('../models')
 const Errors = require('../lib/errors')
 
-router.get('/', async (req, res, next) => {
+router.get('/thread', async (req, res, next) => {
 	try {
-		let q = req.query.q
-		let qRegexp = new RegExp(q, 'g')
+		let searchString = req.query.q
+
 		let offset = +req.query.offset || 0
+		let limit = 10
 
-		let count = await Post.count({
-			where: {
-				content: { $like: '%' + q + '%' }
-			}
-		})
+		/*  
+		Task is to find threads that either have the 
+		string in the title or in the content of the first post
 
-		let posts = await Post.findAll({
+		Method
+		  1) Select first n items from each group (posts and threads), where n is the LIMIT,
+		     greater than id x, where x is previous OFFSET
+		  2) Merge results from both, remove duplicates and sort
+		  3) Select first n items from merged group
+		  4) Set x as the last item from merged group
+		*/
+
+		let threadTitles = await Thread.findAll({
 			where: {
-				content: { $like: '%' + q + '%' }
+				name: { $like: '%' + searchString + '%' }
 			},
 			order: [ ['id', 'DESC'] ],
-			include: Post.includeOptions(),
-			limit: 10,
+			include: [{
+				model: Post,
+				where: {
+					postNumber: 0
+				}
+			}],
+			limit,
 			offset
 		})
 
-		let retPosts = posts.map(p => {
-			let ret = p.toJSON()
-			ret.content = ret.content.replace(qRegexp, '<b>' + q + '</b>')
+		let threadPosts = await Thread.findAll({
+			order: [ ['id', 'DESC'] ],
+			include: [{
+				model: Post,
+				where: {
+					postNumber: 0,
+					content: { $like: '%' + searchString + '%' }
+				}
+			}],
+			limit,
+			offset
+		})
+
+		let merged = [...threadTitles, ...threadPosts];
+		let unique = [];
+		merged.forEach(thread => {
+			let includes = unique.filter(u => thread.id === u.id);
+
+			if(!includes.length) unique.push(thread);
+		});
+		
+		let sorted = unique
+			.sort((a, b) => {
+				return a.id - b.id;
+			})
+			.slice(0, limit);
 
-			return ret
+		res.json({
+			threads: sorted,
+			offset: sorted.length ? sorted.slice(-1)[0].id : null,
+			next: sorted.length < limit ? null : limit
 		})
 
-		let remainingResults = count - (offset + 10)
-		let next;
-		if(remainingResults < 0) {
-			next = 0
-		} else if(remainingResults < 10) {
-			next = remainingResults
-		} else {
-			next = 10
-		}
+	} catch (e) { next(e) }
+})
+
+router.get('/user', async (req, res, next) => {
+	try {
+		let searchString = req.query.q
+
+		let offset = +req.query.offset || 0
+		let limit = 10
+
+		let users = await User.findAll({
+			where: {
+				username: { $like: '%' + searchString + '%' }
+			},
+			order: [ ['username', 'DESC'] ],
+			attributes: { exclude: ['hash'] },
+			limit,
+			offset
+		})
 
 		res.json({
-			posts: retPosts,
-			offset: offset + 10,
-			next
+			users,
+			offset: users.length? users.slice(-1)[0].id : null,
+			next: users.length < limit ? null : limit
 		})
 
 	} catch (e) { next(e) }