a mini social media app for small communities

basic search feature implemented!

Changed files
+150 -65
src
entity
static
templates
webapp
+1 -1
src/entity/user.v
··· 52 52 id: util.or_throw[string](row.vals[0]).int() 53 53 username: util.or_throw[string](row.vals[1]) 54 54 nickname: if row.vals[2] == none { ?string(none) } else { 55 - util.or_throw[string](row.vals[3]) 55 + util.or_throw[string](row.vals[2]) 56 56 } 57 57 password: 'haha lol, nope' 58 58 password_salt: 'haha lol, nope'
+10 -2
src/static/js/search.js
··· 1 - const search = async (query, limit, offset) => { 2 - const data = await fetch(`/api/search?query=${query}&limit=${limit}&offset=${offset}`, { 1 + const search_posts = async (query, limit, offset) => { 2 + const data = await fetch(`/api/post/search?query=${query}&limit=${limit}&offset=${offset}`, { 3 + method: 'GET' 4 + }) 5 + const json = await data.json() 6 + return json 7 + } 8 + 9 + const search_users = async (query, limit, offset) => { 10 + const data = await fetch(`/api/user/search?query=${query}&limit=${limit}&offset=${offset}`, { 3 11 method: 'GET' 4 12 }) 5 13 const json = await data.json()
+5
src/templates/components/user_card_mini.html
··· 1 + <div class="user-card user-card-mini"> 2 + <p> 3 + <a href="/user/@{user.username}">@{user.get_name()}</a> 4 + </p> 5 + </div>
+112 -49
src/templates/search.html
··· 7 7 8 8 <div> 9 9 <input type="text" name="query" id="query"> 10 + <div> 11 + <p>search for:</p> 12 + <input type="radio" name="search-for" id="search-for-posts" value="posts" checked aria-checked> 13 + <label for="search-for-posts">posts</label> 14 + <input type="radio" name="search-for" id="search-for-users" value="users"> 15 + <label for="search-for-users">users</label> 16 + </div> 17 + <br> 10 18 <button id="search">search</button> 11 19 </div> 12 20 ··· 39 47 offset = 0 40 48 } 41 49 42 - document.getElementById('search').addEventListener('click', async () => { 43 - results.innerHTML = '' // yeet the children! 44 - pages.innerHTML = '' // yeet more children! 50 + const add_post_result = result => { 51 + // same as components/post_mini.html except js 52 + const element = document.createElement('div') 53 + element.classList.add('post', 'post-mini') 54 + const p = document.createElement('p') 55 + 56 + const user_link = document.createElement('a') 57 + user_link.href = '/user/' + result.author.username 58 + const user_text = document.createElement('strong') 59 + user_text.innerText = get_display_name(result.author) 60 + user_link.appendChild(user_text) 61 + p.appendChild(user_link) 62 + 63 + p.innerHTML += ': ' 64 + 65 + const post_link = document.createElement('a') 66 + post_link.href = '/post/' + result.post.id 67 + post_link.innerText = result.post.title 68 + p.appendChild(post_link) 69 + 70 + element.appendChild(p) 71 + results.appendChild(element) 72 + } 73 + 74 + const add_user_result = user => { 75 + const element = document.createElement('div') 76 + const p = document.createElement('p') 77 + const user_link = document.createElement('a') 78 + user_link.href = '/user/' + user.username 79 + user_link.innerText = get_display_name(user) 80 + p.appendChild(user_link) 81 + element.appendChild(p) 82 + results.appendChild(element) 83 + } 45 84 46 - console.log('search: ', query.value, limit, offset) 85 + const add_pages = () => { 86 + // creates a separator 87 + const sep = () => { 88 + const span = document.createElement('span') 89 + span.innerText = ' - ' 90 + pages.appendChild(span) 91 + } 47 92 48 - const search_results = await search(query.value, limit, offset) 49 - if (search_results.length >= 0) { 50 - for (result of search_results) { 51 - // same as components/post_mini.html except js 52 - const element = document.createElement('div') 53 - element.classList.add('post', 'post-mini') 54 - const p = document.createElement('p') 93 + const first_link = document.createElement('a') 94 + // we escape the $ here because otherwise V will try to perform replacements at compile-time. 95 + //todo: report this, this behaviour should be changed or at least looked into further. 96 + first_link.href = '/search?q=' + query.value + '&limit=' + limit + '&offset=0' 97 + first_link.innerText = '0' 98 + pages.appendChild(first_link) 55 99 56 - const user_link = document.createElement('a') 57 - user_link.href = '/user/' + result.author.username 58 - const user_text = document.createElement('strong') 59 - user_text.innerText = get_display_name(result.author) 60 - user_link.appendChild(user_text) 61 - p.appendChild(user_link) 100 + sep() 62 101 63 - p.innerHTML += ': ' 102 + const back_link = document.createElement('a') 103 + back_link.href = '/search?q=' + query.value + '&limit=' + limit + '&offset=' + Math.min(0, offset - 10) 104 + back_link.innerText = '<' 105 + pages.appendChild(back_link) 106 + 107 + sep() 108 + 109 + const next_link = document.createElement('a') 110 + next_link.href = '/search?q=' + query.value + '&limit=' + limit + '&offset=' + (offset + 10) 111 + next_link.innerText = '>' 112 + pages.appendChild(next_link) 113 + } 64 114 65 - const post_link = document.createElement('a') 66 - post_link.href = '/post/' + result.post.id 67 - post_link.innerText = result.post.title 68 - p.appendChild(post_link) 115 + document.getElementById('search').addEventListener('click', async () => { 116 + results.innerHTML = '' // yeet the children! 117 + pages.innerHTML = '' // yeet more children! 69 118 70 - element.appendChild(p) 71 - results.appendChild(element) 119 + var search_for 120 + for (const radio of document.getElementsByName('search-for')) { 121 + if (radio.checked) { 122 + search_for = radio.value 123 + break 72 124 } 73 - 74 - // set up pagination 75 - if (offset > 0) { 76 - // creates a separator 77 - function sep() { 78 - const span = document.createElement('span') 79 - span.innerText = ' - ' 80 - pages.appendChild(span) 81 - } 125 + } 126 + if (search_for == undefined) { 127 + alert('please select either "users" or "posts" to search for.') 128 + return 129 + } 82 130 83 - const first_link = document.createElement('a') 84 - // we escape the $ here because otherwise V will try to perform replacements at compile-time. 85 - //todo: report this, this behaviour should be changed or at least looked into further. 86 - first_link.href = '/search?q=' + query.value + '&limit=' + limit + '&offset=0' 87 - first_link.innerText = '0' 88 - pages.appendChild(first_link) 131 + console.log('search: ', query.value, limit, offset) 89 132 90 - sep() 133 + var search_results 134 + if (search_for == 'users') { 135 + search_results = await search_users(query.value, limit, offset) 136 + } else if (search_for == 'posts') { 137 + search_results = await search_posts(query.value, limit, offset) 138 + } else { 139 + // this should never happen 140 + alert('something wrong occured while searching, please report this (01)') 141 + return 142 + } 91 143 92 - const back_link = document.createElement('a') 93 - back_link.href = '/search?q=' + query.value + '&limit=' + limit + '&offset=' + Math.min(0, offset - 10) 94 - back_link.innerText = '<' 95 - pages.appendChild(back_link) 144 + console.log(search_results) 96 145 97 - sep() 146 + if (search_results.length >= 0) { 147 + // i iterate inside the if statements so that i do not have to perform a redundant 148 + // string comparison for every single result. 149 + if (search_for == 'users') { 150 + for (result of search_results) { 151 + add_user_result(result) 152 + } 153 + } else if (search_for == 'posts') { 154 + for (result of search_results) { 155 + add_post_result(result) 156 + } 157 + } else { 158 + // this should never happen 159 + alert('something wrong occured while searching, please report this (02)') 160 + return 161 + } 98 162 99 - const next_link = document.createElement('a') 100 - next_link.href = '/search?q=' + query.value + '&limit=' + limit + '&offset=' + (offset + 10) 101 - next_link.innerText = '>' 102 - pages.appendChild(next_link) 163 + // set up pagination, but only if we actually have pages to display 164 + if (offset > 0) { 165 + add_pages() 103 166 } 104 167 } else { 105 168 results.innerText = 'no results!'
+22 -13
src/webapp/api.v
··· 5 5 import entity { Like, LikeCache, Post, Site, User, Notification } 6 6 import database { PostSearchResult } 7 7 8 + // search_hard_limit is the maximum limit for a search query, used to prevent 9 + // people from requesting searches with huge limits and straining the SQL server 10 + pub const search_hard_limit := 50 11 + 8 12 ////// user ////// 9 13 10 14 @['/api/user/register'; post] ··· 402 406 return ctx.redirect('/') 403 407 } 404 408 409 + @['/api/user/search'; get] 410 + fn (mut app App) api_user_search(mut ctx Context, query string, limit int, offset int) veb.Result { 411 + if limit >= search_hard_limit { 412 + return ctx.text('limit exceeds hard limit (${search_hard_limit})') 413 + } 414 + users := app.search_for_users(query, limit, offset) 415 + return ctx.json[[]User](users) 416 + } 417 + 405 418 ////// post ////// 406 419 407 420 @['/api/post/new_post'; post] ··· 611 624 return ctx.json[Post](post) 612 625 } 613 626 627 + @['/api/post/search'; get] 628 + fn (mut app App) api_post_search(mut ctx Context, query string, limit int, offset int) veb.Result { 629 + if limit >= search_hard_limit { 630 + return ctx.text('limit exceeds hard limit (${search_hard_limit})') 631 + } 632 + posts := app.search_for_posts(query, limit, offset) 633 + return ctx.json[[]PostSearchResult](posts) 634 + } 635 + 614 636 ////// site ////// 615 637 616 638 @['/api/site/set_motd'; post] ··· 634 656 return ctx.redirect('/') 635 657 } 636 658 } 637 - 638 - ////// Misc ////// 639 - 640 - pub const search_hard_limit := 50 641 - 642 - @['/api/search'; get] 643 - fn (mut app App) api_search(mut ctx Context, query string, limit int, offset int) veb.Result { 644 - if limit >= search_hard_limit { 645 - return ctx.text('limit exceeds hard limit (${search_hard_limit})') 646 - } 647 - posts := app.search_for_posts(query, limit, offset) 648 - return ctx.json[[]PostSearchResult](posts) 649 - }