SearchResults.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. <template>
  2. <div class="container">
  3. <div v-if="loading" class="pt-5 text-center">
  4. <div class="spinner-border" role="status">
  5. <span class="sr-only">Loading…</span>
  6. </div>
  7. </div>
  8. <div v-if="networkError" class="pt-5 text-center">
  9. <p class="lead font-weight-lighter">An error occured, results could not be loaded.<br> Please try again later.</p>
  10. </div>
  11. <div v-if="!loading && !networkError" class="mt-5">
  12. <div v-if="analysis == 'all'" class="row">
  13. <div class="col-12 d-flex justify-content-between align-items-center">
  14. <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
  15. <div v-if="placesSearchEnabled" title="Show Places" data-toggle="tooltip">
  16. <span v-if="results.placesPagination.total > 0" class="badge badge-light mr-2 p-1 border" style="margin-top:-5px;">{{formatCount(results.placesPagination.total)}}</span>
  17. <div class="d-inline custom-control custom-switch">
  18. <input type="checkbox" class="custom-control-input" id="placesSwitch" v-model="showPlaces">
  19. <label class="custom-control-label font-weight-bold text-sm text-lighter" for="placesSwitch"><i class="fas fa-map-marker-alt"></i></label>
  20. </div>
  21. </div>
  22. </div>
  23. <div class="col-12 mb-5">
  24. <hr>
  25. </div>
  26. <div v-if="placesSearchEnabled && showPlaces" class="col-12">
  27. <div class="mb-4">
  28. <p class="text-secondary small font-weight-bold">PLACES <span class="pl-1 text-lighter">({{results.placesPagination.total}})</span></p>
  29. </div>
  30. <div v-if="results.places.length" class="mb-5">
  31. <a v-for="(hashtag, index) in results.places" class="mr-3 pr-4 d-inline-block text-decoration-none" :href="buildUrl('places', hashtag)">
  32. <div class="pb-2">
  33. <div class="media align-items-center py-2">
  34. <div class="media-body text-truncate">
  35. <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
  36. <i class="fas fa-map-marker-alt text-lighter mr-2"></i> {{hashtag.value}}
  37. </p>
  38. </div>
  39. </div>
  40. </div>
  41. </a>
  42. <p v-if="results.places.length == 20 || placesCursor > 0" class="text-center mt-3">
  43. <a v-if="placesCursor == 1" href="#" class="btn btn-outline-secondary btn-sm font-weight-bold py-0 disabled" disabled>
  44. <i class="fas fa-chevron-left mr-2"></i> Previous
  45. </a>
  46. <a v-else href="#" @click.prevent="placesPrevPage()" class="btn btn-outline-secondary btn-sm font-weight-bold py-0">
  47. <i class="fas fa-chevron-left mr-2"></i> Previous
  48. </a>
  49. <span class="mx-4 small text-lighter">{{placesCursor}}/{{results.placesPagination.last_page}}</span>
  50. <a v-if="placesCursor !== results.placesPagination.last_page" @click.prevent="placesNextPage()" href="#" class="btn btn-primary btn-sm font-weight-bold py-0">
  51. Next <i class="fas fa-chevron-right ml-2"></i>
  52. </a>
  53. <a v-else href="#" class="btn btn-primary btn-sm font-weight-bold py-0 disabled" disabled>
  54. Next <i class="fas fa-chevron-right ml-2"></i>
  55. </a>
  56. </p>
  57. </div>
  58. <div v-else>
  59. <div class="border py-3 text-center font-weight-bold">No results found</div>
  60. </div>
  61. </div>
  62. <div class="col-md-3">
  63. <div class="mb-4">
  64. <p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
  65. </div>
  66. <div v-if="results.hashtags.length">
  67. <a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
  68. <div class="pb-3">
  69. <div class="media align-items-center py-2 pr-3">
  70. <span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
  71. <i class="fas fa-hashtag text-muted"></i>
  72. </span>
  73. <div class="media-body text-truncate">
  74. <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
  75. #{{hashtag.value}}
  76. </p>
  77. <p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
  78. {{hashtag.count}} posts
  79. </p>
  80. </div>
  81. </div>
  82. </div>
  83. </a>
  84. </div>
  85. <div v-else>
  86. <div class="border py-3 text-center font-weight-bold">No results found</div>
  87. </div>
  88. </div>
  89. <div class="col-md-5">
  90. <div class="mb-4">
  91. <p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
  92. </div>
  93. <div v-if="results.profiles.length">
  94. <a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
  95. <div class="pb-3">
  96. <div class="media align-items-center py-2 pr-3">
  97. <img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
  98. <div class="media-body">
  99. <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
  100. {{profile.value}}
  101. </p>
  102. <p class="mb-0 small font-weight-bold text-muted text-uppercase">
  103. {{profile.entity.post_count}} Posts
  104. </p>
  105. </div>
  106. <div class="ml-3">
  107. <a v-if="profile.entity.following" class="btn btn-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">Following</a>
  108. <a v-else class="btn btn-outline-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">View</a>
  109. </div>
  110. </div>
  111. </div>
  112. </a>
  113. </div>
  114. <div v-else>
  115. <div class="border py-3 text-center font-weight-bold">No results found</div>
  116. </div>
  117. </div>
  118. <div class="col-md-4">
  119. <div class="mb-4">
  120. <p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
  121. </div>
  122. <div v-if="results.statuses.length">
  123. <a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
  124. <img :src="status.thumb" width="90px" height="90px" class="mb-2">
  125. </a>
  126. </div>
  127. <div v-else>
  128. <div class="border py-3 text-center font-weight-bold">No results found</div>
  129. </div>
  130. </div>
  131. </div>
  132. <div v-else-if="analysis == 'hashtag'" class="row">
  133. <div class="col-12 mb-5">
  134. <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
  135. <hr>
  136. </div>
  137. <div class="col-md-6 offset-md-3">
  138. <div class="mb-4">
  139. <p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
  140. </div>
  141. <div v-if="results.hashtags.length">
  142. <a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
  143. <div class="pb-3">
  144. <div class="media align-items-center py-2 pr-3">
  145. <span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
  146. <i class="fas fa-hashtag text-muted"></i>
  147. </span>
  148. <div class="media-body">
  149. <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
  150. #{{hashtag.value}}
  151. </p>
  152. <p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
  153. {{hashtag.count}} posts
  154. </p>
  155. </div>
  156. </div>
  157. </div>
  158. </a>
  159. </div>
  160. <div v-else>
  161. <div class="border py-3 text-center font-weight-bold">No results found</div>
  162. </div>
  163. </div>
  164. </div>
  165. <div v-else-if="analysis == 'profile'" class="row">
  166. <div class="col-12 mb-5">
  167. <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
  168. <hr>
  169. </div>
  170. <div class="col-md-6 offset-md-3">
  171. <div class="mb-4">
  172. <p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
  173. </div>
  174. <div v-if="results.profiles.length">
  175. <div v-for="(profile, index) in results.profiles" class="card mb-4">
  176. <div class="card-header p-0 m-0">
  177. <div style="width: 100%;height: 140px;background: #0070b7"></div>
  178. </div>
  179. <div class="card-body">
  180. <div class="text-center mt-n5 mb-4">
  181. <img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
  182. </div>
  183. <p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
  184. <p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
  185. <div class="d-flex justify-content-center">
  186. <button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Following</button>
  187. <a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="buildUrl('profile',profile)" style="font-weight: 500">View Profile</a>
  188. </div>
  189. </div>
  190. </div>
  191. </div>
  192. <div v-else>
  193. <div class="border py-3 text-center font-weight-bold">No results found</div>
  194. </div>
  195. </div>
  196. </div>
  197. <div v-else-if="analysis == 'webfinger'" class="row">
  198. <div class="col-12 mb-5">
  199. <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
  200. <hr>
  201. <div class="col-md-6 offset-md-3">
  202. <div v-for="(profile, index) in results.profiles" class="card mb-2">
  203. <div class="card-header p-0 m-0">
  204. <div style="width: 100%;height: 140px;background: #0070b7"></div>
  205. </div>
  206. <div class="card-body">
  207. <div class="text-center mt-n5 mb-4">
  208. <img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
  209. </div>
  210. <p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
  211. <p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
  212. <div class="d-flex justify-content-center">
  213. <!-- <button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Unfollow</button> -->
  214. <!-- <button v-else type="button" class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Follow</button> -->
  215. <a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="'/i/web/profile/_/' + profile.entity.id" style="font-weight: 500">View Profile</a>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. <div v-else-if="analysis == 'remote'" class="row">
  223. <div class="col-12 mb-5">
  224. <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
  225. <hr>
  226. </div>
  227. <div v-if="results.profiles.length" class="col-md-6 offset-3">
  228. <a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
  229. <div class="pb-3">
  230. <div class="media align-items-center py-2 pr-3">
  231. <img class="mr-3 rounded-circle border" :src="profile.entity.thumb" width="50px" height="50px" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
  232. <div class="media-body">
  233. <p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
  234. {{profile.value}}
  235. </p>
  236. <p class="mb-0 small font-weight-bold text-muted text-uppercase">
  237. {{profile.entity.post_count}} Posts
  238. </p>
  239. </div>
  240. <div class="ml-3">
  241. <a v-if="profile.entity.following" class="btn btn-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">Following</a>
  242. <a v-else class="btn btn-outline-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">View</a>
  243. </div>
  244. </div>
  245. </div>
  246. </a>
  247. </div>
  248. <div v-if="results.statuses.length" class="col-md-6 offset-3">
  249. <a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
  250. <img :src="status.thumb" width="90px" height="90px" class="mb-2" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
  251. </a>
  252. </div>
  253. </div>
  254. <div v-else-if="analysis == 'remotePost'" class="row">
  255. <div class="col-12 mb-5">
  256. <p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
  257. <hr>
  258. </div>
  259. <div class="col-md-6 offset-md-3">
  260. <div v-if="results.statuses.length">
  261. <div v-for="(status, index) in results.statuses" class="card mb-4 shadow-none border">
  262. <div class="card-header p-0 m-0">
  263. <div style="width: 100%;height: 200px;background: #fff">
  264. <div class="pt-4 text-center">
  265. <img :src="status.thumb" class="img-fluid border" style="max-height: 140px;">
  266. </div>
  267. </div>
  268. </div>
  269. <div class="card-body">
  270. <div class="mt-n4 mb-2">
  271. <div class="media">
  272. <img class="rounded-circle p-1 mr-2 border mt-n3 bg-white shadow" src="/storage/avatars/default.png" width="70px" height="70px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
  273. <div class="media-body pt-3">
  274. <p class="font-weight-bold mb-0">{{status.username}}</p>
  275. </div>
  276. <div class="float-right pt-3">
  277. <p class="small mb-0 text-muted">{{status.timestamp}}</p>
  278. </div>
  279. </div>
  280. </div>
  281. <p class="text-center mb-3 lead" v-html="status.caption"></p>
  282. <!-- <p class="text-center text-muted small text-uppercase mb-4">2 likes</p> -->
  283. <!-- <div class="d-flex justify-content-center">
  284. <a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="status.url" style="font-weight: 500">View Post</a>
  285. </div> -->
  286. </div>
  287. <div class="card-footer">
  288. <a class="btn btn-primary btn-block font-weight-bold rounded-0" :href="status.url">View Post</a>
  289. </div>
  290. </div>
  291. </div>
  292. <div v-else>
  293. <div class="border py-3 text-center font-weight-bold">No results found</div>
  294. </div>
  295. </div>
  296. </div>
  297. <div v-else class="col-12">
  298. <p class="text-center text-muted lead font-weight-bold">No results found</p>
  299. </div>
  300. </div>
  301. </div>
  302. </template>
  303. <style type="text/css" scoped>
  304. .result-card {
  305. text-decoration: none;
  306. }
  307. .result-card .media:hover {
  308. background: #EDF2F7;
  309. }
  310. @media (min-width: 1200px) {
  311. .container {
  312. max-width: 995px;
  313. }
  314. }
  315. </style>
  316. <script type="text/javascript">
  317. export default {
  318. props: ['query', 'profileId'],
  319. data() {
  320. return {
  321. loading: true,
  322. networkError: false,
  323. results: {
  324. hashtags: [],
  325. profiles: [],
  326. statuses: [],
  327. places: [],
  328. },
  329. filters: {
  330. hashtags: true,
  331. profiles: true,
  332. statuses: true
  333. },
  334. analysis: 'profile',
  335. showPlaces: false,
  336. placesCursor: 1,
  337. placesCache: [],
  338. placesSearchEnabled: false,
  339. searchVersion: 2
  340. }
  341. },
  342. beforeMount() {
  343. this.bootSearch();
  344. },
  345. mounted() {
  346. $('.search-bar input').val(this.query);
  347. },
  348. updated() {
  349. $('[data-toggle="tooltip"]').tooltip();
  350. },
  351. methods: {
  352. bootSearch() {
  353. let lexer = this.searchLexer();
  354. this.analysis = lexer;
  355. this.fetchSearchResults();
  356. },
  357. fetchSearchResults() {
  358. if(this.analysis == 'remote') {
  359. let term = this.query;
  360. let parsed = new URL(term);
  361. if(parsed.host === window.location.host) {
  362. window.location.href = term;
  363. return;
  364. }
  365. }
  366. this.searchContext(this.analysis);
  367. },
  368. followProfile(profile, index) {
  369. this.loading = true;
  370. axios.post('/i/follow', {
  371. item: profile.entity.id
  372. }).then(res => {
  373. if(profile.entity.local == true) {
  374. this.fetchSearchResults();
  375. return;
  376. } else {
  377. this.loading = false;
  378. this.results.profiles[index].entity.follow_request = true;
  379. return;
  380. }
  381. }).catch(err => {
  382. if(err.response.data.message) {
  383. swal('Error', err.response.data.message, 'error');
  384. }
  385. });
  386. },
  387. searchLexer() {
  388. let q = this.query;
  389. if(q.startsWith('#')) {
  390. return 'hashtag';
  391. }
  392. if((q.match(/@/g) || []).length == 2) {
  393. return 'webfinger';
  394. }
  395. if(q.startsWith('@')) {
  396. return 'profile';
  397. }
  398. if(q.startsWith('https://')) {
  399. return 'remote';
  400. }
  401. return 'all';
  402. },
  403. buildUrl(type = 'hashtag', obj) {
  404. switch(type) {
  405. case 'hashtag':
  406. return obj.url + '?src=search';
  407. break;
  408. case 'profile':
  409. if(obj.entity.local == true) {
  410. return obj.url;
  411. }
  412. return '/i/web/profile/_/' + obj.entity.id;
  413. break;
  414. default:
  415. return obj.url + '?src=search';
  416. break;
  417. }
  418. },
  419. searchContext(type) {
  420. switch(type) {
  421. case 'all':
  422. axios.get('/api/search', {
  423. params: {
  424. 'q': this.query,
  425. 'src': 'metro',
  426. 'v': this.searchVersion,
  427. 'scope': 'all'
  428. }
  429. }).then(res => {
  430. let results = res.data;
  431. this.results.hashtags = results.hashtags ? results.hashtags : [];
  432. this.results.profiles = results.profiles ? results.profiles : [];
  433. this.results.statuses = results.posts ? results.posts : [];
  434. this.results.places = results.places ? results.places : [];
  435. this.placesCache = results.places;
  436. this.results.placesPagination = results.placesPagination ? results.placesPagination : [];
  437. this.loading = false;
  438. }).catch(err => {
  439. this.loading = false;
  440. this.networkError = true;
  441. });
  442. break;
  443. case 'remote':
  444. axios.get('/api/search', {
  445. params: {
  446. 'q': this.query,
  447. 'src': 'metro',
  448. 'v': this.searchVersion,
  449. 'scope': 'remote'
  450. }
  451. }).then(res => {
  452. let results = res.data;
  453. this.results.hashtags = results.hashtags ? results.hashtags : [];
  454. this.results.profiles = results.profiles ? results.profiles : [];
  455. this.results.statuses = results.posts ? results.posts : [];
  456. if(this.results.profiles.length) {
  457. this.analysis = 'profile';
  458. }
  459. if(this.results.statuses.length) {
  460. this.analysis = 'remotePost';
  461. }
  462. this.loading = false;
  463. }).catch(err => {
  464. this.loading = false;
  465. this.networkError = true;
  466. });
  467. break;
  468. case 'hashtag':
  469. axios.get('/api/search', {
  470. params: {
  471. 'q': this.query.slice(1),
  472. 'src': 'metro',
  473. 'v': this.searchVersion,
  474. 'scope': 'hashtag'
  475. }
  476. }).then(res => {
  477. let results = res.data;
  478. this.results.hashtags = results.hashtags ? results.hashtags : [];
  479. this.results.profiles = results.profiles ? results.profiles : [];
  480. this.results.statuses = results.posts ? results.posts : [];
  481. this.loading = false;
  482. }).catch(err => {
  483. this.loading = false;
  484. this.networkError = true;
  485. });
  486. break;
  487. case 'profile':
  488. axios.get('/api/search', {
  489. params: {
  490. 'q': this.query,
  491. 'src': 'metro',
  492. 'v': this.searchVersion,
  493. 'scope': 'profile'
  494. }
  495. }).then(res => {
  496. let results = res.data;
  497. this.results.hashtags = results.hashtags ? results.hashtags : [];
  498. this.results.profiles = results.profiles ? results.profiles : [];
  499. this.results.statuses = results.posts ? results.posts : [];
  500. this.loading = false;
  501. }).catch(err => {
  502. this.loading = false;
  503. this.networkError = true;
  504. });
  505. break;
  506. case 'webfinger':
  507. axios.get('/api/search', {
  508. params: {
  509. 'q': this.query,
  510. 'src': 'metro',
  511. 'v': this.searchVersion,
  512. 'scope': 'webfinger'
  513. }
  514. }).then(res => {
  515. let results = res.data;
  516. this.results.hashtags = [];
  517. this.results.profiles = results.profiles;
  518. this.results.statuses = [];
  519. this.loading = false;
  520. }).catch(err => {
  521. this.loading = false;
  522. this.networkError = true;
  523. });
  524. break;
  525. default:
  526. this.loading = false;
  527. this.networkError = true;
  528. break;
  529. }
  530. },
  531. placesPrevPage() {
  532. this.placesCursor--;
  533. if(this.placesCursor == 1) {
  534. this.results.places = this.placesCache.slice(0, 20);
  535. return;
  536. }
  537. let plc = this.placesCursor * 20;
  538. this.results.places = this.placesCache.slice(plc, 20);
  539. return;
  540. },
  541. placesNextPage() {
  542. this.placesCursor++;
  543. let plc = this.placesCursor * 20;
  544. if(this.placesCache.length > 20) {
  545. this.results.places = this.placesCache.slice(this.placesCursor == 1 ? 0 : plc, 20);
  546. return;
  547. }
  548. axios.get('/api/search', {
  549. params: {
  550. 'q': this.query,
  551. 'src': 'metro',
  552. 'v': this.searchVersion,
  553. 'scope': 'all',
  554. 'page': this.placesCursor
  555. }
  556. }).then(res => {
  557. let results = res.data;
  558. this.results.places = results.places ? results.places : [];
  559. this.placesCache.push(...results.places);
  560. this.loading = false;
  561. }).catch(err => {
  562. this.loading = false;
  563. this.networkError = true;
  564. });
  565. },
  566. formatCount(num) {
  567. let count = window.App.util.format.count(num);
  568. return count;
  569. }
  570. }
  571. }
  572. </script>