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
22 changes: 15 additions & 7 deletions AffirmationGenerator.Client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ function App() {
.then((languages) => Object.entries(languages).map(([code, label]) => ({ code, label }))),
});

const { data: remainingCount } = useQuery({
const { data: remainingData } = useQuery({
queryKey: ["remainingAffirmations"],
queryFn: () =>
axios
.get<RemainingAffirmationsResponse>("/affirmations/remaining")
.then((response) => response.data.remainingCount),
.then((response) => response.data),
});

const remainingAffirmations = remainingCount ?? 0;
const remainingAffirmations = remainingData?.remainingCount ?? 0;
const resetInSeconds = remainingData?.resetInSeconds ?? 0;

const maxAffirmationsMessage =
"Achieved maximum amount of affirmations per day. Come back tomorrow for more affirmations! 😁";
Expand All @@ -64,11 +65,18 @@ function App() {
axios
.get<AffirmationResponse>("/affirmations", { params: { targetLanguage: targetLanguage } })
.then((response) => response.data)
.then((data) => {
.then(async (data) => {
setAffirmationText(data.text);
setDisplayedText("");
queryClient.setQueryData(["remainingAffirmations"], data.remainingCount);
if (data.remainingCount === 0) setErrorMessage(maxAffirmationsMessage);
if (data.remainingCount === 0) {
await queryClient.invalidateQueries({ queryKey: ["remainingAffirmations"] });
setErrorMessage(maxAffirmationsMessage);
} else {
queryClient.setQueryData(["remainingAffirmations"], {
remainingCount: data.remainingCount,
resetInSeconds: 0,
} as RemainingAffirmationsResponse);
}
setIsShaking(true);
setTimeout(() => setIsShaking(false), 500);
})
Expand All @@ -90,7 +98,7 @@ function App() {
<div className="animated-bg min-h-screen flex flex-col items-center justify-between p-4 font-sans text-gray-800">
<MainCard isShaking={isShaking} error={<ErrorMessage message={displayedErrorMessage} />}>
<MainText text={displayedText} isLoading={isFetching} />
<RemainingItemsText count={remainingAffirmations} />
<RemainingItemsText count={remainingAffirmations} resetInSeconds={resetInSeconds} />
<LanguagesDropdown
value={selectedLanguageCode}
onChange={handleLanguageChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,79 @@
import { useEffect, useRef, useState } from "react";

type RemainingAffirmationsTextProps = {
count: number;
resetInSeconds: number;
};

function RemainingItemsText({ count }: RemainingAffirmationsTextProps) {
function getCountColor(count: number) {
if (count >= 4) return "text-green-600";
if (count >= 2) return "text-yellow-500";
function formatTime(totalSeconds: number) {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;

return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}

function getCountColor(count: number) {
if (count >= 4) return "text-green-600";
if (count >= 2) return "text-yellow-500";

return "text-red-600";
}

function RemainingItemsText({ count, resetInSeconds }: RemainingAffirmationsTextProps) {
const [secondsLeft, setSecondsLeft] = useState<number | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

useEffect(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}

if (count > 0 || resetInSeconds == 0) return;

const targetTime = Date.now() + Math.max(0, Math.ceil(resetInSeconds)) * 1000;

const updateTimer = () => {
const remaining = Math.max(0, Math.ceil((targetTime - Date.now()) / 1000));

setSecondsLeft(remaining);

if (remaining <= 0 && intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};

updateTimer();

intervalRef.current = setInterval(updateTimer, 1000);

return "text-red-600";
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [count, resetInSeconds]);

return (
<div className="flex items-center justify-center md:w-60 w-68 h-12 rounded-lg glass border border-white/20 md:text-lg text-xl text-black font-medium absolute bottom-20 left-1/2 -translate-x-1/2 md:bottom-8 md:left-8 md:translate-x-0">
Remaining affirmations:{" "}
<span className={`ml-1 md:text-lg text-xl font-semibold ${getCountColor(count)}`}>
{count}
</span>
{count === 0 && secondsLeft != null && secondsLeft > 0 ? (
<>
Resets in:{" "}
<span className="ml-1 md:text-lg text-xl font-semibold text-red-600">
{formatTime(secondsLeft)}
</span>
</>
) : (
<>
Remaining affirmations:{" "}
<span className={`ml-1 md:text-lg text-xl font-semibold ${getCountColor(count)}`}>
{count}
</span>
</>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
type RemainingAffirmationsResponse = {
remainingCount: number;
resetInSeconds: number;
};

export default RemainingAffirmationsResponse;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AffirmationGenerator.Server.Application.Queries;
using AffirmationGenerator.Server.Domain;
using AffirmationGenerator.Server.Tests.Extensions;
using NUnit.Framework;
using Shouldly;
Expand All @@ -23,9 +24,9 @@ public void Handle_ShouldReturnSortedLanguages()
var result = _query.Handle();

// Assert
var languages = result.ShouldBeSuccess().Languages;
var response = result.ShouldBeSuccess();

languages.Count.ShouldBeGreaterThan(0);
languages.ShouldBeInOrder(SortDirection.Ascending);
response.Languages.Count.ShouldBe(Enum.GetValues<AffirmationLanguage>().Length);
response.Languages.ShouldBeInOrder(SortDirection.Ascending);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using AffirmationGenerator.Server.Application.Models;
using AffirmationGenerator.Server.Application.Queries;
using AffirmationGenerator.Server.Application.Services.Affirmation;
using AffirmationGenerator.Server.Application.Services.Language;
using AffirmationGenerator.Server.Core;
using AffirmationGenerator.Server.Application.Services.Mapping;
using AffirmationGenerator.Server.Domain;
using AffirmationGenerator.Server.Infrastructure.DeepL;
using AffirmationGenerator.Server.Tests.Extensions;
Expand All @@ -17,31 +16,32 @@ namespace AffirmationGenerator.Server.Tests.Application.Queries;
public sealed class GetAffirmationQueryTests : TestBase
{
private IDeepLTranslatorClient _translatorClient = null!;
private ILanguageCodeMapper<AffirmationLanguage> _languageCodeMapper = null!;
private IMapper<AffirmationLanguage, string> _affirmationLanguageMapper = null!;
private IAffirmationService _affirmationService = null!;
private GetAffirmationQuery _query = null!;

private const int MaxRequestsPerDay = 10;

[SetUp]
public void SetUp()
{
_translatorClient = Substitute.For<IDeepLTranslatorClient>();
_languageCodeMapper = Substitute.For<ILanguageCodeMapper<AffirmationLanguage>>();
_affirmationLanguageMapper = Substitute.For<IMapper<AffirmationLanguage, string>>();
_affirmationService = Substitute.For<IAffirmationService>();
_query = new GetAffirmationQuery(_translatorClient, _affirmationService, _languageCodeMapper);
_query = new GetAffirmationQuery(_translatorClient, _affirmationService, _affirmationLanguageMapper);
}

[Test]
public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmation()
{
// Arrange
const string affirmationText = "Good day!";
const int maxRequestsPerDay = 10;

_affirmationService.GetAffirmation().Returns(affirmationText);
_affirmationService.Get().ReturnsSuccess(affirmationText);

var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.English };

_languageCodeMapper.Map(getAffirmationRequest.TargetLanguage).Returns(LanguageCode.English);
_affirmationLanguageMapper.Map(getAffirmationRequest.TargetLanguage).ReturnsSuccess(LanguageCode.English);

// Act
var result = await _query.Handle(getAffirmationRequest);
Expand All @@ -51,10 +51,10 @@ public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmati

response.TargetLanguage.ShouldBe(AffirmationLanguage.English);
response.Text.ShouldBe(affirmationText);
response.RemainingCount.ShouldBeLessThan(maxRequestsPerDay);
response.RemainingCount.ShouldBeLessThan(MaxRequestsPerDay);

await _affirmationService.Received(1).GetAffirmation();
_languageCodeMapper.Received(1).Map(AffirmationLanguage.English);
await _affirmationService.Received(1).Get();
_affirmationLanguageMapper.Received(1).Map(AffirmationLanguage.English);
await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}

Expand All @@ -64,13 +64,12 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI
// Arrange
const string affirmationText = "Good day!";
const string affirmationTextInGerman = "Guten Tag!";
const int maxRequestsPerDay = 10;

_affirmationService.GetAffirmation().Returns(affirmationText);
_affirmationService.Get().ReturnsSuccess(affirmationText);

var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German };

_languageCodeMapper.Map(getAffirmationRequest.TargetLanguage).Returns(LanguageCode.German);
_affirmationLanguageMapper.Map(getAffirmationRequest.TargetLanguage).ReturnsSuccess(LanguageCode.German);
_translatorClient.Translate(affirmationText, LanguageCode.English, LanguageCode.German).Returns(affirmationTextInGerman);

// Act
Expand All @@ -81,18 +80,18 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI

response.TargetLanguage.ShouldBe(AffirmationLanguage.German);
response.Text.ShouldBe(affirmationTextInGerman);
response.RemainingCount.ShouldBeLessThan(maxRequestsPerDay);
response.RemainingCount.ShouldBeLessThan(MaxRequestsPerDay);

await _affirmationService.Received(1).GetAffirmation();
_languageCodeMapper.Received(1).Map(AffirmationLanguage.German);
await _affirmationService.Received(1).Get();
_affirmationLanguageMapper.Received(1).Map(AffirmationLanguage.German);
await _translatorClient.Received(1).Translate(affirmationText, LanguageCode.English, LanguageCode.German);
}

[Test]
public async Task Handle_WhenNoAffirmation_ShouldReturnError()
{
// Arrange
_affirmationService.GetAffirmation().Returns(Result<string>.Error(new AffirmationNotFound()));
_affirmationService.Get().ReturnsError<string, AffirmationNotFound>();

var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German };

Expand All @@ -102,8 +101,8 @@ public async Task Handle_WhenNoAffirmation_ShouldReturnError()
// Assert
result.ShouldBeError().ShouldBeOfType<AffirmationNotFound>();

await _affirmationService.Received(1).GetAffirmation();
_languageCodeMapper.DidNotReceiveWithAnyArgs().Map(Arg.Any<AffirmationLanguage>());
await _affirmationService.Received(1).Get();
_affirmationLanguageMapper.DidNotReceiveWithAnyArgs().Map(Arg.Any<AffirmationLanguage>());
await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void SetUp()
public async Task Handle_ShouldReturnRemainingAffirmationsCount(int maxRequestsPerDay)
{
// Arrange
_affirmationService.GetRemainingAffirmationsCount().Returns(maxRequestsPerDay);
_affirmationService.GetRemainingCount().Returns(maxRequestsPerDay);

// Act
var result = await _query.Handle();
Expand All @@ -36,7 +36,8 @@ public async Task Handle_ShouldReturnRemainingAffirmationsCount(int maxRequestsP
var response = result.ShouldBeSuccess();
response.RemainingCount.ShouldBe(maxRequestsPerDay);
response.RemainingCount.ShouldBeGreaterThanOrEqualTo(0);
response.ResetInSeconds.ShouldBeGreaterThanOrEqualTo(0);

await _affirmationService.Received(1).GetRemainingAffirmationsCount();
await _affirmationService.Received(1).GetRemainingCount();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ public static class NSubstituteExtensions
public ConfiguredCall ReturnsError<TError>()
where TError : ErrorDetails, new() => value.Returns(Result<T>.Error(new TError()));
}

extension<T>(Result<T> value)
{
public ConfiguredCall ReturnsSuccess(T returnThis) => value.Returns(Result<T>.Success(returnThis));

public ConfiguredCall ReturnsError<TError>()
where TError : ErrorDetails, new() => value.Returns(Result<T>.Error(new TError()));
}
}
5 changes: 3 additions & 2 deletions AffirmationGenerator.Server/Application/DiConfig.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using AffirmationGenerator.Server.Application.Queries;
using AffirmationGenerator.Server.Application.Services.Affirmation;
using AffirmationGenerator.Server.Application.Services.Language;
using AffirmationGenerator.Server.Application.Services.Mapping;
using AffirmationGenerator.Server.Application.Services.Mapping.Language;
using AffirmationGenerator.Server.Domain;

namespace AffirmationGenerator.Server.Application;
Expand All @@ -21,7 +22,7 @@ public IServiceCollection AddApplication(IConfiguration configuration)
services.AddScoped<GetAffirmationLanguagesQuery>();

services.AddScoped<IAffirmationService, AffirmationService>();
services.AddScoped<ILanguageCodeMapper<AffirmationLanguage>, AffirmationLanguageCodeMapper>();
services.AddScoped<IMapper<AffirmationLanguage, string>, AffirmationLanguageMapper>();

return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ namespace AffirmationGenerator.Server.Application.Models;
public sealed record RemainingAffirmationsResponse
{
public required int RemainingCount { get; init; }

public required double ResetInSeconds { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using AffirmationGenerator.Server.Application.Models;
using AffirmationGenerator.Server.Application.Services.Affirmation;
using AffirmationGenerator.Server.Application.Services.Language;
using AffirmationGenerator.Server.Application.Services.Mapping;
using AffirmationGenerator.Server.Core;
using AffirmationGenerator.Server.Core.Extensions;
using AffirmationGenerator.Server.Domain;
Expand All @@ -12,13 +12,13 @@ namespace AffirmationGenerator.Server.Application.Queries;
public sealed class GetAffirmationQuery(
IDeepLTranslatorClient translatorClient,
IAffirmationService affirmationService,
ILanguageCodeMapper<AffirmationLanguage> languageCodeMapper
IMapper<AffirmationLanguage, string> affirmationLanguageMapper
)
{
public async Task<Result<AffirmationResponse>> Handle(GetAffirmationRequest request) =>
await (
from affirmation in affirmationService.GetAffirmation()
from targetLanguageCode in languageCodeMapper.Map(request.TargetLanguage)
from affirmation in affirmationService.Get()
from targetLanguageCode in affirmationLanguageMapper.Map(request.TargetLanguage)
from translatedAffirmation in Translate(affirmation, targetLanguageCode)
select ToResponse(request.TargetLanguage, translatedAffirmation)
);
Expand All @@ -40,6 +40,6 @@ private async Task<AffirmationResponse> ToResponse(AffirmationLanguage targetLan
{
TargetLanguage = targetLanguage,
Text = affirmation,
RemainingCount = await affirmationService.GetRemainingAffirmationsCount(),
RemainingCount = await affirmationService.GetRemainingCount(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ public sealed class GetRemainingAffirmationsQuery(IAffirmationService affirmatio
{
public async Task<Result<RemainingAffirmationsResponse>> Handle()
{
var remainingCount = await affirmationService.GetRemainingAffirmationsCount();
return new RemainingAffirmationsResponse { RemainingCount = remainingCount };
var remainingCount = await affirmationService.GetRemainingCount();

var resetTime = await affirmationService.GetResetTime();

var resetInSeconds = remainingCount == 0 ? resetTime?.TotalSeconds ?? 0 : 0;

return new RemainingAffirmationsResponse { RemainingCount = remainingCount, ResetInSeconds = resetInSeconds };
}
}
Loading
Loading