+14
-2
build.maple
+14
-2
build.maple
···
43
43
run = 'docker rm beep-database && docker volume rm beep-data'
44
44
}
45
45
46
+
task:run.watch = {
47
+
description = 'Watch/run beep'
48
+
category = 'run'
49
+
run = '${v} -d veb_livereload watch run ${v_main} config.maple'
50
+
}
51
+
52
+
task:run.watch.real = {
53
+
description = 'Watch/run beep using config.real.maple'
54
+
category = 'run'
55
+
run = '${v} watch run ${v_main} config.real.maple'
56
+
}
57
+
46
58
task:run = {
47
59
description = 'Run beep'
48
60
category = 'run'
49
-
run = '${v} -d veb_livereload watch run ${v_main} config.maple'
61
+
run = '${v} run ${v_main} config.maple'
50
62
}
51
63
52
64
task:run.real = {
53
65
description = 'Run beep using config.real.maple'
54
66
category = 'run'
55
-
run = '${v} -d veb_livereload watch run ${v_main} config.real.maple'
67
+
run = '${v} -d veb_livereload run ${v_main} config.real.maple'
56
68
}
57
69
58
70
task:cloc = {
+12
doc/database_spec.md
+12
doc/database_spec.md
···
81
81
| `user_id` | int | the user that receives this notification |
82
82
| `summary` | string | the summary for this notification |
83
83
| `body` | string | the full text for this notification |
84
+
85
+
## `SavedPost`
86
+
87
+
> a list of saved posts for a user
88
+
89
+
| name | type | desc |
90
+
|-----------|------|--------------------------------------------------|
91
+
| `id` | int | identifier for this entry, this is mostly unused |
92
+
| `post_id` | int | the id of the post this entry relates to |
93
+
| `user_id` | int | the id of the user that saved this post |
94
+
| `saved` | bool | if this post is saved |
95
+
| `later` | bool | if this post is saved in "read later" |
+6
-3
doc/todo.md
+6
-3
doc/todo.md
···
23
23
created-before:<date>
24
24
is:admin
25
25
```
26
+
- [ ] misc:replace `SEARCH *` with `SEARCH <column>`
26
27
27
28
## planing
28
29
29
30
> p.s. when initially writing "planing," i made a typo. it should be "planning."
30
31
> however, i will not be fixing it, because it is funny.
31
32
32
-
- [ ] post:saving (add the post to a list of saved posts that a user can view later)
33
33
- [ ] post:add more embedded link handling! (discord, github, gitlab, codeberg, etc)
34
34
- [ ] user:follow other users (send notifications on new posts)
35
+
- [ ] site:webhooks
36
+
- could be used so that a github webhook can send a message when a new commit is pushed to beep!
37
+
- [ ] site:log new accounts, account deletions, etc etc in an admin-accessible site log
38
+
- this should be set up to only log things when an admin enables it in the site config, so as to only log when necessary
35
39
36
40
## ideas
37
41
38
42
- [ ] user:per-user post pins
39
43
- could be used as an alternative for a bio to include more information perhaps
40
44
- [ ] site:rss feed?
41
-
- [ ] site:webhooks
42
-
- could be used so that a github webhook can send a message when a new commit is pushed to beep!
43
45
44
46
## done
45
47
···
60
62
as images, music links, etc)
61
63
- should have special handling for spotify, apple music, youtube,
62
64
discord, and other common links. we want those ones to look fancy!
65
+
- [x] post:saving (add the post to a list of saved posts that a user can view later)
63
66
- [x] site:message of the day (admins can add a welcome message displayed on index.html)
64
67
65
68
## graveyard
+1
src/database/post.v
+1
src/database/post.v
···
163
163
// search_for_posts searches for posts matching the given query.
164
164
// todo: query options/filters, such as user:beep, !excluded-text, etc
165
165
pub fn (app &DatabaseAccess) search_for_posts(query string, limit int, offset int) []PostSearchResult {
166
+
//TODO: SANATIZE
166
167
sql_query := "\
167
168
SELECT *, CASE
168
169
WHEN title LIKE '%${query}%' THEN 1
+157
src/database/saved_post.v
+157
src/database/saved_post.v
···
1
+
module database
2
+
3
+
import entity { SavedPost, Post }
4
+
5
+
// get_saved_posts_for gets all SavedPost objects for a given user.
6
+
pub fn (app &DatabaseAccess) get_saved_posts_for(user_id int) []SavedPost {
7
+
saved_posts := sql app.db {
8
+
select from SavedPost where user_id == user_id && saved == true
9
+
} or { [] }
10
+
return saved_posts
11
+
}
12
+
13
+
// get_saved_posts_as_post_for gets all saved posts for a given user converted
14
+
// to Post objects.
15
+
pub fn (app &DatabaseAccess) get_saved_posts_as_post_for(user_id int) []Post {
16
+
saved_posts := sql app.db {
17
+
select from SavedPost where user_id == user_id && saved == true
18
+
} or { [] }
19
+
posts := saved_posts.map(fn [app] (it SavedPost) Post {
20
+
return app.get_post_by_id(it.post_id) or {
21
+
// if the post does not exist, we will remove it now
22
+
sql app.db {
23
+
delete from SavedPost where id == it.id
24
+
} or {
25
+
eprintln('get_saved_posts_as_post_for: failed to remove non-existent post from saved post: ${it}')
26
+
}
27
+
app.get_unknown_post()
28
+
}
29
+
}).filter(it.id != 0)
30
+
return posts
31
+
}
32
+
33
+
// get_saved_posts_as_post_for gets all posts saved for later for a given user
34
+
// converted to Post objects.
35
+
pub fn (app &DatabaseAccess) get_saved_for_later_posts_as_post_for(user_id int) []Post {
36
+
saved_posts := sql app.db {
37
+
select from SavedPost where user_id == user_id && later == true
38
+
} or { [] }
39
+
posts := saved_posts.map(fn [app] (it SavedPost) Post {
40
+
return app.get_post_by_id(it.post_id) or {
41
+
// if the post does not exist, we will remove it now
42
+
sql app.db {
43
+
delete from SavedPost where id == it.id
44
+
} or {
45
+
eprintln('get_saved_for_later_posts_as_post_for: failed to remove non-existent post from saved post: ${it}')
46
+
}
47
+
app.get_unknown_post()
48
+
}
49
+
}).filter(it.id != 0)
50
+
return posts
51
+
}
52
+
53
+
// get_user_post_save_status returns the SavedPost object representing the user
54
+
// and post id. returns none if the post is not saved anywhere.
55
+
pub fn (app &DatabaseAccess) get_user_post_save_status(user_id int, post_id int) ?SavedPost {
56
+
saved_posts := sql app.db {
57
+
select from SavedPost where user_id == user_id && post_id == post_id
58
+
} or { [] }
59
+
if saved_posts.len == 1 {
60
+
return saved_posts[0]
61
+
} else if saved_posts.len == 0 {
62
+
return none
63
+
} else {
64
+
eprintln('get_user_post_save_status: user `${user_id}` had multiple SavedPost entries for post `${post_id}')
65
+
return none
66
+
}
67
+
}
68
+
69
+
pub fn (app &DatabaseAccess) is_post_saved_by(user_id int, post_id int) bool {
70
+
saved_post := app.get_user_post_save_status(user_id, post_id) or {
71
+
return false
72
+
}
73
+
return saved_post.saved
74
+
}
75
+
76
+
pub fn (app &DatabaseAccess) is_post_saved_for_later_by(user_id int, post_id int) bool {
77
+
saved_post := app.get_user_post_save_status(user_id, post_id) or {
78
+
return false
79
+
}
80
+
return saved_post.later
81
+
}
82
+
83
+
// toggle_save_post (un)saves the given post for the user. returns true if this
84
+
// succeeds and false otherwise.
85
+
pub fn (app &DatabaseAccess) toggle_save_post(user_id int, post_id int) bool {
86
+
if s := app.get_user_post_save_status(user_id, post_id) {
87
+
if s.saved {
88
+
sql app.db {
89
+
update SavedPost set saved = false where id == s.id
90
+
} or {
91
+
eprintln('toggle_save_post: failed to unsave post (user_id: ${user_id}, post_id: ${post_id})')
92
+
return false
93
+
}
94
+
return true
95
+
} else {
96
+
sql app.db {
97
+
update SavedPost set saved = true where id == s.id
98
+
} or {
99
+
eprintln('toggle_save_post: failed to save post (user_id: ${user_id}, post_id: ${post_id})')
100
+
return false
101
+
}
102
+
return true
103
+
}
104
+
} else {
105
+
post := SavedPost{
106
+
user_id: user_id
107
+
post_id: post_id
108
+
saved: true
109
+
later: false
110
+
}
111
+
sql app.db {
112
+
insert post into SavedPost
113
+
} or {
114
+
eprintln('toggle_save_post: failed to create saved post: ${post}')
115
+
return false
116
+
}
117
+
return true
118
+
}
119
+
}
120
+
121
+
// toggle_save_for_later_post (un)saves the given post for later for the user.
122
+
// returns true if this succeeds and false otherwise.
123
+
pub fn (app &DatabaseAccess) toggle_save_for_later_post(user_id int, post_id int) bool {
124
+
if s := app.get_user_post_save_status(user_id, post_id) {
125
+
if s.later {
126
+
sql app.db {
127
+
update SavedPost set later = false where id == s.id
128
+
} or {
129
+
eprintln('toggle_save_post: failed to unsave post for later (user_id: ${user_id}, post_id: ${post_id})')
130
+
return false
131
+
}
132
+
return true
133
+
} else {
134
+
sql app.db {
135
+
update SavedPost set later = true where id == s.id
136
+
} or {
137
+
eprintln('toggle_save_post: failed to save post for later (user_id: ${user_id}, post_id: ${post_id})')
138
+
return false
139
+
}
140
+
return true
141
+
}
142
+
} else {
143
+
post := SavedPost{
144
+
user_id: user_id
145
+
post_id: post_id
146
+
saved: false
147
+
later: true
148
+
}
149
+
sql app.db {
150
+
insert post into SavedPost
151
+
} or {
152
+
eprintln('toggle_save_post: failed to create saved post for later: ${post}')
153
+
return false
154
+
}
155
+
return true
156
+
}
157
+
}
+1
src/database/user.v
+1
src/database/user.v
···
210
210
// search_for_users searches for posts matching the given query.
211
211
// todo: query options/filters, such as created-after:<date>, created-before:<date>, etc
212
212
pub fn (app &DatabaseAccess) search_for_users(query string, limit int, offset int) []User {
213
+
//TODO: SANATIZE
213
214
sql_query := "\
214
215
SELECT *, CASE
215
216
WHEN username LIKE '%${query}%' THEN 1
+16
src/entity/saved_post.v
+16
src/entity/saved_post.v
···
1
+
module entity
2
+
3
+
// SavedPost represents a saved post for a given user
4
+
pub struct SavedPost {
5
+
pub mut:
6
+
id int @[primary; sql: serial]
7
+
post_id int
8
+
user_id int
9
+
saved bool
10
+
later bool
11
+
}
12
+
13
+
// can_remove returns true if the SavedPost is neither saved or saved for later.
14
+
pub fn (post &SavedPost) can_remove() bool {
15
+
return !post.saved && !post.later
16
+
}
+1
src/main.v
+1
src/main.v
+14
src/static/js/post.js
+14
src/static/js/post.js
···
11
11
})
12
12
window.location.reload()
13
13
}
14
+
15
+
const save = async id => {
16
+
await fetch('/api/post/save?id=' + id, {
17
+
method: 'GET'
18
+
})
19
+
window.location.reload()
20
+
}
21
+
22
+
const save_for_later = async id => {
23
+
await fetch('/api/post/save_for_later?id=' + id, {
24
+
method: 'GET'
25
+
})
26
+
window.location.reload()
27
+
}
+10
src/static/js/text_area_counter.js
+10
src/static/js/text_area_counter.js
···
1
+
// this script is used to provide character counters to textareas
2
+
3
+
const add_character_counter = (textarea_id, p_id, max_len) => {
4
+
const textarea = document.getElementById(textarea_id)
5
+
const p = document.getElementById(p_id)
6
+
textarea.addEventListener('input', () => {
7
+
p.innerText = textarea.value.length + '/' + max_len
8
+
})
9
+
p.innerText = textarea.value.length + '/' + max_len
10
+
}
+32
-2
src/templates/edit.html
+32
-2
src/templates/edit.html
···
1
1
@include 'partial/header.html'
2
2
3
-
<script src="/static/js/post.js"></script>
4
-
<script src="/static/js/render_body.js"></script>
3
+
<script src="/static/js/post.js" defer></script>
4
+
<script src="/static/js/render_body.js" defer></script>
5
+
<script src="/static/js/text_area_counter.js"></script>
5
6
6
7
<h1>edit post</h1>
7
8
···
18
19
hidden
19
20
aria-hidden
20
21
>
22
+
23
+
<p id="title_chars">0/@{app.config.post.title_max_len}</p>
21
24
<input
22
25
type="text"
23
26
name="title"
···
30
33
required
31
34
>
32
35
<br>
36
+
37
+
<p id="body_chars">0/@{app.config.post.body_max_len}</p>
33
38
<textarea
34
39
name="body"
35
40
id="body"
···
41
46
required
42
47
>@post.body</textarea>
43
48
<br>
49
+
44
50
<input type="submit" value="save">
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
+
</div>
58
+
59
+
<hr>
60
+
61
+
<div>
62
+
<h2>danger zone:</h2>
63
+
<form action="/api/post/delete" method="post">
64
+
<input
65
+
type="number"
66
+
name="id"
67
+
id="id"
68
+
placeholder="post id"
69
+
value="@post.id"
70
+
required aria-required
71
+
readonly aria-readonly
72
+
hidden aria-hidden
73
+
>
74
+
<input type="submit" value="delete">
45
75
</form>
46
76
</div>
47
77
+1
src/templates/index.html
+1
src/templates/index.html
+20
-22
src/templates/post.html
+20
-22
src/templates/post.html
···
21
21
<p><a href="/post/@{post.id}/reply">reply</a></p>
22
22
@end
23
23
24
-
@if ctx.is_logged_in() && post.author_id == user.id
25
-
<p><a href="/post/@{post.id}/edit">edit post</a></p>
26
-
@end
27
-
28
24
@if ctx.is_logged_in()
29
25
<br>
30
26
<div>
···
42
38
dislike
43
39
@end
44
40
</button>
41
+
<button onclick="save(@post.id)">
42
+
@if app.is_post_saved_by(user.id, post.id)
43
+
saved!
44
+
@else
45
+
save
46
+
@end
47
+
</button>
48
+
<button onclick="save_for_later(@post.id)">
49
+
@if app.is_post_saved_for_later_by(user.id, post.id)
50
+
saved for later!
51
+
@else
52
+
save for later
53
+
@end
54
+
</button>
45
55
</div>
46
56
@end
47
57
···
55
65
<h4>admin powers:</h4>
56
66
@end
57
67
58
-
<form action="/api/post/delete" method="post">
59
-
<input
60
-
type="number"
61
-
name="id"
62
-
id="id"
63
-
placeholder="post id"
64
-
value="@post.id"
65
-
required
66
-
readonly
67
-
hidden
68
-
aria-hidden
69
-
>
70
-
<input type="submit" value="delete">
71
-
</form>
68
+
@if post.author_id == user.id
69
+
<p><a href="/post/@{post.id}/edit">edit</a></p>
70
+
@end
72
71
73
72
@if user.admin
74
73
<form action="/api/post/pin" method="post">
···
78
77
id="id"
79
78
placeholder="post id"
80
79
value="@post.id"
81
-
required
82
-
readonly
83
-
hidden
84
-
aria-hidden
80
+
required aria-required
81
+
readonly aria-readonly
82
+
hidden aria-hidden
85
83
>
86
84
<input type="submit" value="pin">
87
85
</form>
+32
src/templates/saved_posts.html
+32
src/templates/saved_posts.html
···
1
+
@include 'partial/header.html'
2
+
3
+
@if ctx.is_logged_in()
4
+
5
+
<script src="/static/js/post.js"></script>
6
+
7
+
<p><a href="/me">back</a></p>
8
+
9
+
<h1>saved posts:</h1>
10
+
11
+
<div>
12
+
@if posts.len > 0
13
+
@for post in posts
14
+
<!-- components/post_mini.html -->
15
+
<div class="post post-mini">
16
+
<p>
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
+
<a href="/post/@post.id">@post.title</a>
19
+
<button onclick="save(@post.id)" style="display: inline-block;">unsave</button>
20
+
</p>
21
+
</div>
22
+
@end
23
+
@else
24
+
<p>none!</p>
25
+
@end
26
+
</div>
27
+
28
+
@else
29
+
<p>uh oh, you need to be logged in to see this page</p>
30
+
@end
31
+
32
+
@include 'partial/footer.html'
+32
src/templates/saved_posts_for_later.html
+32
src/templates/saved_posts_for_later.html
···
1
+
@include 'partial/header.html'
2
+
3
+
@if ctx.is_logged_in()
4
+
5
+
<script src="/static/js/post.js"></script>
6
+
7
+
<p><a href="/me">back</a></p>
8
+
9
+
<h1>saved posts for later:</h1>
10
+
11
+
<div>
12
+
@if posts.len > 0
13
+
@for post in posts
14
+
<!-- components/post_mini.html -->
15
+
<div class="post post-mini">
16
+
<p>
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
+
<a href="/post/@post.id">@post.title</a>
19
+
<button onclick="save_for_later(@post.id)" style="display: inline-block;">unsave</button>
20
+
</p>
21
+
</div>
22
+
@end
23
+
@else
24
+
<p>none!</p>
25
+
@end
26
+
</div>
27
+
28
+
@else
29
+
<p>uh oh, you need to be logged in to see this page</p>
30
+
@end
31
+
32
+
@include 'partial/footer.html'
+11
-3
src/templates/settings.html
+11
-3
src/templates/settings.html
···
1
1
@include 'partial/header.html'
2
2
3
3
@if ctx.is_logged_in()
4
+
<script src="/static/js/text_area_counter.js"></script>
5
+
4
6
<h1>user settings:</h1>
5
7
6
8
<form action="/api/user/set_bio" method="post">
7
-
<label for="bio">bio:</label>
9
+
<label for="bio">bio: (<span id="bio_chars">0/@{app.config.user.bio_max_len}</span>)</label>
8
10
<br>
9
11
<textarea
10
12
name="bio"
···
22
24
<hr>
23
25
24
26
<form action="/api/user/set_pronouns" method="post">
25
-
<label for="pronouns">pronouns:</label>
27
+
<label for="pronouns">pronouns: (<span id="pronouns_chars">0/@{app.config.user.pronouns_max_len}</span>)</label>
26
28
<input
27
29
type="text"
28
30
name="pronouns"
···
39
41
<hr>
40
42
41
43
<form action="/api/user/set_nickname" method="post">
42
-
<label for="nickname">nickname:</label>
44
+
<label for="nickname">nickname: (<span id="nickname_chars">0/@{app.config.user.nickname_max_len}</span>)</label>
43
45
<input
44
46
type="text"
45
47
name="nickname"
···
56
58
<form action="/api/user/set_nickname" method="post">
57
59
<input type="submit" value="reset nickname">
58
60
</form>
61
+
62
+
<script>
63
+
add_character_counter('bio', 'bio_chars', @{app.config.user.bio_max_len})
64
+
add_character_counter('pronouns', 'pronouns_chars', @{app.config.user.pronouns_max_len})
65
+
add_character_counter('nickname', 'nickname_chars', @{app.config.user.nickname_max_len})
66
+
</script>
59
67
60
68
@if app.config.instance.allow_changing_theme
61
69
<hr>
+28
-1
src/templates/user.html
+28
-1
src/templates/user.html
···
18
18
</h1>
19
19
20
20
@if app.logged_in_as(mut ctx, viewing.id)
21
+
<script src="/static/js/text_area_counter.js"></script>
22
+
21
23
<p>this is you!</p>
22
24
23
25
<div>
24
26
<form action="/api/post/new_post" method="post">
25
27
<h2>new post:</h2>
26
28
29
+
<p id="title_chars">0/@{app.config.post.title_max_len}</p>
27
30
<input
28
31
type="text"
29
32
name="title"
···
33
36
pattern="@app.config.post.title_pattern"
34
37
placeholder="title"
35
38
required aria-required
39
+
autocomplete="off" aria-autocomplete="off"
36
40
>
37
41
<br>
38
42
43
+
<p id="body_chars">0/@{app.config.post.body_max_len}</p>
39
44
<textarea
40
45
name="body"
41
46
id="body"
···
45
50
cols="30"
46
51
placeholder="body"
47
52
required aria-required
53
+
autocomplete="off" aria-autocomplete="off"
48
54
></textarea>
49
55
<br>
50
56
51
57
<input type="submit" value="post!">
52
58
</form>
59
+
60
+
<script>
61
+
add_character_counter('title', 'title_chars', @{app.config.post.title_max_len})
62
+
add_character_counter('body', 'body_chars', @{app.config.post.body_max_len})
63
+
</script>
53
64
</div>
65
+
<hr>
54
66
@end
55
67
56
68
@if viewing.bio != ''
···
58
70
<h2>bio:</h2>
59
71
<pre id="bio">@viewing.bio</pre>
60
72
</div>
73
+
<hr>
74
+
@end
75
+
76
+
@if app.logged_in_as(mut ctx, viewing.id)
77
+
<div>
78
+
<p><a href="/me/saved">saved posts</a></p>
79
+
<p><a href="/me/saved_for_later">saved for later</a></p>
80
+
</div>
81
+
<hr>
61
82
@end
62
83
63
84
<div>
64
85
<h2>recent posts:</h2>
65
-
@for post in app.get_posts_from_user(viewing.id, 10)
86
+
@if posts.len > 0
87
+
@for post in posts
66
88
@include 'components/post_small.html'
67
89
@end
90
+
@else
91
+
<p>no posts!</p>
92
+
@end
68
93
</div>
69
94
70
95
@if ctx.is_logged_in() && user.admin
96
+
<hr>
97
+
71
98
<div>
72
99
<h2>admin powers:</h2>
73
100
<form action="/api/user/set_muted" method="post">
+30
src/webapp/api.v
+30
src/webapp/api.v
···
565
565
}
566
566
}
567
567
568
+
@['/api/post/save']
569
+
fn (mut app App) api_post_save(mut ctx Context, id int) veb.Result {
570
+
user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
571
+
572
+
if app.get_post_by_id(id) != none {
573
+
if app.toggle_save_post(user.id, id) {
574
+
return ctx.text('toggled save')
575
+
} else {
576
+
return ctx.server_error('failed to save post')
577
+
}
578
+
} else {
579
+
return ctx.server_error('post does not exist')
580
+
}
581
+
}
582
+
583
+
@['/api/post/save_for_later']
584
+
fn (mut app App) api_post_save_for_later(mut ctx Context, id int) veb.Result {
585
+
user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
586
+
587
+
if app.get_post_by_id(id) != none {
588
+
if app.toggle_save_for_later_post(user.id, id) {
589
+
return ctx.text('toggled save')
590
+
} else {
591
+
return ctx.server_error('failed to save post')
592
+
}
593
+
} else {
594
+
return ctx.server_error('post does not exist')
595
+
}
596
+
}
597
+
568
598
@['/api/post/get_title']
569
599
fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result {
570
600
post := app.get_post_by_id(id) or { return ctx.server_error('no such post') }
+23
src/webapp/pages.v
+23
src/webapp/pages.v
···
33
33
return ctx.redirect('/user/${user.username}')
34
34
}
35
35
36
+
@['/me/saved']
37
+
fn (mut app App) me_saved(mut ctx Context) veb.Result {
38
+
user := app.whoami(mut ctx) or {
39
+
ctx.error('not logged in')
40
+
return ctx.redirect('/login')
41
+
}
42
+
ctx.title = '${app.config.instance.name} - saved posts'
43
+
posts := app.get_saved_posts_as_post_for(user.id)
44
+
return $veb.html('../templates/saved_posts.html')
45
+
}
46
+
47
+
@['/me/saved_for_later']
48
+
fn (mut app App) me_saved_for_later(mut ctx Context) veb.Result {
49
+
user := app.whoami(mut ctx) or {
50
+
ctx.error('not logged in')
51
+
return ctx.redirect('/login')
52
+
}
53
+
ctx.title = '${app.config.instance.name} - posts saved for later'
54
+
posts := app.get_saved_for_later_posts_as_post_for(user.id)
55
+
return $veb.html('../templates/saved_posts_for_later.html')
56
+
}
57
+
36
58
fn (mut app App) settings(mut ctx Context) veb.Result {
37
59
user := app.whoami(mut ctx) or {
38
60
ctx.error('not logged in')
···
75
97
return ctx.redirect('/')
76
98
}
77
99
ctx.title = '${app.config.instance.name} - ${user.get_name()}'
100
+
posts := app.get_posts_from_user(viewing.id, 10)
78
101
return $veb.html('../templates/user.html')
79
102
}
80
103