a mini social media app for small communities

add user themes and post pinning, along with sanatization for user inputs

+2
config.maple
··· 1 1 dev_mode = false 2 2 3 + static_path = 'src/static' 4 + 3 5 http = { 4 6 port = 8008 5 7 }
+39
doc/themes.md
··· 1 + # themes 2 + 3 + beep supports per-user themes, because we like customizability. by default, no 4 + theme is selected and beep is 100% plain html. 5 + 6 + themes can be applied to beep by going to your profile and scrolling to your 7 + settings, then changing the theme url. 8 + 9 + here is a list of themes that work with beep and themes for beep! 10 + 11 + ## drop-in 12 + 13 + > note that these themes were **not** made specifically for beep, they just work 14 + > well out-of-the-box with it! 15 + 16 + | name | source | css theme url | 17 + |-------------------|----------------------------------------|---------------------------------------------------------------| 18 + | sakura | <https://github.com/oxalorg/sakura> | https://cdn.jsdelivr.net/npm/sakura.css/css/sakura.css | 19 + | water.css (auto) | <https://github.com/kognise/water.css> | https://cdn.jsdelivr.net/npm/water.css@2/out/water.min.css | 20 + | water.css (dark) | <https://github.com/kognise/water.css> | https://cdn.jsdelivr.net/npm/water.css@2/out/dark.min.css | 21 + | water.css (light) | <https://github.com/kognise/water.css> | https://cdn.jsdelivr.net/npm/water.css@2/out/light.min.css | 22 + 23 + > here is a big list of drop-in themes: 24 + > <https://github.com/dohliam/dropin-minimal-css> 25 + 26 + ## beep-specific 27 + 28 + | name | source | css theme url | 29 + |------|--------|---------------| 30 + | | | | 31 + 32 + > there is nothing here yet! do you want to be the one to change that? 33 + 34 + ## built-in 35 + 36 + (todo :D) 37 + 38 + > beep also features some built-in themes, some of which are based on the themes 39 + > present in the "it just works" list!
+33 -9
src/api.v
··· 133 133 } 134 134 135 135 @['/api/user/set_nickname'; post] 136 - fn (mut app App) api_user_set_nickname(mut ctx Context) veb.Result { 137 - mut nickname := ?string(ctx.form['nickname'] or { '' }) 138 - nickname = sanatize(nickname or { '' }) 139 - if (nickname or { '' }) == '' { 140 - nickname = none 141 - } 142 - 136 + fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result { 143 137 user := app.whoami(mut ctx) or { 144 138 ctx.error('you are not logged in!') 145 139 return ctx.redirect('/login') 146 140 } 147 141 142 + mut sanatized_nickname := ?string(sanatize(nickname).trim_space()) 143 + if sanatized_nickname or { '' } == '' { 144 + sanatized_nickname = none 145 + } 146 + 148 147 sql app.db { 149 - update User set nickname = nickname where id == user.id 148 + update User set nickname = sanatized_nickname where id == user.id 150 149 } or { 151 150 ctx.error('failed to change nickname') 152 - eprintln('failed to update nickname for ${user} (${user.nickname} -> ${nickname})') 151 + eprintln('failed to update nickname for ${user} (${user.nickname} -> ${sanatized_nickname})') 153 152 return ctx.redirect('/me') 154 153 } 155 154 ··· 177 176 eprintln('insufficient perms to update mute status for ${user} (${user.muted} -> ${muted})') 178 177 return ctx.redirect('/user/${user.username}') 179 178 } 179 + } 180 + 181 + @['/api/user/set_theme'; post] 182 + fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result { 183 + user := app.whoami(mut ctx) or { 184 + ctx.error('you are not logged in!') 185 + return ctx.redirect('/login') 186 + } 187 + 188 + mut theme := ?string(none) 189 + if url.trim_space() != '' { 190 + theme = sanatize(url).trim_space() 191 + } 192 + 193 + println('set theme to: ${theme}') 194 + 195 + sql app.db { 196 + update User set theme = theme where id == user.id 197 + } or { 198 + ctx.error('failed to change theme') 199 + eprintln('failed to update theme for ${user} (${user.theme} -> ${theme})') 200 + return ctx.redirect('/me') 201 + } 202 + 203 + return ctx.redirect('/me') 180 204 } 181 205 182 206 ////// Posts //////
+10 -1
src/app.v
··· 1 1 module main 2 2 3 - import auth 3 + import veb 4 4 import db.pg 5 + import auth 5 6 import entity { User, Post, Like, LikeCache } 6 7 7 8 pub struct App { 9 + veb.StaticHandler 8 10 pub: 9 11 config Config 10 12 pub mut: ··· 76 78 return none 77 79 } 78 80 return posts[0] 81 + } 82 + 83 + pub fn (app &App) get_pinned_posts() []Post { 84 + posts := sql app.db { 85 + select from Post where pinned == true 86 + } or { [] } 87 + return posts 79 88 } 80 89 81 90 pub fn (app &App) whoami(mut ctx Context) ?User {
+3 -1
src/config.v
··· 4 4 5 5 pub struct Config { 6 6 pub mut: 7 - dev_mode bool 7 + dev_mode bool 8 + static_path string 8 9 http struct { 9 10 pub mut: 10 11 port int ··· 41 42 mut config := Config{} 42 43 43 44 config.dev_mode = loaded.get('dev_mode').to_bool() 45 + config.static_path = loaded.get('static_path').to_str() 44 46 45 47 loaded_http := loaded.get('http') 46 48 config.http.port = loaded_http.get('port').to_int()
+2
src/entity/post.v
··· 11 11 title string 12 12 body string 13 13 14 + pinned bool 15 + 14 16 posted_at time.Time = time.now() 15 17 }
+7
src/entity/user.v
··· 15 15 muted bool 16 16 admin bool 17 17 18 + theme ?string 19 + 18 20 created_at time.Time = time.now() 19 21 } 20 22 ··· 22 24 pub fn (user User) get_name() string { 23 25 return user.nickname or { user.username } 24 26 } 27 + 28 + @[inline] 29 + pub fn (user User) get_theme() string { 30 + return user.theme or { '' } 31 + }
+3 -1
src/main.v
··· 36 36 auth: auth.new(db) 37 37 } 38 38 39 + app.mount_static_folder_at(app.config.static_path, '/static')! 40 + 39 41 init_db(db)! 40 42 41 43 if config.dev_mode { 42 - println('NOTE: YOU ARE IN DEV MODE') 44 + println('\033[1;31mNOTE: YOU ARE IN DEV MODE\033[0m') 43 45 } 44 46 45 47 veb.run[App, Context](mut app, app.config.http.port)
+2 -1
src/pages.v
··· 5 5 6 6 fn (mut app App) index(mut ctx Context) veb.Result { 7 7 ctx.title = 'beep' 8 + user := app.whoami(mut ctx) or { User{} } 8 9 recent_posts := app.get_recent_posts() 9 - user := app.whoami(mut ctx) or { User{} } 10 + pinned_posts := app.get_pinned_posts() 10 11 return $veb.html() 11 12 } 12 13
src/static/style.css

This is a binary file and will not be displayed.

+1
src/templates/assets/style.html
··· 1 + <link rel="stylesheet" href="/static/style.css">
+1 -1
src/templates/components/post.html
··· 1 - <div> 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 3 <p>likes: @{app.get_net_likes_for_post(post.id)}</p> 4 4 <p>@post.body</p>
+6
src/templates/components/post_mini.html
··· 1 + <div class="post post-mini"> 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 + <a href="/post/@post.id">@post.title</a> 5 + </p> 6 + </div>
+2 -2
src/templates/components/post_small.html
··· 1 - <div> 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>: @post.title</p> 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 3 @if post.body.len > 50 4 4 <p>@{post.body[..50]}...</p> 5 5 @else
+10 -1
src/templates/index.html
··· 7 7 @end 8 8 9 9 <div> 10 + @if pinned_posts.len > 0 11 + <h2>pinned posts:</h2> 12 + <div> 13 + @for post in pinned_posts 14 + @include 'components/post_small.html' 15 + @end 16 + </div> 17 + @end 18 + 10 19 <h2>recent posts:</h2> 11 20 <div> 12 21 @if recent_posts.len > 0 13 - @for post in app.get_recent_posts() 22 + @for post in recent_posts 14 23 @include 'components/post_small.html' 15 24 @end 16 25 @else
+7 -1
src/templates/me.html
··· 58 58 value="@{user.nickname or { '' }}" 59 59 required 60 60 > 61 - <input type="submit" value="update"> 61 + <input type="submit" value="save"> 62 62 </form> 63 63 <form action="/api/user/set_nickname" method="post"> 64 64 <input type="submit" value="reset nickname"> 65 + </form> 66 + <br> 67 + <form action="/api/user/set_theme" method="post"> 68 + <label for="url">theme:</label> 69 + <input type="url" name="url" id="url" value="@{user.theme or { '' }}"> 70 + <input type="submit" value="save"> 65 71 </form> 66 72 </div> 67 73
+4
src/templates/partial/header.html
··· 8 8 <meta name="viewport" content="width=device-width, initial-scale=1" /> 9 9 <title>@ctx.title</title> 10 10 @include 'assets/style.html' 11 + @if ctx.is_logged_in() && user.theme != none 12 + <link rel="stylesheet" href="@user.get_theme()"> 13 + @endif 11 14 </head> 12 15 13 16 <body> ··· 39 42 </header> 40 43 41 44 <main> 45 + <!-- TODO: fix this lol --> 42 46 @if ctx.form_error != '' 43 47 <div> 44 48 <p><strong>error:</strong> ctx.form_error</p>