implement search

This commit is contained in:
Nolan Lawson 2018-02-06 20:54:49 -08:00
parent eb5b6999ce
commit 8761a46767
17 changed files with 349 additions and 23 deletions

View File

@ -18,4 +18,5 @@ module.exports = [
{id:'fa-user-times', src:'node_modules/font-awesome-svg-png/white/svg/user-times.svg', title: 'Stop Following'},
{id:'fa-user-plus', src:'node_modules/font-awesome-svg-png/white/svg/user-plus.svg', title: 'Follow'},
{id:'fa-external-link', src:'node_modules/font-awesome-svg-png/white/svg/external-link.svg', title: 'External Link'},
{id:'fa-search', src:'node_modules/font-awesome-svg-png/white/svg/search.svg', title: 'Search'},
]

View File

@ -0,0 +1,24 @@
<div class="loading-page">
<LoadingSpinner />
</div>
<style>
.loading-page {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
</style>
<script>
import LoadingSpinner from './LoadingSpinner.html'
export default {
components: {
LoadingSpinner
}
}
</script>

View File

@ -13,6 +13,9 @@
<NavItem :page name="federated" href="/federated" svg="#fa-globe" label="Federated" />
</li>
<li>
<NavItem :page name="search" href="/search" svg="#fa-search" label="Search" />
</li>
<li>
<NavItem :page name="settings" href="/settings" svg="#fa-gear" label="Settings" />
</li>
</ul>

View File

@ -0,0 +1,49 @@
<SearchResult href="/accounts/{{account.id}}">
<div class="search-result-account">
<Avatar :account size="small" className="search-result-account-avatar"/>
<div class="search-result-account-name">
{{account.display_name}}
</div>
<div class="search-result-account-username">
{{'@' + account.acct}}
</div>
</div>
</SearchResult>
<style>
.search-result-account {
display: grid;
grid-template-areas:
"avatar name"
"avatar username";
grid-column-gap: 20px;
grid-template-columns: max-content 1fr;
align-items: center;
}
:global(.search-result-account-avatar) {
grid-area: avatar;
}
.search-result-account-name {
grid-area: name;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.2em;
}
.search-result-account-username {
grid-area: username;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--deemphasized-text-color);
}
</style>
<script>
import Avatar from '../status/Avatar.html'
import SearchResult from './SearchResult.html'
export default {
components: {
Avatar,
SearchResult
}
}
</script>

View File

@ -0,0 +1,13 @@
<SearchResult href="/tags/{{hashtag}}">
{{'#' + hashtag}}
</SearchResult>
<style>
</style>
<script>
import SearchResult from './SearchResult.html'
export default {
components: {
SearchResult
}
}
</script>

View File

@ -0,0 +1,93 @@
<form class="search-input-form" on:submit="onSubmit(event)">
<div class="search-input-wrapper">
<input type="search"
class="search-input"
placeholder="Search"
aria-label="Search input"
required
bind:value="$queryInSearch">
</div>
<button type="submit" class="primary search-button" aria-label="Search">
<svg>
<use xlink:href="#fa-search" />
</svg>
</button>
</form>
{{#if loading}}
<div class="search-results-container">
<LoadingPage />
</div>
{{elseif $searchResults && $searchResultsForQuery === $queryInSearch}}
<div class="search-results-container">
<SearchResults />
</div>
{{/if}}
<style>
.search-input-form {
display: grid;
grid-template-columns: 1fr min-content;
grid-gap: 10px;
}
.search-input-wrapper {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.search-input {
padding: 10px 15px;
border-radius: 10px;
flex: 1;
}
.search-button svg {
fill: var(--button-primary-text);
width: 18px;
height: 18px;
flex: 1;
}
.search-results-container {
position: relative;
margin-top: 20px;
}
@media (min-width: 768px) {
.search-button {
min-width: 100px;
}
}
</style>
<script>
import { store } from '../../_store/store'
import LoadingPage from '../LoadingPage.html'
import { toast } from '../../_utils/toast'
import { search } from '../../_utils/mastodon/search'
import SearchResults from './SearchResults.html'
export default {
store: () => store,
components: {
LoadingPage,
SearchResults
},
methods: {
async onSubmit (e) {
e.preventDefault()
let instanceName = this.store.get('currentInstance')
let accessToken = this.store.get('accessToken')
let queryInSearch = this.store.get('queryInSearch')
this.set({loading: true})
try {
let results = await search(instanceName, accessToken, queryInSearch)
this.store.set({
searchResultsForQuery: queryInSearch,
searchResults: results
})
} catch (e) {
toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
console.error(e)
} finally {
this.set({loading: false})
}
}
}
}
</script>

View File

@ -0,0 +1,28 @@
<li class="search-result">
<a href="{{href}}" class="search-result-anchor">
<slot></slot>
</a>
</li>
<style>
.search-result {
box-sizing: border-box;
border-bottom: 1px solid var(--main-border);
display: flex;
}
.search-result:last-child {
border-bottom: none;
}
.search-result-anchor {
padding: 20px;
flex: 1;
background: var(--settings-list-item-bg);
color: var(--body-text-color);
}
.search-result-anchor:hover {
background: var(--settings-list-item-bg-hover);
text-decoration: none;
}
.search-result-anchor:active {
background: var(--settings-list-item-bg-active);
}
</style>

View File

@ -0,0 +1,34 @@
<ul class="search-results">
{{#each $searchResults.hashtags as hashtag}}
<HashtagSearchResult :hashtag />
{{/each}}
{{#each $searchResults.accounts as account}}
<AccountSearchResult :account />
{{/each}}
{{#each $searchResults.statuses as status, index}}
<StatusSearchResult :status :index length="{{$searchResults.statuses.length}}"/>
{{/each}}
</ul>
<style>
.search-results {
list-style: none;
box-sizing: border-box;
border: 1px solid var(--main-border);
border-radius: 2px;
}
</style>
<script>
import { store } from '../../_store/store'
import AccountSearchResult from './AccountSearchResult.html'
import HashtagSearchResult from './HashtagSearchResult.html'
import StatusSearchResult from './StatusSearchResult.html'
export default {
store: () => store,
components: {
AccountSearchResult,
HashtagSearchResult,
StatusSearchResult
}
}
</script>

View File

@ -0,0 +1,18 @@
<SearchResult href="/statuses/{{status.id}}">
<Status :index :length
timelineType="search" timelineValue="search"
status="{{status}}" />
</SearchResult>
<style>
</style>
<script>
import SearchResult from './SearchResult.html'
import Status from '../status/Status.html'
export default {
components: {
SearchResult,
Status
}
}
</script>

View File

@ -1,12 +1,12 @@
{{#if error}}
<svg class="{{className}} avatar size-{{size}}" aria-hidden="true">
<svg class="{{className || ''}} avatar size-{{size}}" aria-hidden="true">
<use xlink:href="#fa-user" />
</svg>
{{elseif $autoplayGifs}}
<img class="{{className}} avatar size-{{size}}" aria-hidden="true" alt=""
<img class="{{className || ''}} avatar size-{{size}}" aria-hidden="true" alt=""
src="{{account.avatar}}" on:imgLoadError="set({error: true})" />
{{else}}
<NonAutoplayImg className="{{className}} avatar size-{{size}}" ariaHidden="true" alt=""
<NonAutoplayImg className="{{className || ''}} avatar size-{{size}}" ariaHidden="true" alt=""
src="{{account.avatar}}" staticSrc="{{account.avatar_static}}" on:imgLoadError="set({error: true})" />
{{/if}}
<style>

View File

@ -1,4 +1,4 @@
<article class="status-article {{originalStatus.visibility === 'direct' ? 'status-direct' : ''}}"
<article class="status-article {{getClasses(originalStatus, timelineType)}}"
tabindex="0"
aria-posinset="{{index}}" aria-setsize="{{length}}"
on:recalculateHeight>
@ -21,10 +21,8 @@
<style>
.status-article {
width: 560px;
max-width: calc(100vw - 40px);
padding: 10px 20px;
border-bottom: 1px solid var(--main-border);
display: grid;
grid-template-areas:
".............. status-header"
@ -37,6 +35,11 @@
grid-template-columns: 58px 1fr;
}
.status-article.status-in-timeline {
width: 560px;
border-bottom: 1px solid var(--main-border);
}
.status-article.status-direct {
background-color: var(--status-direct-background);
}
@ -69,6 +72,12 @@
StatusSpoiler
},
store: () => store,
helpers: {
getClasses(originalStatus, timelineType) {
return (originalStatus.visibility === 'direct' ? 'status-direct' : '') +
' ' + (timelineType !== 'search' ? 'status-in-timeline' : '')
}
},
computed: {
originalStatus: (status) => status.reblog ? status.reblog : status,
statusId: (originalStatus) => originalStatus.id,

View File

@ -1,8 +1,6 @@
<div class="timeline" role="feed" aria-label="{{label}}">
{{#if !$initialized}}
<div class="loading-page">
<LoadingSpinner />
</div>
<LoadingPage />
{{/if}}
{{#if timelineType === 'notifications'}}
<VirtualList component="{{NotificationVirtualListItem}}"
@ -44,17 +42,6 @@
min-height: 60vh;
position: relative;
}
.loading-page {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
</style>
<script>
import { store } from '../../_store/store'
@ -67,7 +54,7 @@
import { timelines } from '../../_static/timelines'
import { database } from '../../_utils/database/database'
import { initializeTimeline, fetchTimelineItemsOnScrollToBottom, setupTimeline } from '../../_actions/timeline'
import LoadingSpinner from '../LoadingSpinner.html'
import LoadingPage from '../LoadingPage.html'
export default {
async oncreate() {
@ -118,7 +105,7 @@
components: {
VirtualList,
PseudoVirtualList,
LoadingSpinner
LoadingPage
},
methods: {
initialize() {

View File

@ -23,6 +23,7 @@ class PinaforeStore extends LocalStorageStore {
const store = new PinaforeStore({
instanceNameInSearch: '',
queryInSearch: '',
currentInstance: null,
loggedInInstances: {},
loggedInInstancesInOrder: [],

View File

@ -0,0 +1,12 @@
import { get, paramsString } from '../ajax'
export function search(instanceName, accessToken, query) {
let url = `https://${instanceName}/api/v1/search?` + paramsString({
q: query,
resolve: true
})
return get(url, {
'Authorization': `Bearer ${accessToken}`
})
}

48
routes/search.html Normal file
View File

@ -0,0 +1,48 @@
<:Head>
<title>Pinafore Search</title>
</:Head>
<Layout page='search'>
{{#if $isUserLoggedIn}}
<div class="search-page">
<Search></Search>
</div>
{{else}}
<HiddenFromSSR>
<FreeTextLayout>
<h1>Search</h1>
<p>You can search once logged in to an instance.</p>
</FreeTextLayout>
</HiddenFromSSR>
{{/if}}
</Layout>
<style>
.search-page {
min-height: 60vh;
padding: 20px 20px;
}
@media (max-width: 767px) {
.search-page {
padding: 20px 10px;
}
}
</style>
<script>
import Layout from './_components/Layout.html'
import FreeTextLayout from './_components/FreeTextLayout.html'
import { store } from './_store/store.js'
import HiddenFromSSR from './_components/HiddenFromSSR'
import Search from './_components/search/Search.html'
export default {
store: () => store,
components: {
Layout,
Search,
FreeTextLayout,
HiddenFromSSR
}
}
</script>

View File

@ -129,3 +129,8 @@ button::-moz-focus-inner {
border: 0;
}
/* Firefox hacks to remove ugly red border.
Unnecessary since it gives a warning if you submit an empty field anyway. */
input:required, input:invalid {
box-shadow: none;
}

View File

@ -11,7 +11,7 @@
<style>
/* auto-generated w/ build-sass.js */
body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#90a8ee;--action-button-fill-color-hover:#a2b6f0;--action-button-fill-color-active:#577ae4;--action-button-fill-color-pressed:#2351dc;--action-button-fill-color-pressed-hover:#3862e0;--action-button-fill-color-pressed-active:#1d44b8;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#c5d1f6;--very-deemphasized-link-color:rgba(65,105,225,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#d2dcf8}
body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue;font-size:14px;line-height:1.3;color:var(--body-text-color);background:var(--body-bg);position:fixed;left:0;right:0;bottom:0;top:0}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;will-change:transform;position:absolute;top:72px;left:0;right:0;bottom:0}@media (max-width: 767px){.container{top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px}@media (max-width: 767px){main{margin:5px auto 15px}}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px}button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover{background:var(--button-bg-hover)}button:active{background:var(--button-bg-active)}button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}button::-moz-focus-inner{border:0}
body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue;font-size:14px;line-height:1.3;color:var(--body-text-color);background:var(--body-bg);position:fixed;left:0;right:0;bottom:0;top:0}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;will-change:transform;position:absolute;top:72px;left:0;right:0;bottom:0}@media (max-width: 767px){.container{top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px}@media (max-width: 767px){main{margin:5px auto 15px}}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px}button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover{background:var(--button-bg-hover)}button:active{background:var(--button-bg-active)}button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}
body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline,body.theme-gecko.offline{--button-primary-bg:#ababab;--button-primary-text:#fff;--button-primary-border:#4d4d4d;--button-primary-bg-active:#9c9c9c;--button-primary-bg-hover:#b0b0b0;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#999;--main-bg:#fff;--body-bg:#fafafa;--body-text-color:#333;--main-border:#dadada;--svg-fill:#999;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#999;--nav-border:gray;--nav-a-border:#999;--nav-a-selected-border:#fff;--nav-a-selected-bg:#b3b3b3;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#bfbfbf;--nav-a-bg-hover:#a6a6a6;--nav-a-border-hover:#999;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#c7c7c7;--action-button-fill-color-hover:#d1d1d1;--action-button-fill-color-active:#a6a6a6;--action-button-fill-color-pressed:#878787;--action-button-fill-color-pressed-hover:#949494;--action-button-fill-color-pressed-active:#737373;--settings-list-item-bg:#fff;--settings-list-item-text:#999;--settings-list-item-text-hover:#999;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#bfbfbf;--very-deemphasized-link-color:rgba(153,153,153,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#ededed}
</style>
@ -83,6 +83,7 @@ body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-o
<symbol id="fa-user-times" viewBox="0 0 2048 1792"><title>Stop Following</title><path d="M704 896q-159 0-271.5-112.5T320 512t112.5-271.5T704 128t271.5 112.5T1088 512 975.5 783.5 704 896zm1077 320l249 249q9 9 9 23 0 13-9 22l-136 136q-9 9-22 9-14 0-23-9l-249-249-249 249q-9 9-23 9-13 0-22-9l-136-136q-9-9-9-22 0-14 9-23l249-249-249-249q-9-9-9-23 0-13 9-22l136-136q9-9 22-9 14 0 23 9l249 249 249-249q9-9 23-9 13 0 22 9l136 136q9 9 9 22 0 14-9 23zm-498 0l-181 181q-37 37-37 91 0 53 37 90l83 83q-21 3-44 3H267q-121 0-194-69T0 1405q0-53 3.5-103.5t14-109T44 1084t43-97.5 62-81 85.5-53.5T346 832q19 0 39 17 154 122 319 122t319-122q20-17 39-17 28 0 57 6-28 27-41 50t-13 56q0 54 37 91z"></path></symbol>
<symbol id="fa-user-plus" viewBox="0 0 2048 1792"><title>Follow</title><path d="M704 896q-159 0-271.5-112.5T320 512t112.5-271.5T704 128t271.5 112.5T1088 512 975.5 783.5 704 896zm960 128h352q13 0 22.5 9.5t9.5 22.5v192q0 13-9.5 22.5t-22.5 9.5h-352v352q0 13-9.5 22.5t-22.5 9.5h-192q-13 0-22.5-9.5t-9.5-22.5v-352h-352q-13 0-22.5-9.5t-9.5-22.5v-192q0-13 9.5-22.5t22.5-9.5h352V672q0-13 9.5-22.5t22.5-9.5h192q13 0 22.5 9.5t9.5 22.5v352zm-736 224q0 52 38 90t90 38h256v238q-68 50-171 50H267q-121 0-194-69T0 1405q0-53 3.5-103.5t14-109T44 1084t43-97.5 62-81 85.5-53.5T346 832q19 0 39 17 79 61 154.5 91.5T704 971t164.5-30.5T1023 849q20-17 39-17 132 0 217 96h-223q-52 0-90 38t-38 90v192z"></path></symbol>
<symbol id="fa-external-link" viewBox="0 0 1792 1792"><title>External Link</title><path d="M1408 928v320q0 119-84.5 203.5T1120 1536H288q-119 0-203.5-84.5T0 1248V416q0-119 84.5-203.5T288 128h704q14 0 23 9t9 23v64q0 14-9 23t-23 9H288q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113V928q0-14 9-23t23-9h64q14 0 23 9t9 23zm384-864v512q0 26-19 45t-45 19-45-19l-176-176-652 652q-10 10-23 10t-23-10L695 983q-10-10-10-23t10-23l652-652-176-176q-19-19-19-45t19-45 45-19h512q26 0 45 19t19 45z"></path></symbol>
<symbol id="fa-search" viewBox="0 0 1792 1792"><title>Search</title><path d="M1216 832q0-185-131.5-316.5T768 384 451.5 515.5 320 832t131.5 316.5T768 1280t316.5-131.5T1216 832zm512 832q0 52-38 90t-90 38q-54 0-90-38l-343-342q-179 124-399 124-143 0-273.5-55.5t-225-150-150-225T64 832t55.5-273.5 150-225 225-150T768 128t273.5 55.5 225 150 150 225T1472 832q0 220-124 399l343 343q37 37 37 90z"></path></symbol>
</svg><!-- end insert svg here -->
</svg>
<!-- The application will be rendered inside this element,