Review the PlotDirector subscription, pricing, signup and account-management areas and implement a complete trial-visibility pass.
This commit is contained in:
parent
61d16be6d7
commit
d4c886ce2f
@ -30,10 +30,12 @@ public sealed class ManageAccountController(
|
||||
return RedirectToAction("Login", "Account");
|
||||
}
|
||||
|
||||
var subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID);
|
||||
return View(new ManageAccountViewModel
|
||||
{
|
||||
User = user,
|
||||
Subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID),
|
||||
Subscription = subscription,
|
||||
Trial = TrialVisibilityViewModel.FromSubscription(subscription, DateTime.UtcNow),
|
||||
HasPaidBillingRisk = await subscriptions.HasPaidBillingRiskAsync(user.UserID)
|
||||
});
|
||||
}
|
||||
@ -104,6 +106,7 @@ public sealed class ManageAccountController(
|
||||
{
|
||||
User = user,
|
||||
CurrentSubscription = subscription,
|
||||
Trial = TrialVisibilityViewModel.FromSubscription(subscription, DateTime.UtcNow),
|
||||
Usage = subscription is null ? null : await subscriptions.GetUsageSummaryAsync(user.UserID),
|
||||
AvailableLevels = await subscriptions.GetActiveSubscriptionLevelsAsync()
|
||||
});
|
||||
|
||||
@ -66,6 +66,7 @@ public sealed class UserSubscriptionDetail : UserSubscription
|
||||
public int? MonthlyPricePence { get; set; }
|
||||
public int? AnnualPricePence { get; set; }
|
||||
public string CurrencyCode { get; set; } = "GBP";
|
||||
public bool RequiresStripeSubscription { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SubscriptionLimitResult
|
||||
|
||||
@ -372,6 +372,7 @@ public sealed class ProjectService(
|
||||
IWarningRepository warnings,
|
||||
IWritingScheduleRepository writingSchedule,
|
||||
IProjectActivityService activity,
|
||||
ISubscriptionService subscriptions,
|
||||
ICurrentUserService currentUser,
|
||||
IHardDeleteFileCleanupService fileCleanup,
|
||||
ILogger<ProjectService> logger) : IProjectService
|
||||
@ -386,7 +387,8 @@ public sealed class ProjectService(
|
||||
return new ProjectIndexViewModel
|
||||
{
|
||||
ActiveProjects = await projects.ListActiveForUserAsync(currentUser.UserId.Value),
|
||||
ArchivedProjects = await projects.ListArchivedForUserAsync(currentUser.UserId.Value)
|
||||
ArchivedProjects = await projects.ListArchivedForUserAsync(currentUser.UserId.Value),
|
||||
Trial = TrialVisibilityViewModel.FromSubscription(await subscriptions.GetCurrentSubscriptionAsync(currentUser.UserId.Value), DateTime.UtcNow)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ public sealed class ManageAccountViewModel
|
||||
{
|
||||
public AppUser User { get; set; } = new();
|
||||
public UserSubscriptionDetail? Subscription { get; set; }
|
||||
public TrialVisibilityViewModel? Trial { get; set; }
|
||||
public bool HasPaidBillingRisk { get; set; }
|
||||
}
|
||||
|
||||
@ -14,10 +15,77 @@ public sealed class ManageSubscriptionViewModel
|
||||
{
|
||||
public AppUser User { get; set; } = new();
|
||||
public UserSubscriptionDetail? CurrentSubscription { get; set; }
|
||||
public TrialVisibilityViewModel? Trial { get; set; }
|
||||
public SubscriptionUsageSummary? Usage { get; set; }
|
||||
public IReadOnlyList<SubscriptionLevel> AvailableLevels { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class TrialVisibilityViewModel
|
||||
{
|
||||
public const int TrialDurationDays = 30;
|
||||
|
||||
public string PlanName { get; init; } = string.Empty;
|
||||
public DateTime StartDateUtc { get; init; }
|
||||
public DateTime ExpiryDateUtc { get; init; }
|
||||
public int DaysRemaining { get; init; }
|
||||
public int DaysElapsed { get; init; }
|
||||
public int ProgressPercentage { get; init; }
|
||||
public bool IsExpired { get; init; }
|
||||
public int? WarningThresholdDays { get; init; }
|
||||
|
||||
public string StatusLabel => IsExpired
|
||||
? "Trial expired"
|
||||
: DaysRemaining == 1 ? "1 day remaining" : $"{DaysRemaining} days remaining";
|
||||
|
||||
public string WarningCssClass => WarningThresholdDays switch
|
||||
{
|
||||
<= 1 => "trial-status--one-day",
|
||||
<= 3 => "trial-status--three-days",
|
||||
<= 7 => "trial-status--seven-days",
|
||||
<= 14 => "trial-status--fourteen-days",
|
||||
_ => "trial-status--steady"
|
||||
};
|
||||
|
||||
public static TrialVisibilityViewModel? FromSubscription(UserSubscriptionDetail? subscription, DateTime utcNow)
|
||||
{
|
||||
if (subscription is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var isTrialPlan = subscription.IsTrial
|
||||
|| (!subscription.RequiresStripeSubscription && string.IsNullOrWhiteSpace(subscription.StripeSubscriptionId));
|
||||
if (!isTrialPlan)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var start = subscription.StartDateUTC;
|
||||
var expiry = subscription.TrialExpiryDateUTC ?? start.AddDays(TrialDurationDays);
|
||||
var totalDays = Math.Max(1, (int)Math.Ceiling((expiry - start).TotalDays));
|
||||
var rawRemaining = (int)Math.Ceiling((expiry - utcNow).TotalDays);
|
||||
var daysRemaining = Math.Max(0, rawRemaining);
|
||||
var daysElapsed = Math.Clamp(totalDays - daysRemaining, 0, totalDays);
|
||||
var progress = Math.Clamp((int)Math.Round(daysElapsed * 100m / totalDays), 0, 100);
|
||||
|
||||
return new TrialVisibilityViewModel
|
||||
{
|
||||
PlanName = subscription.DisplayName,
|
||||
StartDateUtc = start,
|
||||
ExpiryDateUtc = expiry,
|
||||
DaysRemaining = daysRemaining,
|
||||
DaysElapsed = daysElapsed,
|
||||
ProgressPercentage = progress,
|
||||
IsExpired = utcNow >= expiry,
|
||||
WarningThresholdDays = daysRemaining <= 1 ? 1
|
||||
: daysRemaining <= 3 ? 3
|
||||
: daysRemaining <= 7 ? 7
|
||||
: daysRemaining <= 14 ? 14
|
||||
: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PricingViewModel
|
||||
{
|
||||
public IReadOnlyList<SubscriptionLevel> Plans { get; set; } = [];
|
||||
|
||||
@ -25,6 +25,7 @@ public sealed class ProjectIndexViewModel
|
||||
{
|
||||
public IReadOnlyList<Project> ActiveProjects { get; init; } = [];
|
||||
public IReadOnlyList<Project> ArchivedProjects { get; init; } = [];
|
||||
public TrialVisibilityViewModel? Trial { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ProjectDetailViewModel
|
||||
|
||||
@ -7,10 +7,19 @@
|
||||
<div>
|
||||
<p class="eyebrow">Account</p>
|
||||
<h1>Create your account</h1>
|
||||
<p class="lead-text">Create a PlotDirector account so your workspace can use the new authentication foundation.</p>
|
||||
<p class="lead-text">Create your account to start a free 30-day PlotDirector trial immediately. No payment details are required.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="trial-status-panel trial-status-panel--signup">
|
||||
<div>
|
||||
<p class="eyebrow">Free trial</p>
|
||||
<h2>30 days to plan, test and decide</h2>
|
||||
<p>Your trial begins as soon as registration is complete. When it expires, your account will show that the trial has ended and you can upgrade from the subscription page.</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-primary" asp-controller="Pricing" asp-action="Index">Compare plans</a>
|
||||
</section>
|
||||
|
||||
<section class="edit-panel">
|
||||
<form asp-action="Register" method="post" class="asset-create-form">
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
@{
|
||||
ViewData["Title"] = "Manage Account";
|
||||
var subscription = Model.Subscription;
|
||||
var trial = Model.Trial;
|
||||
}
|
||||
|
||||
<div class="page-heading compact">
|
||||
@ -64,11 +65,39 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (trial is not null)
|
||||
{
|
||||
<div class="trial-status-panel @trial.WarningCssClass">
|
||||
<div class="trial-status-panel__heading">
|
||||
<div>
|
||||
<p class="eyebrow">Free trial status</p>
|
||||
<h3>@trial.StatusLabel</h3>
|
||||
</div>
|
||||
<a class="btn btn-outline-primary btn-sm" asp-action="Subscription">Upgrade</a>
|
||||
</div>
|
||||
<p>Your 30-day @trial.PlanName trial expires on @FormatDate(trial.ExpiryDateUtc).</p>
|
||||
<div class="trial-progress" aria-label="Trial progress">
|
||||
<div class="trial-progress__bar" style="width: @trial.ProgressPercentage%"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<p class="eyebrow">Current plan</p>
|
||||
<p>@subscription.DisplayName</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="eyebrow">Trial start</p>
|
||||
<p>@(trial is null ? "Not on trial" : FormatDate(trial.StartDateUtc))</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="eyebrow">Trial expiry</p>
|
||||
<p>@(trial is null ? "Not on trial" : FormatDate(trial.ExpiryDateUtc))</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="eyebrow">Days remaining</p>
|
||||
<p>@(trial is null ? "Not on trial" : trial.StatusLabel)</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="eyebrow">Books</p>
|
||||
<p>@FormatLimit(subscription.MaxBooks)</p>
|
||||
@ -159,6 +188,8 @@
|
||||
private static string FormatLimit(int value) => value >= 999999 ? "Practical unlimited" : value.ToString("N0");
|
||||
|
||||
private static string FormatStorage(int value) => value >= 1024 ? $"{value / 1024:N0} GB" : $"{value:N0} MB";
|
||||
|
||||
private static string FormatDate(DateTime value) => value.ToLocalTime().ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
@{
|
||||
ViewData["Title"] = "Subscription";
|
||||
var subscription = Model.CurrentSubscription;
|
||||
var trial = Model.Trial;
|
||||
var usage = Model.Usage;
|
||||
}
|
||||
|
||||
@ -34,6 +35,23 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (trial is not null)
|
||||
{
|
||||
<div class="trial-status-panel @trial.WarningCssClass">
|
||||
<div class="trial-status-panel__heading">
|
||||
<div>
|
||||
<p class="eyebrow">Free trial status</p>
|
||||
<h3>@trial.StatusLabel</h3>
|
||||
</div>
|
||||
<a class="btn btn-primary btn-sm" asp-controller="Pricing" asp-action="Index">Upgrade</a>
|
||||
</div>
|
||||
<p>Your 30-day @trial.PlanName trial started on @FormatDate(trial.StartDateUtc) and expires on @FormatDate(trial.ExpiryDateUtc).</p>
|
||||
<div class="trial-progress" aria-label="Trial progress">
|
||||
<div class="trial-progress__bar" style="width: @trial.ProgressPercentage%"></div>
|
||||
</div>
|
||||
<p class="muted mb-0">@trial.DaysElapsed days used, @trial.DaysRemaining days remaining.</p>
|
||||
</div>
|
||||
}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<p class="eyebrow">Plan</p>
|
||||
@ -53,11 +71,19 @@
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="eyebrow">Trial</p>
|
||||
<p>@(subscription.IsTrial ? "Trial" : "No")</p>
|
||||
<p>@(trial is null ? "No" : "30-day free trial")</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="eyebrow">Trial start</p>
|
||||
<p>@(trial is null ? "Not set" : FormatDate(trial.StartDateUtc))</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="eyebrow">Trial expiry</p>
|
||||
<p>@FormatDate(subscription.TrialExpiryDateUTC)</p>
|
||||
<p>@(trial is null ? FormatDate(subscription.TrialExpiryDateUTC) : FormatDate(trial.ExpiryDateUtc))</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="eyebrow">Days remaining</p>
|
||||
<p>@(trial is null ? "Not on trial" : trial.StatusLabel)</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="eyebrow">Started</p>
|
||||
@ -160,7 +186,7 @@
|
||||
<p>@(string.IsNullOrWhiteSpace(level.Description) ? "No description available." : level.Description)</p>
|
||||
@if (!level.RequiresStripeSubscription)
|
||||
{
|
||||
<p class="lead-text">Free / trial / application managed</p>
|
||||
<p class="lead-text">30-day free trial / application managed</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -197,7 +223,7 @@
|
||||
</dl>
|
||||
@if (!level.RequiresStripeSubscription)
|
||||
{
|
||||
<p class="muted">Draft Desk is managed inside PlotDirector.</p>
|
||||
<p class="muted">Draft Desk is a 30-day free trial. No payment details are required.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
<div class="marketing-container pricing-hero__inner">
|
||||
<p class="marketing-eyebrow">Pricing</p>
|
||||
<h1>Simple pricing for stories of every size.</h1>
|
||||
<p class="marketing-lead">Start free, subscribe when you're ready, or choose a plan straight away.</p>
|
||||
<p class="marketing-lead">Start with a 30-day free trial. No payment details are required until you choose a paid plan.</p>
|
||||
<div class="marketing-actions">
|
||||
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="StartTrial">Start Free Trial</a>
|
||||
<a class="marketing-btn marketing-btn--secondary" href="#plans">Compare Plans</a>
|
||||
@ -71,7 +71,12 @@
|
||||
{
|
||||
var isCurrent = Model.CurrentSubscriptionLevelID == plan.SubscriptionLevelID;
|
||||
var isSuggested = suggestedPlanId == plan.SubscriptionLevelID;
|
||||
<article class="pricing-card @(isSuggested ? "pricing-card--featured" : string.Empty)">
|
||||
var isTrialPlan = !plan.RequiresStripeSubscription;
|
||||
<article class="pricing-card @(isSuggested ? "pricing-card--featured" : string.Empty) @(isTrialPlan ? "pricing-card--trial" : string.Empty)">
|
||||
@if (isTrialPlan)
|
||||
{
|
||||
<p class="pricing-badge pricing-badge--trial">30-day free trial</p>
|
||||
}
|
||||
@if (isSuggested)
|
||||
{
|
||||
<p class="pricing-badge">Most Popular</p>
|
||||
@ -94,12 +99,17 @@
|
||||
else
|
||||
{
|
||||
<strong>Free</strong>
|
||||
<span>trial workspace</span>
|
||||
<small>Assigned when you create an account.</small>
|
||||
<span>30-day trial workspace</span>
|
||||
<small>No payment details required. Starts immediately after registration.</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ul class="pricing-feature-list">
|
||||
@if (!plan.RequiresStripeSubscription)
|
||||
{
|
||||
<li>30 days of free access</li>
|
||||
<li>No credit card required</li>
|
||||
}
|
||||
<li>@FormatLimit(plan.MaxBooks) books</li>
|
||||
<li>@FormatLimit(plan.MaxCharactersPerProject) characters per project</li>
|
||||
<li>@FormatLimit(plan.MaxScenesPerBook) scenes per book</li>
|
||||
@ -166,7 +176,7 @@
|
||||
<div class="faq-grid">
|
||||
<article>
|
||||
<h3>Can I start with a free trial?</h3>
|
||||
<p>Yes. Creating an account starts you on the Draft Desk evaluation plan, so you can try the workspace before choosing a paid subscription.</p>
|
||||
<p>Yes. Creating an account starts a 30-day Draft Desk trial immediately, so you can try the workspace before choosing a paid subscription.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Can I subscribe without using the trial?</h3>
|
||||
@ -186,7 +196,11 @@
|
||||
</article>
|
||||
<article>
|
||||
<h3>Do I need a credit card for the trial?</h3>
|
||||
<p>The registration flow creates the evaluation workspace. Payment details are only handled when you choose a paid subscription through Stripe Checkout.</p>
|
||||
<p>No. Payment details are not required for the free trial. Stripe Checkout is only used when you choose a paid subscription.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>What happens when the trial expires?</h3>
|
||||
<p>Your trial status will show in your dashboard and subscription pages. When the 30 days are over, choose a paid plan to continue with higher paid-plan access.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
@ -196,7 +210,7 @@
|
||||
<div class="marketing-container final-cta__inner">
|
||||
<p class="marketing-eyebrow">Begin with clarity</p>
|
||||
<h2>Ready to take control of your story?</h2>
|
||||
<p>Try PlotDirector free, or choose the paid plan that fits the novel you are building.</p>
|
||||
<p>Try PlotDirector free for 30 days with no payment details, or choose the paid plan that fits the novel you are building.</p>
|
||||
<div class="marketing-actions">
|
||||
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="StartTrial">Start Free Trial</a>
|
||||
<a class="marketing-btn marketing-btn--secondary" href="#plans">View Plans</a>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
@model ProjectIndexViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Projects";
|
||||
var trial = Model.Trial;
|
||||
}
|
||||
|
||||
<div class="page-heading">
|
||||
@ -14,6 +15,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (trial is not null)
|
||||
{
|
||||
<section class="trial-dashboard-banner @trial.WarningCssClass"
|
||||
data-trial-banner
|
||||
data-trial-banner-key="projects:@trial.ExpiryDateUtc.ToString("yyyyMMdd")"
|
||||
aria-label="Trial status">
|
||||
<div class="trial-dashboard-banner__body">
|
||||
<p class="eyebrow">Free trial status</p>
|
||||
<h2>@trial.StatusLabel on @trial.PlanName</h2>
|
||||
<p>Your 30-day trial expires on @FormatDate(trial.ExpiryDateUtc). Upgrade whenever you are ready.</p>
|
||||
</div>
|
||||
<div class="trial-dashboard-banner__actions">
|
||||
<a class="btn btn-primary btn-sm" asp-controller="ManageAccount" asp-action="Subscription">Upgrade</a>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-trial-banner-dismiss>Dismiss</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (TempData["ArchiveMessage"] is string archiveMessage)
|
||||
{
|
||||
<div class="alert alert-success">@archiveMessage</div>
|
||||
@ -140,6 +159,10 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@functions {
|
||||
private static string FormatDate(DateTime value) => value.ToLocalTime().ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
@ -558,6 +558,49 @@ a:focus-visible {
|
||||
color: var(--plotline-text);
|
||||
}
|
||||
|
||||
.trial-status-panel,
|
||||
.trial-dashboard-banner {
|
||||
border-color: rgba(84, 68, 48, 0.16);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 253, 248, 0.96), rgba(242, 226, 207, 0.56)),
|
||||
var(--plotline-surface);
|
||||
box-shadow: var(--plotline-shadow-subtle);
|
||||
}
|
||||
|
||||
.trial-status-panel--signup {
|
||||
border-color: rgba(49, 95, 103, 0.22);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(223, 236, 239, 0.72), rgba(255, 253, 248, 0.92)),
|
||||
var(--plotline-surface);
|
||||
}
|
||||
|
||||
.trial-status-panel h2,
|
||||
.trial-status-panel h3,
|
||||
.trial-dashboard-banner h2 {
|
||||
color: var(--plotline-text);
|
||||
}
|
||||
|
||||
.trial-progress {
|
||||
box-shadow: inset 0 1px 2px rgba(62, 45, 28, 0.08);
|
||||
}
|
||||
|
||||
.trial-progress__bar {
|
||||
background: linear-gradient(90deg, var(--plotline-secondary), var(--plotline-accent));
|
||||
}
|
||||
|
||||
.trial-status--fourteen-days {
|
||||
border-color: rgba(184, 118, 43, 0.28);
|
||||
}
|
||||
|
||||
.trial-status--seven-days,
|
||||
.trial-status--three-days,
|
||||
.trial-status--one-day {
|
||||
border-color: rgba(184, 118, 43, 0.38);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(251, 236, 211, 0.84), rgba(255, 253, 248, 0.92)),
|
||||
var(--plotline-surface);
|
||||
}
|
||||
|
||||
.table {
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-color: var(--plotline-text);
|
||||
@ -3649,6 +3692,14 @@ a:focus-visible {
|
||||
box-shadow: 0 26px 58px rgba(31, 42, 68, 0.13);
|
||||
}
|
||||
|
||||
.pricing-card--trial {
|
||||
border-color: rgba(95, 120, 108, 0.52);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 253, 249, 0.98), rgba(235, 241, 237, 0.9)),
|
||||
#fff;
|
||||
box-shadow: 0 24px 54px rgba(31, 42, 68, 0.11);
|
||||
}
|
||||
|
||||
.pricing-badge {
|
||||
position: absolute;
|
||||
top: clamp(1.2rem, 2.4vw, 1.65rem);
|
||||
@ -3666,6 +3717,12 @@ a:focus-visible {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pricing-badge--trial {
|
||||
border-color: rgba(95, 120, 108, 0.48);
|
||||
color: #314b42;
|
||||
background: rgba(95, 120, 108, 0.18);
|
||||
}
|
||||
|
||||
.pricing-card__heading {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
|
||||
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
File diff suppressed because one or more lines are too long
@ -169,6 +169,77 @@ td.text-end {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.trial-status-panel,
|
||||
.trial-dashboard-banner {
|
||||
border: 1px solid var(--plotline-line);
|
||||
border-radius: 8px;
|
||||
padding: 18px 20px;
|
||||
background: var(--plotline-panel);
|
||||
}
|
||||
|
||||
.trial-status-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.trial-status-panel--signup {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.trial-status-panel h2,
|
||||
.trial-status-panel h3,
|
||||
.trial-dashboard-banner h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.trial-status-panel p,
|
||||
.trial-dashboard-banner p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.trial-status-panel__heading,
|
||||
.trial-dashboard-banner {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.trial-dashboard-banner {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.trial-dashboard-banner__body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.trial-dashboard-banner__actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.trial-progress {
|
||||
height: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(84, 68, 48, 0.12);
|
||||
}
|
||||
|
||||
.trial-progress__bar {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.trial-status-panel[hidden],
|
||||
.trial-dashboard-banner[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.location-page .page-heading.compact,
|
||||
.location-page .edit-panel,
|
||||
.location-page .empty-panel,
|
||||
@ -3974,6 +4045,19 @@ body.dragging-location [data-drag-type="location"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.trial-status-panel--signup,
|
||||
.trial-status-panel__heading,
|
||||
.trial-dashboard-banner {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.trial-dashboard-banner__actions,
|
||||
.trial-dashboard-banner__actions .btn,
|
||||
.trial-status-panel--signup .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attention-list p {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
@ -37,6 +37,32 @@
|
||||
applyTheme(readTheme() || "light");
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const banner = document.querySelector("[data-trial-banner]");
|
||||
if (!banner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `plotline.trialBanner.dismissed.${banner.dataset.trialBannerKey || "current"}`;
|
||||
try {
|
||||
if (localStorage.getItem(key) === "true") {
|
||||
banner.hidden = true;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// The banner can still be dismissed for this page view if storage is unavailable.
|
||||
}
|
||||
|
||||
banner.querySelector("[data-trial-banner-dismiss]")?.addEventListener("click", () => {
|
||||
banner.hidden = true;
|
||||
try {
|
||||
localStorage.setItem(key, "true");
|
||||
} catch {
|
||||
// Dismissal persistence is a convenience only.
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const modalElement = document.getElementById("plotlineConfirmModal");
|
||||
const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null;
|
||||
|
||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user