Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/shared/Jordnaer.Shared/FeatureFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public static class FeatureFlags
public const string AccountSettings = "AccountSettings";
public const string NotificationSettings = "NotificationSettings";
public const string MapSearch = "MapSearch";
public const string WysiwygEditor = "WysiwygEditor";
}
7 changes: 7 additions & 0 deletions src/shared/Jordnaer.Shared/Posts/PostSearchResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@ public class PostSearchResult
{
public List<PostDto> Posts { get; set; } = [];
public int TotalCount { get; set; }
public string? NextCursor { get; set; }

/// <summary>
/// Indicates whether there are more results available.
/// This is a computed property based on NextCursor.
/// </summary>
public bool HasMore => NextCursor is not null;
}
6 changes: 3 additions & 3 deletions src/web/Jordnaer/Features/Chat/ChatMessageList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</MudStack>
return;
case not null when IsMobile:
<MudAppBar Dense ToolBarClass="justify-space-between h-100" Color="Color.Primary" Elevation="5">
<MudAppBar Dense ToolBarClass="justify-space-between h-100" Color="Color.Info" Elevation="5">

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

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

</MudList>

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

Expand Down
7 changes: 3 additions & 4 deletions src/web/Jordnaer/Features/GroupPosts/GroupPostForm.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
@inject ISnackbar Snackbar

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

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

private Jordnaer.Features.WYSIWYG.TextEditorComponent _textEditor = null!;
private TextEditorComponent _textEditor = null!;
private string _postText = string.Empty;
private bool _isSending = false;
private UserProfile? _userProfile;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,77 @@
using Jordnaer.Database;
using Jordnaer.Shared;
using Microsoft.EntityFrameworkCore;
using OneOf;
using OneOf.Types;
namespace Jordnaer.Features.GroupPosts;
public class GroupPostService(IDbContextFactory<JordnaerDbContext> contextFactory)
{
public async Task<OneOf<GroupPostDto, NotFound>> GetPostAsync(Guid postId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var post = await context.GroupPosts
.AsNoTracking()
.Where(x => x.Id == postId)
.Select(x => x.ToGroupPostDto())
.FirstOrDefaultAsync(cancellationToken);
return post is null
? new NotFound()
: post;
}
public async Task<List<GroupPostDto>> GetPostsAsync(Guid groupId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var posts = await context.GroupPosts
.AsNoTracking()
.Where(x => x.GroupId == groupId)
.OrderByDescending(x => x.CreatedUtc)
.Include(x => x.UserProfile)
.ToListAsync(cancellationToken);
return posts.Select(x => x.ToGroupPostDto()).ToList();
}
public async Task<OneOf<Success, Error<string>>> CreatePostAsync(GroupPost post,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.GroupPosts
.AsNoTracking()
.AnyAsync(x => x.Id == post.Id,
cancellationToken))
{
return new Error<string>("Opslaget eksisterer allerede.");
}
context.GroupPosts.Add(post);
await context.SaveChangesAsync(cancellationToken);
return new Success();
}
public async Task<OneOf<Success, Error<string>>> DeletePostAsync(Guid postId, string userId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsDeleted = await context.GroupPosts
.Where(x => x.Id == postId && x.UserProfileId == userId)
.ExecuteDeleteAsync(cancellationToken);
return rowsDeleted > 0
? new Success()
: new Error<string>("Opslaget blev ikke fundet eller du har ikke rettigheder til at slette det.");
}
}
using Jordnaer.Database;
using Jordnaer.Shared;
using Microsoft.EntityFrameworkCore;
using OneOf;
using OneOf.Types;

namespace Jordnaer.Features.GroupPosts;



public class GroupPostService(IDbContextFactory<JordnaerDbContext> contextFactory)
{
public async Task<OneOf<GroupPostDto, NotFound>> GetPostAsync(Guid postId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

var post = await context.GroupPosts
.AsNoTracking()
.Where(x => x.Id == postId)
.Select(x => x.ToGroupPostDto())
.FirstOrDefaultAsync(cancellationToken);

return post is null
? new NotFound()
: post;
}

public async Task<List<GroupPostDto>> GetPostsAsync(Guid groupId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

var posts = await context.GroupPosts
.AsNoTracking()
.Where(x => x.GroupId == groupId)
.OrderByDescending(x => x.CreatedUtc)
.Include(x => x.UserProfile)
.ToListAsync(cancellationToken);

return posts.Select(x => x.ToGroupPostDto()).ToList();
}

public async Task<OneOf<Success, Error<string>>> CreatePostAsync(GroupPost post,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

if (await context.GroupPosts
.AsNoTracking()
.AnyAsync(x => x.Id == post.Id,
cancellationToken))
{
return new Error<string>("Opslaget eksisterer allerede.");
}

context.GroupPosts.Add(post);

await context.SaveChangesAsync(cancellationToken);

return new Success();
}

public async Task<OneOf<Success, Error<string>>> DeletePostAsync(Guid postId, string userId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

var rowsDeleted = await context.GroupPosts
.Where(x => x.Id == postId && x.UserProfileId == userId)
.ExecuteDeleteAsync(cancellationToken);

return rowsDeleted > 0
? new Success()
: new Error<string>("Opslaget blev ikke fundet eller du har ikke rettigheder til at slette det.");
}
}
4 changes: 3 additions & 1 deletion src/web/Jordnaer/Features/Map/MapSearchFilter.razor
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@using JetBrains.Annotations
@inject IDataForsyningenClient DataForsyningenClient
@inject ILocationService LocationService
@inject IJSRuntime JsRuntime
Expand Down Expand Up @@ -128,6 +129,7 @@
}
}

[UsedImplicitly]
private class GeolocationPosition
{
public double Latitude { get; set; }
Expand All @@ -146,7 +148,7 @@

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

return searchResponse.IsSuccessful && searchResponse.Content is not null
return searchResponse is { IsSuccessful: true, Content: not null }
? searchResponse.Content.Select(response => response.ToString())
: [];
}
Expand Down
54 changes: 54 additions & 0 deletions src/web/Jordnaer/Features/PostSearch/PostSearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,60 @@ internal static IQueryable<Post> ApplyContentFilter(string? filter, IQueryable<P
return posts;
}

public async Task<OneOf<PostSearchResult, Error<string>>> GetRecentPostsAsync(
int pageSize = 10,
string? cursor = null,
CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

var query = context.Posts
.AsNoTracking()
.AsQueryable();

// Apply cursor filter if provided
if (!string.IsNullOrEmpty(cursor) && DateTimeOffset.TryParse(cursor, out var cursorDate))
{
query = query.Where(x => x.CreatedUtc < cursorDate);
}

// Fetch one extra to determine if there are more results
var posts = await query
.Include(x => x.UserProfile)
.Include(x => x.Categories)
.OrderByDescending(x => x.CreatedUtc)
.Take(pageSize + 1)
.Select(x => x.ToPostDto())
.ToListAsync(cancellationToken);

var hasMore = posts.Count > pageSize;
string? nextCursor = null;

if (hasMore)
{
// Remove the extra post
posts.RemoveAt(posts.Count - 1);
// Set cursor to the last post's CreatedUtc
nextCursor = posts[^1].CreatedUtc.ToString("O");
}

return new PostSearchResult
{
Posts = posts,
TotalCount = 0, // Not meaningful for cursor-based pagination
NextCursor = nextCursor // HasMore is computed from NextCursor
};
}
catch (Exception exception)
{
logger.LogError(exception, "Exception occurred while loading recent posts. " +
"Cursor: {Cursor}, PageSize: {PageSize}", cursor, pageSize);
return new Error<string>(ErrorMessages.Something_Went_Wrong_Try_Again);
}
}

private static ReadOnlySpan<KeyValuePair<string, object?>> MakeTagList(PostSearchFilter filter)
{
return new KeyValuePair<string, object?>[]
Expand Down
Loading