Skip to content

Commit e36c792

Browse files
Merge pull request #477 from NielsPilgaard/feature/posts
Feature/posts
2 parents a1e4f69 + a2acc8f commit e36c792

30 files changed

Lines changed: 3173 additions & 457 deletions

src/shared/Jordnaer.Shared/FeatureFlags.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ public static class FeatureFlags
88
public const string AccountSettings = "AccountSettings";
99
public const string NotificationSettings = "NotificationSettings";
1010
public const string MapSearch = "MapSearch";
11+
public const string WysiwygEditor = "WysiwygEditor";
1112
}

src/shared/Jordnaer.Shared/Posts/PostSearchResult.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@ public class PostSearchResult
44
{
55
public List<PostDto> Posts { get; set; } = [];
66
public int TotalCount { get; set; }
7+
public string? NextCursor { get; set; }
8+
9+
/// <summary>
10+
/// Indicates whether there are more results available.
11+
/// This is a computed property based on NextCursor.
12+
/// </summary>
13+
public bool HasMore => NextCursor is not null;
714
}

src/web/Jordnaer/Features/Chat/ChatMessageList.razor

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</MudStack>
2828
return;
2929
case not null when IsMobile:
30-
<MudAppBar Dense ToolBarClass="justify-space-between h-100" Color="Color.Primary" Elevation="5">
30+
<MudAppBar Dense ToolBarClass="justify-space-between h-100" Color="Color.Info" Elevation="5">
3131

3232
<MudIconButton Icon="@Icons.Material.Filled.ArrowBack" OnClick="BackToList" Color="Color.Info"
3333
Variant="Variant.Filled" title="Tilbage" />
@@ -85,14 +85,14 @@
8585

8686
<MudTextField id="chat-message-input" @ref="_messageInput" T="string" FullWidth Immediate Clearable AutoFocus
8787
Adornment="Adornment.End" OnKeyDown="SendMessageOnEnter" AdornmentIcon="@Icons.Material.Filled.Send"
88-
AdornmentColor="@(string.IsNullOrWhiteSpace(_messageInput.Value) ? Color.Default : Color.Primary)"
88+
AdornmentColor="@(string.IsNullOrWhiteSpace(_messageInput.Value) ? Color.Default : Color.Info)"
8989
OnAdornmentClick="SendMessage" Class="mt-3 p-1" Lines="2" IconSize="Size.Large"
9090
Style="@($"background: {JordnaerTheme.CustomTheme.PaletteLight.BackgroundGray}")" />
9191

9292
</MudList>
9393

9494
<ScrollToBottom Selector="@ChatMessageWindowClass" OnClick="async () => await _messageInput.FocusAsync()">
95-
<MudFab Size="Size.Large" Color="Color.Primary" StartIcon="@Icons.Material.Filled.ArrowCircleDown" />
95+
<MudFab Size="Size.Large" Color="Color.Info" StartIcon="@Icons.Material.Filled.ArrowCircleDown" />
9696
</ScrollToBottom>
9797
</MudItem>
9898

src/web/Jordnaer/Features/GroupPosts/GroupPostForm.razor

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
@inject ISnackbar Snackbar
77

88
<MudPaper Class="pa-4" Elevation="3">
9-
<Jordnaer.Features.WYSIWYG.TextEditorComponent @ref="_textEditor" Label="Skriv et opslag"
10-
Placeholder="Del noget med gruppen..." />
9+
<TextEditorComponent @ref="_textEditor" Label="Skriv et opslag" Placeholder="Del noget med gruppen..." />
1110

1211
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
1312
<MudSpacer />
14-
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="CreatePostAsync" Disabled="@_isSending">
13+
<MudButton Variant="Variant.Filled" Color="Color.Info" OnClick="CreatePostAsync" Disabled="@_isSending">
1514
@if (_isSending)
1615
{
1716
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" />
@@ -32,7 +31,7 @@
3231
[Parameter]
3332
public EventCallback OnPostCreated { get; set; }
3433

35-
private Jordnaer.Features.WYSIWYG.TextEditorComponent _textEditor = null!;
34+
private TextEditorComponent _textEditor = null!;
3635
private string _postText = string.Empty;
3736
private bool _isSending = false;
3837
private UserProfile? _userProfile;

src/web/Jordnaer/Features/GroupPosts/PostService.cs renamed to src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs

Lines changed: 77 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,77 @@
1-
using Jordnaer.Database;
2-
using Jordnaer.Shared;
3-
using Microsoft.EntityFrameworkCore;
4-
using OneOf;
5-
using OneOf.Types;
6-
7-
namespace Jordnaer.Features.GroupPosts;
8-
9-
10-
11-
public class GroupPostService(IDbContextFactory<JordnaerDbContext> contextFactory)
12-
{
13-
public async Task<OneOf<GroupPostDto, NotFound>> GetPostAsync(Guid postId,
14-
CancellationToken cancellationToken = default)
15-
{
16-
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
17-
18-
var post = await context.GroupPosts
19-
.AsNoTracking()
20-
.Where(x => x.Id == postId)
21-
.Select(x => x.ToGroupPostDto())
22-
.FirstOrDefaultAsync(cancellationToken);
23-
24-
return post is null
25-
? new NotFound()
26-
: post;
27-
}
28-
29-
public async Task<List<GroupPostDto>> GetPostsAsync(Guid groupId,
30-
CancellationToken cancellationToken = default)
31-
{
32-
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
33-
34-
var posts = await context.GroupPosts
35-
.AsNoTracking()
36-
.Where(x => x.GroupId == groupId)
37-
.OrderByDescending(x => x.CreatedUtc)
38-
.Include(x => x.UserProfile)
39-
.ToListAsync(cancellationToken);
40-
41-
return posts.Select(x => x.ToGroupPostDto()).ToList();
42-
}
43-
44-
public async Task<OneOf<Success, Error<string>>> CreatePostAsync(GroupPost post,
45-
CancellationToken cancellationToken = default)
46-
{
47-
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
48-
49-
if (await context.GroupPosts
50-
.AsNoTracking()
51-
.AnyAsync(x => x.Id == post.Id,
52-
cancellationToken))
53-
{
54-
return new Error<string>("Opslaget eksisterer allerede.");
55-
}
56-
57-
context.GroupPosts.Add(post);
58-
59-
await context.SaveChangesAsync(cancellationToken);
60-
61-
return new Success();
62-
}
63-
64-
public async Task<OneOf<Success, Error<string>>> DeletePostAsync(Guid postId, string userId,
65-
CancellationToken cancellationToken = default)
66-
{
67-
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
68-
69-
var rowsDeleted = await context.GroupPosts
70-
.Where(x => x.Id == postId && x.UserProfileId == userId)
71-
.ExecuteDeleteAsync(cancellationToken);
72-
73-
return rowsDeleted > 0
74-
? new Success()
75-
: new Error<string>("Opslaget blev ikke fundet eller du har ikke rettigheder til at slette det.");
76-
}
77-
}
1+
using Jordnaer.Database;
2+
using Jordnaer.Shared;
3+
using Microsoft.EntityFrameworkCore;
4+
using OneOf;
5+
using OneOf.Types;
6+
7+
namespace Jordnaer.Features.GroupPosts;
8+
9+
10+
11+
public class GroupPostService(IDbContextFactory<JordnaerDbContext> contextFactory)
12+
{
13+
public async Task<OneOf<GroupPostDto, NotFound>> GetPostAsync(Guid postId,
14+
CancellationToken cancellationToken = default)
15+
{
16+
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
17+
18+
var post = await context.GroupPosts
19+
.AsNoTracking()
20+
.Where(x => x.Id == postId)
21+
.Select(x => x.ToGroupPostDto())
22+
.FirstOrDefaultAsync(cancellationToken);
23+
24+
return post is null
25+
? new NotFound()
26+
: post;
27+
}
28+
29+
public async Task<List<GroupPostDto>> GetPostsAsync(Guid groupId,
30+
CancellationToken cancellationToken = default)
31+
{
32+
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
33+
34+
var posts = await context.GroupPosts
35+
.AsNoTracking()
36+
.Where(x => x.GroupId == groupId)
37+
.OrderByDescending(x => x.CreatedUtc)
38+
.Include(x => x.UserProfile)
39+
.ToListAsync(cancellationToken);
40+
41+
return posts.Select(x => x.ToGroupPostDto()).ToList();
42+
}
43+
44+
public async Task<OneOf<Success, Error<string>>> CreatePostAsync(GroupPost post,
45+
CancellationToken cancellationToken = default)
46+
{
47+
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
48+
49+
if (await context.GroupPosts
50+
.AsNoTracking()
51+
.AnyAsync(x => x.Id == post.Id,
52+
cancellationToken))
53+
{
54+
return new Error<string>("Opslaget eksisterer allerede.");
55+
}
56+
57+
context.GroupPosts.Add(post);
58+
59+
await context.SaveChangesAsync(cancellationToken);
60+
61+
return new Success();
62+
}
63+
64+
public async Task<OneOf<Success, Error<string>>> DeletePostAsync(Guid postId, string userId,
65+
CancellationToken cancellationToken = default)
66+
{
67+
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
68+
69+
var rowsDeleted = await context.GroupPosts
70+
.Where(x => x.Id == postId && x.UserProfileId == userId)
71+
.ExecuteDeleteAsync(cancellationToken);
72+
73+
return rowsDeleted > 0
74+
? new Success()
75+
: new Error<string>("Opslaget blev ikke fundet eller du har ikke rettigheder til at slette det.");
76+
}
77+
}

src/web/Jordnaer/Features/Map/MapSearchFilter.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@using JetBrains.Annotations
12
@inject IDataForsyningenClient DataForsyningenClient
23
@inject ILocationService LocationService
34
@inject IJSRuntime JsRuntime
@@ -128,6 +129,7 @@
128129
}
129130
}
130131

132+
[UsedImplicitly]
131133
private class GeolocationPosition
132134
{
133135
public double Latitude { get; set; }
@@ -146,7 +148,7 @@
146148

147149
var searchResponse = await DataForsyningenClient.GetAddressesWithAutoComplete(value, cancellationToken);
148150

149-
return searchResponse.IsSuccessful && searchResponse.Content is not null
151+
return searchResponse is { IsSuccessful: true, Content: not null }
150152
? searchResponse.Content.Select(response => response.ToString())
151153
: [];
152154
}

src/web/Jordnaer/Features/PostSearch/PostSearchService.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,60 @@ internal static IQueryable<Post> ApplyContentFilter(string? filter, IQueryable<P
128128
return posts;
129129
}
130130

131+
public async Task<OneOf<PostSearchResult, Error<string>>> GetRecentPostsAsync(
132+
int pageSize = 10,
133+
string? cursor = null,
134+
CancellationToken cancellationToken = default)
135+
{
136+
try
137+
{
138+
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
139+
140+
var query = context.Posts
141+
.AsNoTracking()
142+
.AsQueryable();
143+
144+
// Apply cursor filter if provided
145+
if (!string.IsNullOrEmpty(cursor) && DateTimeOffset.TryParse(cursor, out var cursorDate))
146+
{
147+
query = query.Where(x => x.CreatedUtc < cursorDate);
148+
}
149+
150+
// Fetch one extra to determine if there are more results
151+
var posts = await query
152+
.Include(x => x.UserProfile)
153+
.Include(x => x.Categories)
154+
.OrderByDescending(x => x.CreatedUtc)
155+
.Take(pageSize + 1)
156+
.Select(x => x.ToPostDto())
157+
.ToListAsync(cancellationToken);
158+
159+
var hasMore = posts.Count > pageSize;
160+
string? nextCursor = null;
161+
162+
if (hasMore)
163+
{
164+
// Remove the extra post
165+
posts.RemoveAt(posts.Count - 1);
166+
// Set cursor to the last post's CreatedUtc
167+
nextCursor = posts[^1].CreatedUtc.ToString("O");
168+
}
169+
170+
return new PostSearchResult
171+
{
172+
Posts = posts,
173+
TotalCount = 0, // Not meaningful for cursor-based pagination
174+
NextCursor = nextCursor // HasMore is computed from NextCursor
175+
};
176+
}
177+
catch (Exception exception)
178+
{
179+
logger.LogError(exception, "Exception occurred while loading recent posts. " +
180+
"Cursor: {Cursor}, PageSize: {PageSize}", cursor, pageSize);
181+
return new Error<string>(ErrorMessages.Something_Went_Wrong_Try_Again);
182+
}
183+
}
184+
131185
private static ReadOnlySpan<KeyValuePair<string, object?>> MakeTagList(PostSearchFilter filter)
132186
{
133187
return new KeyValuePair<string, object?>[]

0 commit comments

Comments
 (0)