Skip to content
Closed
35 changes: 22 additions & 13 deletions JournalApp/Components/DataPointView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ else if (Point.Type == PointType.Medication)
[Parameter]
public EventCallback StateChanged { get; set; }

[Parameter]
public MoodAnimation MoodAnimation { get; set; }

public int ScaleIndexForMudRating
{
get => Point.ScaleIndex ?? 0;
Expand All @@ -127,6 +130,25 @@ else if (Point.Type == PointType.Medication)
}
}

void OnMoodSelected(string mood)
{
logger.LogDebug("Mood selected: {Mood}", mood);
Point.Mood = mood;

// Trigger the animation if MoodAnimation is provided
if (MoodAnimation != null && mood != "🤔")
{
MoodAnimation.Show(mood);
}

// Show a motivational quote when the sob emoji is selected
if (mood == DataPoint.Moods[^1]) // Sob emoji (😭) is the last mood in the list
{
var quote = MotivationalQuotes.GetRandomQuote();
Snackbar.Add(quote, Severity.Info, c => c.Icon = Icons.Material.Rounded.FavoriteBorder);
}
}

void DecrementSleep()
{
logger.LogDebug("Decrementing sleep");
Expand Down Expand Up @@ -167,19 +189,6 @@ else if (Point.Type == PointType.Medication)
await StateChanged.InvokeAsync();
}

void OnMoodSelected(string mood)
{
logger.LogDebug("Mood selected: {mood}", mood);
Point.Mood = mood;

// Show a motivational quote when the sob emoji is selected
if (mood == DataPoint.Moods[^1]) // Sob emoji (😭) is the last mood in the list
{
var quote = MotivationalQuotes.GetRandomQuote();
Snackbar.Add(quote, Severity.Info, c => c.Icon = Icons.Material.Rounded.FavoriteBorder);
}
}

/// <summary>
/// Returns a header to be shown above the view for the point on the main timeline.
/// </summary>
Expand Down
65 changes: 65 additions & 0 deletions JournalApp/Components/MoodAnimation.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@namespace JournalApp
@inject PreferenceService PreferenceService

@if (_isVisible)
{
<div class="mood-animation-overlay" style="background-color: @_color;" @onclick="Hide">
<div class="mood-animation-content">
<div class="mood-animation-emoji">@_emoji</div>
</div>
@if (!string.IsNullOrEmpty(GetEffectClass()))
{
<div class="mood-animation-effects @GetEffectClass()"></div>
}
</div>
}

@code {
private bool _isVisible = false;
private string _emoji = "";
private string _color = "transparent";

public void Show(string emoji)
{
// Skip animation if disabled in settings
if (PreferenceService.DisableExtraAnimations)
{
return;
}

_emoji = emoji;
_color = PreferenceService.GetMoodColor(emoji);
_isVisible = true;
StateHasChanged();

// Auto-hide after animation completes (faster animation)
Task.Delay(1000).ContinueWith(_ =>
{
InvokeAsync(() =>
{
Hide();
});
});
}

private void Hide()
{
_isVisible = false;
StateHasChanged();
}

private string GetEffectClass()
{
return _emoji switch
{
"😭" => "rain-effect",
"😢" => "rain-effect light",
"😕" => "clouds-effect",
"😐" => "",
"🙂" => "sparkles-effect",
"😀" => "sunshine-effect",
"🤩" => "stars-effect",
_ => ""
};
}
}
274 changes: 274 additions & 0 deletions JournalApp/Components/MoodAnimation.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
.mood-animation-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
animation: fadeInOut 1s ease-in-out forwards;
cursor: pointer;
pointer-events: all;
}

.mood-animation-content {
display: flex;
align-items: center;
justify-content: center;
animation: scaleUp 1s cubic-bezier(0.05, 0.7, 0.1, 1) forwards;
}

.mood-animation-emoji {
font-size: 20vw;
user-select: none;
}

.mood-animation-effects {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}

@keyframes fadeInOut {
0% {
opacity: 0;
}
15% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
opacity: 0;
}
}

/* Material Design 3 Expressive: Simplified scale animation */
@keyframes scaleUp {
0% {
transform: scale(0.8);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}

/* Special effects for each mood */
.rain-effect::before {
content: '💧';
position: absolute;
font-size: 3vw;
animation: rainFall 0.8s linear infinite;
}

.rain-effect::after {
content: '💧';
position: absolute;
font-size: 3vw;
animation: rainFall 0.8s linear infinite 0.3s;
left: 30%;
}

.rain-effect.light::before,
.rain-effect.light::after {
animation-duration: 1s;
opacity: 0.7;
}

@keyframes rainFall {
0% {
top: -10%;
left: 10%;
opacity: 1;
}
100% {
top: 110%;
left: 15%;
opacity: 0.5;
}
}

.rain-effect {
background: repeating-linear-gradient(
180deg,
transparent,
transparent 40px,
rgba(100, 150, 200, 0.1) 40px,
rgba(100, 150, 200, 0.1) 42px
);
animation: rainLines 0.3s linear infinite;
}

@keyframes rainLines {
0% {
background-position: 0 0;
}
100% {
background-position: 0 42px;
}
}

.clouds-effect::before {
content: '☁️';
position: absolute;
font-size: 8vw;
top: 20%;
left: 10%;
animation: cloudFloat 3s ease-in-out infinite;
opacity: 0.6;
}

.clouds-effect::after {
content: '☁️';
position: absolute;
font-size: 6vw;
top: 30%;
right: 10%;
animation: cloudFloat 3s ease-in-out infinite 1s;
opacity: 0.5;
}

@keyframes cloudFloat {
0%, 100% {
transform: translateX(0);
}
50% {
transform: translateX(20px);
}
}

.sparkles-effect {
background-image:
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.8) 1px, transparent 1px),
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.6) 1px, transparent 1px),
radial-gradient(circle at 40% 70%, rgba(255, 255, 255, 0.7) 1px, transparent 1px),
radial-gradient(circle at 70% 60%, rgba(255, 255, 255, 0.5) 1px, transparent 1px);
animation: sparkle 1s ease-in-out;
}

@keyframes sparkle {
0%, 100% {
opacity: 0;
}
50% {
opacity: 1;
}
}

.sunshine-effect::before {
content: '☀️';
position: absolute;
font-size: 10vw;
top: 10%;
right: 15%;
animation: sunRise 1s ease-out;
opacity: 0.8;
}

@keyframes sunRise {
0% {
transform: translateY(50px) scale(0);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateY(0) scale(1);
opacity: 0.8;
}
}

.sunshine-effect {
background: radial-gradient(ellipse at top right, rgba(255, 230, 100, 0.3) 0%, transparent 60%);
}

.stars-effect::before,
.stars-effect::after {
content: '⭐';
position: absolute;
font-size: 5vw;
animation: starTwinkle 1s ease-in-out infinite;
}

.stars-effect::before {
top: 15%;
left: 15%;
}

.stars-effect::after {
bottom: 20%;
right: 20%;
animation-delay: 0.5s;
}

@keyframes starTwinkle {
0%, 100% {
transform: scale(0) rotate(0deg);
opacity: 0;
}
50% {
transform: scale(1.5) rotate(180deg);
opacity: 1;
}
}

.stars-effect {
background-image:
radial-gradient(circle at 30% 40%, rgba(255, 255, 100, 0.6) 2px, transparent 2px),
radial-gradient(circle at 70% 30%, rgba(255, 255, 150, 0.5) 2px, transparent 2px),
radial-gradient(circle at 50% 80%, rgba(255, 255, 200, 0.4) 2px, transparent 2px);
animation: twinkle 1s ease-in-out;
}

@keyframes twinkle {
0%, 100% {
opacity: 0;
}
50% {
opacity: 1;
}
}

/* Reduced motion support for accessibility */
@media (prefers-reduced-motion: reduce) {
.mood-animation-overlay {
animation: fadeInOutReduced 1s ease-in-out forwards;
}

.mood-animation-content {
animation: none;
}

.mood-animation-emoji {
animation: none !important;
transform: scale(1);
}

.mood-animation-effects {
display: none;
}

@keyframes fadeInOutReduced {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
Loading
Loading