a mini social media app for small communities

add dislike functionality, fix some nitpicks, and as always a bit of cleanup

Changed files
+125 -62
doc
src
+1 -1
config.maple
··· 20 20 title_pattern = '(.|\s)*' 21 21 22 22 body_min_len = 1 23 - body_max_len = 500 23 + body_max_len = 1000 24 24 body_pattern = '(.|\s)*' 25 25 } 26 26
+3 -3
doc/todo.md
··· 4 4 5 5 ## in-progress 6 6 7 - - [/] post:likes/dislikes 8 - 9 7 ## planing 10 8 11 - - [ ] site:stylesheet (and a toggle for html-only mode) 12 9 - [ ] post:mentioning ('tagging') other users in posts 13 10 - [ ] post:replies 14 11 - [ ] post:tags ('hashtags') ··· 26 23 - [x] user:nicknames 27 24 - [x] user:bio/about me 28 25 - [x] user:listed pronouns 26 + - [x] post:likes/dislikes 27 + - [ ] ~~site:stylesheet (and a toggle for html-only mode)~~ 28 + - replaced with per-user optional stylesheets
+58 -2
src/api.v
··· 343 343 } 344 344 } 345 345 346 - @['/api/post/toggle_like'] 347 - fn (mut app App) api_post_toggle_like(mut ctx Context, id int) veb.Result { 346 + @['/api/post/like'] 347 + fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result { 348 348 user := app.whoami(mut ctx) or { 349 349 return ctx.unauthorized('not logged in') 350 350 } ··· 364 364 } 365 365 return ctx.ok('unliked post') 366 366 } else { 367 + // remove the old dislike, if it exists 368 + if app.does_user_dislike_post(user.id, post.id) { 369 + sql app.db { 370 + delete from Like where user_id == user.id && post_id == post.id 371 + } or { 372 + eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it') 373 + } 374 + } 375 + 367 376 like := Like{ 368 377 user_id: user.id 369 378 post_id: post.id ··· 380 389 return ctx.ok('liked post') 381 390 } 382 391 } 392 + 393 + @['/api/post/dislike'] 394 + fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result { 395 + user := app.whoami(mut ctx) or { 396 + return ctx.unauthorized('not logged in') 397 + } 398 + 399 + post := app.get_post_by_id(id) or { 400 + return ctx.server_error('post does not exist') 401 + } 402 + 403 + if app.does_user_dislike_post(user.id, post.id) { 404 + sql app.db { 405 + delete from Like where user_id == user.id && post_id == post.id 406 + // yeet the old cached like value 407 + delete from LikeCache where post_id == post.id 408 + } or { 409 + eprintln('user ${user.id} failed to unlike post ${id}') 410 + return ctx.server_error('failed to unlike post') 411 + } 412 + return ctx.ok('undisliked post') 413 + } else { 414 + // remove the old like, if it exists 415 + if app.does_user_like_post(user.id, post.id) { 416 + sql app.db { 417 + delete from Like where user_id == user.id && post_id == post.id 418 + } or { 419 + eprintln('user ${user.id} failed to remove like on post ${id} when disliking it') 420 + } 421 + } 422 + 423 + like := Like{ 424 + user_id: user.id 425 + post_id: post.id 426 + is_like: false 427 + } 428 + sql app.db { 429 + insert like into Like 430 + // yeet the old cached like value 431 + delete from LikeCache where post_id == post.id 432 + } or { 433 + eprintln('user ${user.id} failed to dislike post ${id}') 434 + return ctx.server_error('failed to dislike post') 435 + } 436 + return ctx.ok('disliked post') 437 + } 438 + }
+11
src/app.v
··· 169 169 return !likes.first().is_like 170 170 } 171 171 172 + pub fn (app &App) does_user_like_or_dislike_post(user_id int, post_id int) bool { 173 + likes := sql app.db { 174 + select from Like where user_id == user_id && post_id == post_id 175 + } or { [] } 176 + if likes.len > 1 { 177 + // something is very wrong lol 178 + eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})') 179 + } 180 + return likes.len == 1 181 + } 182 + 172 183 pub fn (app &App) get_net_likes_for_post(post_id int) int { 173 184 // check cache 174 185 cache := sql app.db {
+12
src/static/style.css
··· 6 6 .post p { 7 7 margin: 0; 8 8 } 9 + 10 + pre { 11 + white-space: pre-wrap; 12 + } 13 + 14 + /* 15 + * some themes make input fields display: block, which overrides my hidden 16 + * attribute. to resolve that, i will just override the override. 17 + */ 18 + input[hidden] { 19 + display: none !important; 20 + }
+9 -43
src/templates/admin.html
··· 7 7 @else 8 8 9 9 <h1>admin dashboard</h1> 10 - <p>logged in as: @user</p> 10 + <p>logged in as:</p> 11 + <pre>@user</pre> 11 12 <div> 12 13 <h2>user list:</h2> 13 - <div> 14 + <ul> 14 15 @for u in app.get_users() 15 - <div> 16 - <a href="/user/@u.username">@u.get_name() (@@@u.username) [@u.id]</a> 17 - <p>muted=@u.muted, admin=@u.admin</p> 18 - <p>created_at=@u.created_at</p> 19 - <form action="/api/user/set_muted" method="post"> 20 - <input 21 - type="text" 22 - name="id" 23 - id="id" 24 - value="@u.id" 25 - required 26 - readonly 27 - hidden 28 - > 29 - @if !user.muted 30 - <input 31 - type="checkbox" 32 - name="muted" 33 - id="muted" 34 - value="true" 35 - checked 36 - readonly 37 - hidden 38 - > 39 - <input type="submit" value="mute"> 40 - @else 41 - <input 42 - type="checkbox" 43 - name="muted" 44 - id="muted" 45 - value="false" 46 - checked 47 - readonly 48 - hidden 49 - > 50 - <input type="submit" value="unmute"> 51 - @end 52 - </form> 53 - </div> 54 - <hr> 16 + <li> 17 + <a href="/user/@u.username">@u.get_name()</a> 18 + (@@@u.username) [@u.id] 19 + {muted=@u.muted, admin=@u.admin, created_at=@u.created_at} 20 + </li> 55 21 @end 56 - </div> 22 + </ul> 57 23 </div> 58 24 59 25 @end
+25 -9
src/templates/components/post.html
··· 1 1 <div class="post post-full"> 2 2 <h2><a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a> - @post.title</h2> 3 - <p>@post.body</p> 3 + <pre>@post.body</pre> 4 4 <p><em>likes: @{app.get_net_likes_for_post(post.id)}</em></p> 5 5 <p><em>posted at: @post.posted_at</em></p> 6 6 7 7 @if ctx.is_logged_in() 8 8 <script> 9 9 const like = async (id) => { 10 - await fetch('/api/post/toggle_like?id=' + id, { 10 + await fetch('/api/post/like?id=' + id, { 11 + method: 'GET' 12 + }) 13 + window.location.reload() 14 + } 15 + 16 + const dislike = async (id) => { 17 + await fetch('/api/post/dislike?id=' + id, { 11 18 method: 'GET' 12 19 }) 13 20 window.location.reload() ··· 16 23 17 24 <br> 18 25 19 - <button onclick="like(@post.id)"> 20 - @if app.does_user_like_post(user.id, post.id) 21 - liked! 22 - @else 23 - like 24 - @end 25 - </button> 26 + <div> 27 + <button onclick="like(@post.id)"> 28 + @if app.does_user_like_post(user.id, post.id) 29 + liked :D 30 + @else 31 + like 32 + @end 33 + </button> 34 + <button onclick="dislike(@post.id)"> 35 + @if app.does_user_dislike_post(user.id, post.id) 36 + disliked D: 37 + @else 38 + dislike 39 + @end 40 + </button> 41 + </div> 26 42 @end 27 43 28 44 @if app.config.dev_mode || (ctx.is_logged_in() && user.admin) || (ctx.is_logged_in() && post.author_id == user.id)
+3
src/templates/components/user/admin.html
··· 10 10 required 11 11 readonly 12 12 hidden 13 + aria-hidden 13 14 > 14 15 @if !user.muted 15 16 <input ··· 20 21 checked 21 22 readonly 22 23 hidden 24 + aria-hidden 23 25 > 24 26 <input type="submit" value="mute"> 25 27 @else ··· 31 33 checked 32 34 readonly 33 35 hidden 36 + aria-hidden 34 37 > 35 38 <input type="submit" value="unmute"> 36 39 @end
+1 -1
src/templates/components/user/name.html
··· 3 3 (@@@user.username) 4 4 5 5 @if user.pronouns != '' 6 - <h2>(@user.pronouns)</h2> 6 + (@user.pronouns) 7 7 @end 8 8 9 9 @if user.muted && user.admin
-1
src/templates/partial/footer.html
··· 5 5 <p><a href="https://github.com/emmathemartian/beep">source</a></p> 6 6 @if app.config.dev_mode 7 7 <p>token: @{ctx.get_cookie('token')}</p> 8 - <p>user: @{user}</p> 9 8 @end 10 9 </footer> 11 10
+2 -2
src/templates/partial/header.html
··· 24 24 <a href="/">home</a> 25 25 - 26 26 27 - @if app.config.dev_mode 27 + @if app.config.dev_mode || (ctx.is_logged_in() && user.admin) 28 28 <a href="/admin">admin</a> 29 29 - 30 30 @end ··· 44 44 <!-- TODO: fix this lol --> 45 45 @if ctx.form_error != '' 46 46 <div> 47 - <p><strong>error:</strong> ctx.form_error</p> 47 + <p><strong>error:</strong> @ctx.form_error</p> 48 48 </div> 49 49 @end