239 lines
12 KiB
Plaintext

@model PricingViewModel
@using Microsoft.AspNetCore.Html
@{
ViewData["Title"] = "Pricing";
ViewData["ShellClass"] = "marketing-shell";
var paidPlans = Model.Plans.Where(x => x.RequiresStripeSubscription).ToList();
var suggestedPlanId = paidPlans.Count >= 2 ? paidPlans[paidPlans.Count / 2].SubscriptionLevelID : (int?)null;
}
<article class="marketing-home pricing-page">
<section class="pricing-hero">
<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 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>
</div>
</div>
</section>
@if (TempData["PricingMessage"] is string pricingMessage)
{
<div class="marketing-container">
<div class="pricing-message" role="status">@pricingMessage</div>
</div>
}
<section class="pricing-plans" id="plans" aria-labelledby="plans-heading">
<div class="marketing-container">
<div class="section-heading">
<p class="marketing-eyebrow">Plans</p>
<h2 id="plans-heading">Choose the space your story needs.</h2>
</div>
@if (!Model.Plans.Any())
{
<div class="pricing-empty">
<h2>Plans are being prepared.</h2>
<p>Active subscription levels are not available yet. Please check back soon.</p>
</div>
}
else
{
<div class="pricing-card-grid">
@foreach (var plan in Model.Plans)
{
var isCurrent = Model.CurrentSubscriptionLevelID == plan.SubscriptionLevelID;
var isSuggested = suggestedPlanId == plan.SubscriptionLevelID;
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>
}
<div class="pricing-card__heading">
<h3>@plan.DisplayName</h3>
<p>@PlanTagline(plan)</p>
</div>
<div class="pricing-price">
@if (plan.RequiresStripeSubscription)
{
@PriceMarkup(plan.MonthlyPricePence, plan.CurrencyCode)
<span>per month</span>
@if (plan.AnnualPricePence.HasValue)
{
<small>@FormatAnnualPrice(plan.AnnualPricePence, plan.CurrencyCode) billed annually</small>
}
}
else
{
<strong>Free</strong>
<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>
<li>@FormatStorageAllowance(plan.MaxStorageMB) storage</li>
<li>@FormatLimit(plan.MaxCollaborators) collaborators</li>
</ul>
<div class="pricing-card__actions">
@if (!plan.RequiresStripeSubscription)
{
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="StartTrial">Start Free Trial</a>
}
else
{
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="Checkout" asp-route-id="@plan.SubscriptionLevelID" asp-route-billingInterval="Monthly">
@(isCurrent ? "Current Plan" : "Subscribe Now")
</a>
@if (plan.AnnualPricePence.HasValue)
{
<a class="pricing-link" asp-controller="Pricing" asp-action="Checkout" asp-route-id="@plan.SubscriptionLevelID" asp-route-billingInterval="Annual">Choose annual billing</a>
}
}
</div>
</article>
}
</div>
}
</div>
</section>
<section class="pricing-compare" aria-labelledby="compare-heading">
<div class="marketing-container">
<div class="section-heading">
<p class="marketing-eyebrow">What is included</p>
<h2 id="compare-heading">Everything you need to keep a complex novel coherent.</h2>
</div>
<div class="comparison-grid">
<article>
<h3>Planning capacity</h3>
<p>Database-backed limits for books, chapters, scenes, characters, storage, and collaborators scale with the plan you choose.</p>
</article>
<article>
<h3>Story organisation</h3>
<p>Characters, plot threads, story assets, locations, and the story bible help keep the moving pieces of your manuscript in one place.</p>
</article>
<article>
<h3>Structure and flow</h3>
<p>Timeline, scene metrics, story health, and analytics views help you understand pacing, pressure, and revision priorities.</p>
</article>
<article>
<h3>Continuity confidence</h3>
<p>Relationship mapping, continuity warnings, import, export, backups, and restore points support serious drafting and revision work.</p>
</article>
</div>
</div>
</section>
<section class="pricing-faq" aria-labelledby="faq-heading">
<div class="marketing-container">
<div class="section-heading">
<p class="marketing-eyebrow">Questions</p>
<h2 id="faq-heading">Pricing FAQ</h2>
</div>
<div class="faq-grid">
<article>
<h3>Can I start with a free trial?</h3>
<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>
<p>Yes. Choose a paid plan from this page. If you are not signed in, PlotDirector will ask you to log in before continuing to checkout.</p>
</article>
<article>
<h3>Can I change plan later?</h3>
<p>Yes. Signed-in users can manage subscription changes from the account subscription area.</p>
</article>
<article>
<h3>What happens to my work if I cancel?</h3>
<p>Your existing story data remains in PlotDirector. Your account may return to the application-managed Draft Desk level after the paid subscription ends.</p>
</article>
<article>
<h3>Is PlotDirector suitable for a series?</h3>
<p>Yes. The higher plan limits are designed for authors managing multiple books, long arcs, and larger casts.</p>
</article>
<article>
<h3>Do I need a credit card for the trial?</h3>
<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>
</section>
<section class="final-cta">
<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 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>
</div>
</div>
</section>
</article>
@functions {
private const int PracticalUnlimited = 999999;
private static string FormatLimit(int value) =>
value >= PracticalUnlimited ? "Unlimited" : value.ToString("N0");
private static string FormatStorageAllowance(int megabytes) =>
megabytes >= PracticalUnlimited
? "Unlimited"
: StorageUsage.FormatBytes(megabytes * 1024L * 1024L);
private static IHtmlContent PriceMarkup(int? pence, string? currencyCode)
{
if (!pence.HasValue)
{
return new HtmlString("<strong>Contact us</strong>");
}
var code = string.IsNullOrWhiteSpace(currencyCode) ? "GBP" : currencyCode.ToUpperInvariant();
var symbol = string.Equals(code, "GBP", StringComparison.OrdinalIgnoreCase) ? "£" : string.Empty;
return new HtmlString($"<strong><span class=\"pricing-price__value\">{symbol}{pence.Value / 100m:0.00}</span> <small>{code}</small></strong>");
}
private static string FormatAnnualPrice(int? pence, string? currencyCode)
{
if (!pence.HasValue)
{
return "Contact us";
}
var code = string.IsNullOrWhiteSpace(currencyCode) ? "GBP" : currencyCode.ToUpperInvariant();
var symbol = string.Equals(code, "GBP", StringComparison.OrdinalIgnoreCase) ? "£" : string.Empty;
return $"{symbol}{pence.Value / 100m:0.00}";
}
private static string PlanTagline(SubscriptionLevel plan) =>
string.IsNullOrWhiteSpace(plan.Description)
? plan.RequiresStripeSubscription ? "For authors ready to build with confidence." : "A calm place to begin."
: plan.Description;
}