a mini social media app for small communities

clean up a bit and add nsfw indicators for posts

+3
config.maple
··· 61 61 body_min_len = 1 62 62 body_max_len = 1000 63 63 body_pattern = '.*' 64 + 65 + // Whether or not posts can be marked as NSFW. 66 + allow_nsfw = true 64 67 } 65 68 66 69 // User settings.
+3
src/entity/post.v
··· 14 14 body string 15 15 16 16 pinned bool 17 + nsfw bool 17 18 18 19 posted_at time.Time = time.now() 19 20 } ··· 33 34 // this throws a cgen error when put in Post{} 34 35 //todo: report this 35 36 posted_at := time.parse(ct('posted_at')) or { panic(err) } 37 + nsfw := util.map_or_throw[string, bool](ct('nsfw'), |it| it.bool()) 36 38 37 39 return Post{ 38 40 id: ct('id').int() ··· 43 45 title: ct('title') 44 46 body: ct('body') 45 47 pinned: util.map_or_throw[string, bool](ct('pinned'), |it| it.bool()) 48 + nsfw: nsfw 46 49 posted_at: posted_at 47 50 } 48 51 }
+16 -2
src/static/style.css
··· 4 4 padding: 8px; 5 5 } 6 6 7 - .post p, 8 - .notification p { 7 + .post > p, 8 + .notification > p { 9 9 margin: 0; 10 10 } 11 11 12 + .post > pre, 13 + .notification > pre { 14 + margin: 0; 15 + display: inline; 16 + } 17 + 12 18 .post + .post, 13 19 .notification + .notification { 14 20 margin-top: 6px; ··· 16 22 17 23 pre { 18 24 white-space: pre-wrap; 25 + } 26 + 27 + span.nsfw-indicator { 28 + border: 2px solid red; 29 + border-radius: 2px; 30 + padding-left: 4px; 31 + padding-right: 4px; 32 + margin-left: 6px; 19 33 } 20 34 21 35 /*
+71
src/templates/components/new_post.html
··· 1 + <script src="/static/js/text_area_counter.js"></script> 2 + <div> 3 + <form action="/api/post/new_post" method="post"> 4 + <h2>new post:</h2> 5 + 6 + @if replying 7 + <input 8 + type="number" 9 + name="replying_to" 10 + id="replying_to" 11 + required aria-required 12 + readonly aria-readonly 13 + hidden aria-hidden 14 + value="@replying_to" 15 + > 16 + @end 17 + 18 + <p id="title_chars">0/@{app.config.post.title_max_len}</p> 19 + @if replying 20 + <input 21 + type="text" 22 + name="title" 23 + id="title" 24 + value="reply to @{replying_to_user.get_name()}" 25 + required aria-required 26 + readonly aria-readonly 27 + hidden aria-hidden 28 + > 29 + @else 30 + <input 31 + type="text" 32 + name="title" 33 + id="title" 34 + minlength="@app.config.post.title_min_len" 35 + maxlength="@app.config.post.title_max_len" 36 + pattern="@app.config.post.title_pattern" 37 + placeholder="title" 38 + required aria-required 39 + > 40 + @end 41 + <br> 42 + 43 + <p id="body_chars">0/@{app.config.post.body_max_len}</p> 44 + <textarea 45 + name="body" 46 + id="body" 47 + minlength="@app.config.post.body_min_len" 48 + maxlength="@app.config.post.body_max_len" 49 + rows="10" 50 + cols="30" 51 + placeholder="body" 52 + required aria-required 53 + autocomplete="off" aria-autocomplete="off" 54 + ></textarea> 55 + <br> 56 + 57 + @if app.config.post.allow_nsfw 58 + <label for="nsfw">is nsfw:</label> 59 + <input type="checkbox" name="nsfw" id="nsfw" /> 60 + @else 61 + <input type="checkbox" name="nsfw" id="nsfw" hidden aria-hidden /> 62 + @end 63 + 64 + <input type="submit" value="post!"> 65 + </form> 66 + 67 + <script> 68 + add_character_counter('title', 'title_chars', @{app.config.post.title_max_len}) 69 + add_character_counter('body', 'body_chars', @{app.config.post.body_max_len}) 70 + </script> 71 + </div>
+3
src/templates/components/post_mini.html
··· 2 2 <p> 3 3 <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>: 4 4 <a href="/post/@post.id">@post.title</a> 5 + @if post.nsfw 6 + <span class="nsfw-indicator">(<em>nsfw</em>)</span> 7 + @end 5 8 </p> 6 9 </div>
+16 -4
src/templates/components/post_small.html
··· 1 1 <div class="post post-small"> 2 - <p><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>: <span class="post-title">@post.title</span></p> 3 - @if post.body.len > 50 4 - <p>@{post.body[..50]}...</p> 2 + <p> 3 + <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>: 4 + <span class="post-title">@post.title</span> 5 + @if post.nsfw 6 + <span class="nsfw-indicator">(<em>nsfw</em>)</span> 7 + @end 8 + </p> 9 + 10 + @if post.nsfw 11 + <p>view full to see post body</p> 5 12 @else 6 - <p>@post.body</p> 13 + @if post.body.len > 50 14 + <pre id="post-@{post.id}">@{post.body[..50]}...</pre> 15 + @else 16 + <pre id="post-@{post.id}">@post.body</pre> 17 + @end 7 18 @end 19 + 8 20 <p>likes: @{app.get_net_likes_for_post(post.id)} | posted at: @post.posted_at | <a href="/post/@post.id">view full post</a></p> 9 21 </div>
+5 -5
src/templates/edit.html
··· 49 49 50 50 <input type="submit" value="save"> 51 51 </form> 52 - 53 - <script> 54 - add_character_counter('title', 'title_chars', @{app.config.post.title_max_len}) 55 - add_character_counter('body', 'body_chars', @{app.config.post.body_max_len}) 56 - </script> 57 52 </div> 58 53 59 54 <hr> ··· 74 69 <input type="submit" value="delete"> 75 70 </form> 76 71 </div> 72 + 73 + <script> 74 + add_character_counter('title', 'title_chars', @{app.config.post.title_max_len}) 75 + add_character_counter('body', 'body_chars', @{app.config.post.body_max_len}) 76 + </script> 77 77 78 78 @include 'partial/footer.html'
+1 -52
src/templates/new_post.html
··· 8 8 @else 9 9 <h2>make a post...</h2> 10 10 @end 11 - 12 - <div> 13 - <form action="/api/post/new_post" method="post"> 14 - @if replying 15 - <input 16 - type="number" 17 - name="replying_to" 18 - id="replying_to" 19 - required aria-required 20 - readonly aria-readonly 21 - hidden aria-hidden 22 - value="@replying_to" 23 - > 24 - <input 25 - type="text" 26 - name="title" 27 - id="title" 28 - value="reply to @{replying_to_user.get_name()}" 29 - required aria-required 30 - readonly aria-readonly 31 - hidden aria-hidden 32 - > 33 - @else 34 - <input 35 - type="text" 36 - name="title" 37 - id="title" 38 - minlength="@app.config.post.title_min_len" 39 - maxlength="@app.config.post.title_max_len" 40 - pattern="@app.config.post.title_pattern" 41 - placeholder="title" 42 - required aria-required 43 - > 44 - @end 45 - 46 - <br> 47 - <textarea 48 - name="body" 49 - id="body" 50 - minlength="@app.config.post.body_min_len" 51 - maxlength="@app.config.post.body_max_len" 52 - rows="10" 53 - cols="30" 54 - placeholder="in reply to @{replying_to_user.get_name()}..." 55 - required 56 - ></textarea> 57 - 58 - <br> 59 - 60 - <input type="submit" value="post!"> 61 - </form> 62 - </div> 11 + @include 'components/new_post.html' 63 12 @else 64 13 <p>uh oh, you need to be logged in to see this page</p> 65 14 @end
+15 -1
src/templates/post.html
··· 5 5 6 6 <div class="post post-full"> 7 7 <h2> 8 - <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> 8 + <a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"> 9 + <strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong> 10 + </a> 9 11 - 10 12 @if replying_to_post.id == 0 11 13 @post.title 12 14 @else 13 15 replied to <a href="/user/@{replying_to_user.username}">@{replying_to_user.get_name()}</a> 14 16 @end 17 + @if post.nsfw 18 + <span class="nsfw-indicator">(<em>nsfw</em>)</span> 19 + @end 15 20 </h2> 21 + 22 + @if post.nsfw 23 + <details> 24 + <summary>click to show post (nsfw)</summary> 25 + <pre id="post-@{post.id}">@post.body</pre> 26 + </details> 27 + @else 16 28 <pre id="post-@{post.id}">@post.body</pre> 29 + @end 30 + 17 31 <p><em>likes: @{app.get_net_likes_for_post(post.id)}</em></p> 18 32 <p><em>posted at: @post.posted_at</em></p> 19 33
+3
src/templates/saved_posts.html
··· 16 16 <p> 17 17 <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>: 18 18 <a href="/post/@post.id">@post.title</a> 19 + @if post.nsfw 20 + <span class="nsfw-indicator">(<em>nsfw</em>)</span> 21 + @end 19 22 <button onclick="save(@post.id)" style="display: inline-block;">unsave</button> 20 23 </p> 21 24 </div>
+3
src/templates/saved_posts_for_later.html
··· 16 16 <p> 17 17 <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>: 18 18 <a href="/post/@post.id">@post.title</a> 19 + @if post.nsfw 20 + <span class="nsfw-indicator">(<em>nsfw</em>)</span> 21 + @end 19 22 <button onclick="save_for_later(@post.id)" style="display: inline-block;">unsave</button> 20 23 </p> 21 24 </div>
+8
src/templates/search.html
··· 67 67 post_link.innerText = result.post.title 68 68 p.appendChild(post_link) 69 69 70 + if (result.post.nsfw) 71 + { 72 + const nsfw_indicator = document.createElement('span') 73 + nsfw_indicator.classList.add('nsfw-indicator') 74 + nsfw_indicator.innerHTML = '(<em>nsfw</em>)'; 75 + p.appendChild(nsfw_indicator) 76 + } 77 + 70 78 element.appendChild(p) 71 79 results.appendChild(element) 72 80 }
+1 -42
src/templates/user.html
··· 23 23 24 24 @if app.logged_in_as(mut ctx, viewing.id) 25 25 <p>this is you!</p> 26 - 27 26 @if !user.automated 28 - <script src="/static/js/text_area_counter.js"></script> 29 - <div> 30 - <form action="/api/post/new_post" method="post"> 31 - <h2>new post:</h2> 32 - 33 - <p id="title_chars">0/@{app.config.post.title_max_len}</p> 34 - <input 35 - type="text" 36 - name="title" 37 - id="title" 38 - minlength="@app.config.post.title_min_len" 39 - maxlength="@app.config.post.title_max_len" 40 - pattern="@app.config.post.title_pattern" 41 - placeholder="title" 42 - required aria-required 43 - autocomplete="off" aria-autocomplete="off" 44 - > 45 - <br> 46 - 47 - <p id="body_chars">0/@{app.config.post.body_max_len}</p> 48 - <textarea 49 - name="body" 50 - id="body" 51 - minlength="@app.config.post.body_min_len" 52 - maxlength="@app.config.post.body_max_len" 53 - rows="10" 54 - cols="30" 55 - placeholder="body" 56 - required aria-required 57 - autocomplete="off" aria-autocomplete="off" 58 - ></textarea> 59 - <br> 60 - 61 - <input type="submit" value="post!"> 62 - </form> 63 - 64 - <script> 65 - add_character_counter('title', 'title_chars', @{app.config.post.title_max_len}) 66 - add_character_counter('body', 'body_chars', @{app.config.post.body_max_len}) 67 - </script> 68 - </div> 27 + @include 'components/new_post.html' 69 28 <hr> 70 29 @end 71 30 @end
+7
src/webapp/api.v
··· 509 509 return ctx.redirect('/post/new') 510 510 } 511 511 512 + nsfw := 'nsfw' in ctx.form 513 + if nsfw && !app.config.post.allow_nsfw { 514 + ctx.error('nsfw posts are not allowed on this instance') 515 + return ctx.redirect('/post/new') 516 + } 517 + 512 518 mut post := Post{ 513 519 author_id: user.id 514 520 title: title 515 521 body: body 522 + nsfw: nsfw 516 523 } 517 524 518 525 if replying_to != 0 {
+2
src/webapp/config.v
··· 45 45 body_min_len int 46 46 body_max_len int 47 47 body_pattern string 48 + allow_nsfw bool 48 49 } 49 50 user struct { 50 51 pub mut: ··· 111 112 config.post.body_min_len = loaded_post.get('body_min_len').to_int() 112 113 config.post.body_max_len = loaded_post.get('body_max_len').to_int() 113 114 config.post.body_pattern = loaded_post.get('body_pattern').to_str() 115 + config.post.allow_nsfw = loaded_post.get('allow_nsfw').to_bool() 114 116 115 117 loaded_user := loaded.get('user') 116 118 config.user.username_min_len = loaded_user.get('username_min_len').to_int()
+6
src/webapp/pages.v
··· 112 112 } 113 113 ctx.title = '${app.config.instance.name} - ${user.get_name()}' 114 114 posts := app.get_posts_from_user(viewing.id, 10) 115 + 116 + // needed for new_post component 117 + replying := false 118 + replying_to := 0 119 + replying_to_user := User{} 120 + 115 121 return $veb.html('../templates/user.html') 116 122 } 117 123