a mini social media app for small communities

temporary push to transfer machines

-1
.gitignore
··· 22 23 # vweb and database 24 *.db 25 - *.js 26 27 # Local V install 28 /v/
··· 22 23 # vweb and database 24 *.db 25 26 # Local V install 27 /v/
+6 -1
src/api.v
··· 325 return ctx.redirect('/login') 326 } 327 328 - if user.admin || app.config.dev_mode { 329 sql app.db { 330 delete from Post where id == id 331 delete from Like where post_id == id
··· 325 return ctx.redirect('/login') 326 } 327 328 + post := app.get_post_by_id(id) or { 329 + ctx.error('post does not exist') 330 + return ctx.redirect('/') 331 + } 332 + 333 + if user.admin || user.id == post.author_id { 334 sql app.db { 335 delete from Post where id == id 336 delete from Like where post_id == id
+3 -2
src/pages.v
··· 30 return ctx.redirect('/login') 31 } 32 ctx.title = '${app.config.instance.name} - ${user.get_name()}' 33 - return $veb.html() 34 } 35 36 fn (mut app App) admin(mut ctx Context) veb.Result { ··· 41 42 @['/user/:username'] 43 fn (mut app App) user(mut ctx Context, username string) veb.Result { 44 - user := app.get_user_by_name(username) or { 45 ctx.error('user not found') 46 return ctx.redirect('/') 47 }
··· 30 return ctx.redirect('/login') 31 } 32 ctx.title = '${app.config.instance.name} - ${user.get_name()}' 33 + return ctx.redirect('/user/${user.username}') 34 } 35 36 fn (mut app App) admin(mut ctx Context) veb.Result { ··· 41 42 @['/user/:username'] 43 fn (mut app App) user(mut ctx Context, username string) veb.Result { 44 + user := app.whoami(mut ctx) or { User{} } 45 + viewing := app.get_user_by_name(username) or { 46 ctx.error('user not found') 47 return ctx.redirect('/') 48 }
+24
src/static/js/post.js
···
··· 1 + const like = async id => { 2 + await fetch('/api/post/like?id=' + id, { 3 + method: 'GET' 4 + }) 5 + window.location.reload() 6 + } 7 + 8 + const dislike = async id => { 9 + await fetch('/api/post/dislike?id=' + id, { 10 + method: 'GET' 11 + }) 12 + window.location.reload() 13 + } 14 + 15 + const render_post_body = async (id, mention_pattern) => { 16 + const element = document.getElementById(`post-${id}`) 17 + var body = element.innerText 18 + const matches = body.matchAll(new RegExp(mention_pattern, 'g')) 19 + for (const match of matches) { 20 + console.log('found match: ' + match) 21 + const username = match[0].substring(1) 22 + element.innerHTML = element.innerHTML.replaceAll(username, '<a href="/user/' + username + '">' + username + '</a>') 23 + } 24 + }
+7 -20
src/templates/components/post.html
··· 1 - <div class="post post-full"> 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 - <pre>@post.body</pre> 4 <p><em>likes: @{app.get_net_likes_for_post(post.id)}</em></p> 5 <p><em>posted at: @post.posted_at</em></p> 6 7 @if ctx.is_logged_in() 8 - <script> 9 - const like = async (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, { 18 - method: 'GET' 19 - }) 20 - window.location.reload() 21 - } 22 - </script> 23 - 24 <br> 25 - 26 <div> 27 <button onclick="like(@post.id)"> 28 @if app.does_user_like_post(user.id, post.id) ··· 41 </div> 42 @end 43 44 - @if app.config.dev_mode || (ctx.is_logged_in() && user.admin) || (ctx.is_logged_in() && post.author_id == user.id) 45 <hr> 46 <h4>admin powers:</h4> 47 <form action="/api/post/delete" method="post"> 48 <input 49 type="number"
··· 1 + <div class="post post-full" onload="render_post_body(@{post.id}, '@@@{app.config.user.username_pattern}')"> 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 + <pre id="post-@{post.id}">@post.body</pre> 4 <p><em>likes: @{app.get_net_likes_for_post(post.id)}</em></p> 5 <p><em>posted at: @post.posted_at</em></p> 6 7 @if ctx.is_logged_in() 8 <br> 9 <div> 10 <button onclick="like(@post.id)"> 11 @if app.does_user_like_post(user.id, post.id) ··· 24 </div> 25 @end 26 27 + @if (ctx.is_logged_in() && user.admin) || (ctx.is_logged_in() && post.author_id == user.id) 28 <hr> 29 + @if user.admin 30 <h4>admin powers:</h4> 31 + @else if post.author_id == user.id 32 + <h4>manage post:</h4> 33 + @end 34 <form action="/api/post/delete" method="post"> 35 <input 36 type="number"
-42
src/templates/components/user/admin.html
··· 1 - @if ctx.is_logged_in() && user.admin 2 - <div> 3 - <h2>admin powers:</h2> 4 - <form action="/api/user/set_muted" method="post"> 5 - <input 6 - type="text" 7 - name="id" 8 - id="id" 9 - value="@user.id" 10 - required 11 - readonly 12 - hidden 13 - aria-hidden 14 - > 15 - @if !user.muted 16 - <input 17 - type="checkbox" 18 - name="muted" 19 - id="muted" 20 - value="true" 21 - checked 22 - readonly 23 - hidden 24 - aria-hidden 25 - > 26 - <input type="submit" value="mute"> 27 - @else 28 - <input 29 - type="checkbox" 30 - name="muted" 31 - id="muted" 32 - value="false" 33 - checked 34 - readonly 35 - hidden 36 - aria-hidden 37 - > 38 - <input type="submit" value="unmute"> 39 - @end 40 - </form> 41 - </div> 42 - @end
···
-6
src/templates/components/user/bio.html
··· 1 - @if user.bio != '' 2 - <div> 3 - <h2>bio:</h2> 4 - <p>@user.bio</p> 5 - </div> 6 - @end
···
-10
src/templates/components/user/info.html
··· 1 - <div> 2 - <h2>user info:</h2> 3 - <p>id: @user.id</p> 4 - <p>username: @user.username</p> 5 - <p>display name: @user.get_name()</p> 6 - @if app.logged_in_as(mut ctx, user.id) 7 - <p><a href="/api/user/logout">log out</a></p> 8 - <p><a href="/api/user/full_logout">log out of all devices</a></p> 9 - @end 10 - </div>
···
-20
src/templates/components/user/name.html
··· 1 - <h1> 2 - @{user.get_name()} 3 - (@@@user.username) 4 - 5 - @if user.pronouns != '' 6 - (@user.pronouns) 7 - @end 8 - 9 - @if user.muted && user.admin 10 - [muted admin, somehow] 11 - @else if user.muted 12 - [muted] 13 - @else if user.admin 14 - [admin] 15 - @end 16 - </h1> 17 - 18 - @if app.logged_in_as(mut ctx, user.id) 19 - <p>this is you!</p> 20 - @end
···
-28
src/templates/components/user/new_post.html
··· 1 - <div> 2 - <form action="/api/post/new_post" method="post"> 3 - <h2>new post:</h2> 4 - <input 5 - type="text" 6 - name="title" 7 - id="title" 8 - minlength="@app.config.post.title_min_len" 9 - maxlength="@app.config.post.title_max_len" 10 - pattern="@app.config.post.title_pattern" 11 - placeholder="title" 12 - required 13 - > 14 - <br> 15 - <textarea 16 - name="body" 17 - id="body" 18 - minlength="@app.config.post.body_min_len" 19 - maxlength="@app.config.post.body_max_len" 20 - rows="10" 21 - cols="30" 22 - placeholder="body" 23 - required 24 - ></textarea> 25 - <br> 26 - <input type="submit" value="post!"> 27 - </form> 28 - </div>
···
-6
src/templates/components/user/posts.html
··· 1 - <div> 2 - <h2>posts:</h2> 3 - @for post in app.get_posts_from_user(user.id) 4 - @include 'components/post_small.html' 5 - @end 6 - </div>
···
-57
src/templates/components/user/settings.html
··· 1 - <div> 2 - <h2>settings:</h2> 3 - <form action="/api/user/set_bio" method="post"> 4 - <label for="bio">bio:</label> 5 - <br> 6 - <textarea 7 - name="bio" 8 - id="bio" 9 - cols="30" 10 - rows="10" 11 - minlength="@app.config.user.bio_min_len" 12 - maxlength="@app.config.user.bio_max_len" 13 - required 14 - >@user.bio</textarea> 15 - <input type="submit" value="save"> 16 - </form> 17 - <form action="/api/user/set_pronouns" method="post"> 18 - <label for="pronouns">pronouns:</label> 19 - <input 20 - type="text" 21 - name="pronouns" 22 - id="pronouns" 23 - minlength="@app.config.user.pronouns_min_len" 24 - maxlength="@app.config.user.pronouns_max_len" 25 - pattern="@app.config.user.pronouns_pattern" 26 - value="@user.pronouns" 27 - required 28 - > 29 - <br> 30 - <input type="submit" value="save"> 31 - </form> 32 - <form action="/api/user/set_nickname" method="post"> 33 - <label for="nickname">nickname:</label> 34 - <input 35 - type="text" 36 - name="nickname" 37 - id="nickname" 38 - pattern="@app.config.user.nickname_pattern" 39 - minlength="@app.config.user.nickname_min_len" 40 - maxlength="@app.config.user.nickname_max_len" 41 - value="@{user.nickname or { '' }}" 42 - required 43 - > 44 - <input type="submit" value="save"> 45 - </form> 46 - <form action="/api/user/set_nickname" method="post"> 47 - <input type="submit" value="reset nickname"> 48 - </form> 49 - @if app.config.user.allow_changing_theme 50 - <br> 51 - <form action="/api/user/set_theme" method="post"> 52 - <label for="url">theme:</label> 53 - <input type="url" name="url" id="url" value="@{user.theme or { '' }}"> 54 - <input type="submit" value="save"> 55 - </form> 56 - @end 57 - </div>
···
-16
src/templates/me.html
··· 1 - @include 'partial/header.html' 2 - 3 - @if ctx.is_logged_in() 4 - 5 - @include 'components/user/name.html' 6 - @include 'components/user/bio.html' 7 - @include 'components/user/new_post.html' 8 - @include 'components/user/posts.html' 9 - @include 'components/user/info.html' 10 - @include 'components/user/settings.html 11 - 12 - @else 13 - <p>uh oh, you are not logged in! you can log in <a href="/login">here</a></p> 14 - @end 15 - 16 - @include 'partial/footer.html'
···
+1
src/templates/post.html
··· 1 @include 'partial/header.html' 2 <br> 3 @include 'components/post.html' 4 @include 'partial/footer.html'
··· 1 @include 'partial/header.html' 2 + <script src="/static/js/post.js"></script> 3 <br> 4 @include 'components/post.html' 5 @include 'partial/footer.html'
+177 -5
src/templates/user.html
··· 1 @include 'partial/header.html' 2 3 - @include 'components/user/name.html' 4 - @include 'components/user/bio.html' 5 - @include 'components/user/posts.html' 6 - @include 'components/user/info.html' 7 - @include 'components/user/admin.html' 8 9 @include 'partial/footer.html'
··· 1 @include 'partial/header.html' 2 3 + <h1> 4 + @{viewing.get_name()} 5 + (@@@viewing.username) 6 + 7 + @if viewing.pronouns != '' 8 + (@viewing.pronouns) 9 + @end 10 + 11 + @if viewing.muted && viewing.admin 12 + [muted admin, somehow] 13 + @else if viewing.muted 14 + [muted] 15 + @else if viewing.admin 16 + [admin] 17 + @end 18 + </h1> 19 + 20 + @if app.logged_in_as(mut ctx, viewing.id) 21 + <p>this is you!</p> 22 + 23 + <div> 24 + <form action="/api/post/new_post" method="post"> 25 + <h2>new post:</h2> 26 + <input 27 + type="text" 28 + name="title" 29 + id="title" 30 + minlength="@app.config.post.title_min_len" 31 + maxlength="@app.config.post.title_max_len" 32 + pattern="@app.config.post.title_pattern" 33 + placeholder="title" 34 + required 35 + > 36 + <br> 37 + <textarea 38 + name="body" 39 + id="body" 40 + minlength="@app.config.post.body_min_len" 41 + maxlength="@app.config.post.body_max_len" 42 + rows="10" 43 + cols="30" 44 + placeholder="body" 45 + required 46 + ></textarea> 47 + <br> 48 + <input type="submit" value="post!"> 49 + </form> 50 + </div> 51 + @end 52 + 53 + @if viewing.bio != '' 54 + <div> 55 + <h2>bio:</h2> 56 + <p>@viewing.bio</p> 57 + </div> 58 + @end 59 + 60 + <div> 61 + <h2>posts:</h2> 62 + @for post in app.get_posts_from_user(viewing.id) 63 + @include 'components/post_small.html' 64 + @end 65 + </div> 66 + 67 + <div> 68 + <h2>user info:</h2> 69 + <p>id: @viewing.id</p> 70 + <p>username: @viewing.username</p> 71 + <p>display name: @viewing.get_name()</p> 72 + @if app.logged_in_as(mut ctx, viewing.id) 73 + <p><a href="/api/user/logout">log out</a></p> 74 + <p><a href="/api/user/full_logout">log out of all devices</a></p> 75 + @end 76 + </div> 77 + 78 + @if app.logged_in_as(mut ctx, viewing.id) 79 + <div> 80 + <h2>user settings:</h2> 81 + <form action="/api/user/set_bio" method="post"> 82 + <label for="bio">bio:</label> 83 + <br> 84 + <textarea 85 + name="bio" 86 + id="bio" 87 + cols="30" 88 + rows="10" 89 + minlength="@app.config.user.bio_min_len" 90 + maxlength="@app.config.user.bio_max_len" 91 + required 92 + >@user.bio</textarea> 93 + <input type="submit" value="save"> 94 + </form> 95 + <form action="/api/user/set_pronouns" method="post"> 96 + <label for="pronouns">pronouns:</label> 97 + <input 98 + type="text" 99 + name="pronouns" 100 + id="pronouns" 101 + minlength="@app.config.user.pronouns_min_len" 102 + maxlength="@app.config.user.pronouns_max_len" 103 + pattern="@app.config.user.pronouns_pattern" 104 + value="@user.pronouns" 105 + required 106 + > 107 + <br> 108 + <input type="submit" value="save"> 109 + </form> 110 + <form action="/api/user/set_nickname" method="post"> 111 + <label for="nickname">nickname:</label> 112 + <input 113 + type="text" 114 + name="nickname" 115 + id="nickname" 116 + pattern="@app.config.user.nickname_pattern" 117 + minlength="@app.config.user.nickname_min_len" 118 + maxlength="@app.config.user.nickname_max_len" 119 + value="@{user.nickname or { '' }}" 120 + required 121 + > 122 + <input type="submit" value="save"> 123 + </form> 124 + <form action="/api/user/set_nickname" method="post"> 125 + <input type="submit" value="reset nickname"> 126 + </form> 127 + @if app.config.user.allow_changing_theme 128 + <br> 129 + <form action="/api/user/set_theme" method="post"> 130 + <label for="url">theme:</label> 131 + <input type="url" name="url" id="url" value="@{user.theme or { '' }}"> 132 + <input type="submit" value="save"> 133 + </form> 134 + @end 135 + </div> 136 + @end 137 + 138 + @if ctx.is_logged_in() && user.admin 139 + <div> 140 + <h2>admin powers:</h2> 141 + <form action="/api/user/set_muted" method="post"> 142 + <input 143 + type="text" 144 + name="id" 145 + id="id" 146 + value="@user.id" 147 + required 148 + readonly 149 + hidden 150 + aria-hidden 151 + > 152 + @if !user.muted 153 + <input 154 + type="checkbox" 155 + name="muted" 156 + id="muted" 157 + value="true" 158 + checked 159 + readonly 160 + hidden 161 + aria-hidden 162 + > 163 + <input type="submit" value="mute"> 164 + @else 165 + <input 166 + type="checkbox" 167 + name="muted" 168 + id="muted" 169 + value="false" 170 + checked 171 + readonly 172 + hidden 173 + aria-hidden 174 + > 175 + <input type="submit" value="unmute"> 176 + @end 177 + </form> 178 + </div> 179 + @end 180 181 @include 'partial/footer.html'