226 lines
7.6 KiB
C#
226 lines
7.6 KiB
C#
using PlotLine.Data;
|
|
using PlotLine.Models;
|
|
using PlotLine.ViewModels;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IOnboardingService
|
|
{
|
|
Task<UserOnboardingState> GetOrCreateAsync();
|
|
Task<OnboardingWizardViewModel> GetWizardAsync();
|
|
Task<bool> ShouldShowOnboardingAsync();
|
|
Task<OnboardingNudgeViewModel> GetDashboardNudgeAsync();
|
|
Task<bool> ShouldShowWordCompanionHeaderAsync();
|
|
Task MoveToNextStepAsync();
|
|
Task SetCurrentStepAsync(string currentStep);
|
|
Task SaveWritingJourneyAsync(string writingJourney);
|
|
Task SaveWritingSoftwareAsync(string writingSoftware);
|
|
Task MarkCompletedAsync();
|
|
}
|
|
|
|
public sealed class OnboardingService(IOnboardingRepository onboarding, ICurrentUserService currentUser) : IOnboardingService
|
|
{
|
|
private static readonly HashSet<string> WritingJourneys = new(StringComparer.Ordinal)
|
|
{
|
|
WritingJourneyValues.PlanningNewStory,
|
|
WritingJourneyValues.AlreadyStarted,
|
|
WritingJourneyValues.ManuscriptMostlyComplete
|
|
};
|
|
|
|
private static readonly HashSet<string> WritingSoftware = new(StringComparer.Ordinal)
|
|
{
|
|
WritingSoftwareValues.MicrosoftWord,
|
|
WritingSoftwareValues.GoogleDocs,
|
|
WritingSoftwareValues.Scrivener,
|
|
WritingSoftwareValues.LibreOfficeOpenOffice,
|
|
WritingSoftwareValues.MarkdownObsidian,
|
|
WritingSoftwareValues.Other
|
|
};
|
|
|
|
public async Task<UserOnboardingState> GetOrCreateAsync()
|
|
=> await onboarding.GetOrCreateAsync(RequireUserId());
|
|
|
|
public async Task<OnboardingWizardViewModel> GetWizardAsync()
|
|
{
|
|
var state = await GetOrCreateAsync();
|
|
var step = ResolveStep(state);
|
|
|
|
return new OnboardingWizardViewModel
|
|
{
|
|
CurrentStep = step,
|
|
StepNumber = StepNumber(step),
|
|
WritingJourney = state.WritingJourney,
|
|
WritingSoftware = state.WritingSoftware,
|
|
Options = step switch
|
|
{
|
|
OnboardingSteps.WritingJourney => WritingJourneyOptions(),
|
|
OnboardingSteps.WritingSoftware => WritingSoftwareOptions(),
|
|
_ => []
|
|
}
|
|
};
|
|
}
|
|
|
|
public async Task<bool> ShouldShowOnboardingAsync()
|
|
{
|
|
if (!currentUser.UserId.HasValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var state = await onboarding.GetAsync(currentUser.UserId.Value);
|
|
return state?.OnboardingCompletedUtc is null;
|
|
}
|
|
|
|
public async Task<OnboardingNudgeViewModel> GetDashboardNudgeAsync()
|
|
=> new() { ShouldShow = await ShouldShowOnboardingAsync() };
|
|
|
|
public async Task<bool> ShouldShowWordCompanionHeaderAsync()
|
|
=> currentUser.UserId.HasValue
|
|
&& await onboarding.ShouldShowWordCompanionHeaderAsync(currentUser.UserId.Value);
|
|
|
|
public async Task MoveToNextStepAsync()
|
|
{
|
|
var state = await GetOrCreateAsync();
|
|
var nextStep = ResolveStep(state) switch
|
|
{
|
|
OnboardingSteps.Welcome => OnboardingSteps.WritingJourney,
|
|
OnboardingSteps.WritingJourney => OnboardingSteps.WritingSoftware,
|
|
_ => OnboardingSteps.Complete
|
|
};
|
|
|
|
await onboarding.SetCurrentStepAsync(RequireUserId(), nextStep);
|
|
}
|
|
|
|
public async Task SetCurrentStepAsync(string currentStep)
|
|
{
|
|
if (currentStep is not (OnboardingSteps.Welcome or OnboardingSteps.WritingJourney or OnboardingSteps.WritingSoftware or OnboardingSteps.Complete))
|
|
{
|
|
throw new InvalidOperationException("Choose a valid onboarding step.");
|
|
}
|
|
|
|
await onboarding.SetCurrentStepAsync(RequireUserId(), currentStep);
|
|
}
|
|
|
|
public async Task SaveWritingJourneyAsync(string writingJourney)
|
|
{
|
|
if (!WritingJourneys.Contains(writingJourney))
|
|
{
|
|
throw new InvalidOperationException("Choose a writing journey option.");
|
|
}
|
|
|
|
await onboarding.SaveWritingJourneyAsync(RequireUserId(), writingJourney);
|
|
}
|
|
|
|
public async Task SaveWritingSoftwareAsync(string writingSoftware)
|
|
{
|
|
if (!WritingSoftware.Contains(writingSoftware))
|
|
{
|
|
throw new InvalidOperationException("Choose a writing software option.");
|
|
}
|
|
|
|
await onboarding.SaveWritingSoftwareAsync(RequireUserId(), writingSoftware);
|
|
}
|
|
|
|
public async Task MarkCompletedAsync()
|
|
=> await onboarding.MarkCompletedAsync(RequireUserId());
|
|
|
|
private int RequireUserId()
|
|
=> currentUser.UserId ?? throw new InvalidOperationException("Sign in to use onboarding.");
|
|
|
|
private static string ResolveStep(UserOnboardingState state)
|
|
{
|
|
if (state.OnboardingCompletedUtc.HasValue)
|
|
{
|
|
return OnboardingSteps.Complete;
|
|
}
|
|
|
|
if (state.CurrentStep is OnboardingSteps.Welcome or OnboardingSteps.WritingJourney or OnboardingSteps.WritingSoftware)
|
|
{
|
|
return state.CurrentStep;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(state.WritingJourney))
|
|
{
|
|
return OnboardingSteps.WritingJourney;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(state.WritingSoftware))
|
|
{
|
|
return OnboardingSteps.WritingSoftware;
|
|
}
|
|
|
|
return OnboardingSteps.Complete;
|
|
}
|
|
|
|
private static int StepNumber(string step) => step switch
|
|
{
|
|
OnboardingSteps.Welcome => 1,
|
|
OnboardingSteps.WritingJourney => 2,
|
|
OnboardingSteps.WritingSoftware => 3,
|
|
_ => 3
|
|
};
|
|
|
|
private static IReadOnlyList<OnboardingOptionViewModel> WritingJourneyOptions() =>
|
|
[
|
|
new()
|
|
{
|
|
Value = WritingJourneyValues.PlanningNewStory,
|
|
Title = "I am planning a brand new story",
|
|
Description = "You are shaping the idea, structure, characters, or world before drafting."
|
|
},
|
|
new()
|
|
{
|
|
Value = WritingJourneyValues.AlreadyStarted,
|
|
Title = "I have already started writing",
|
|
Description = "You have draft material and want PlotDirector to help you organise what exists."
|
|
},
|
|
new()
|
|
{
|
|
Value = WritingJourneyValues.ManuscriptMostlyComplete,
|
|
Title = "My manuscript is largely complete",
|
|
Description = "You are ready to review structure, continuity, and story detail across the manuscript."
|
|
}
|
|
];
|
|
|
|
private static IReadOnlyList<OnboardingOptionViewModel> WritingSoftwareOptions() =>
|
|
[
|
|
new()
|
|
{
|
|
Value = WritingSoftwareValues.MicrosoftWord,
|
|
Title = "Microsoft Word",
|
|
Description = "Best fit for the PlotDirector Word Companion workflow.",
|
|
Badge = "Recommended"
|
|
},
|
|
new()
|
|
{
|
|
Value = WritingSoftwareValues.GoogleDocs,
|
|
Title = "Google Docs",
|
|
Description = "Use exports later when manuscript import is added."
|
|
},
|
|
new()
|
|
{
|
|
Value = WritingSoftwareValues.Scrivener,
|
|
Title = "Scrivener",
|
|
Description = "Use a manuscript export later when import support arrives."
|
|
},
|
|
new()
|
|
{
|
|
Value = WritingSoftwareValues.LibreOfficeOpenOffice,
|
|
Title = "LibreOffice / OpenOffice",
|
|
Description = "Good for document-based workflows outside Microsoft Word."
|
|
},
|
|
new()
|
|
{
|
|
Value = WritingSoftwareValues.MarkdownObsidian,
|
|
Title = "Markdown / Obsidian",
|
|
Description = "For plain-text writing systems and linked-note workflows."
|
|
},
|
|
new()
|
|
{
|
|
Value = WritingSoftwareValues.Other,
|
|
Title = "Other",
|
|
Description = "PlotDirector can still tailor future setup around your tools."
|
|
}
|
|
];
|
|
}
|