pricing stuff updates

This commit is contained in:
bflatastic authored and Brandon Thomas committed 2026-01-27 05:24:32 -05:00
1 parent 4883185ad8
commit 7b2b71eaf0
15 files changed
+849 -304

No files matched your search

+19 -9
View File
@@ -1,19 +1,30 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-VKW89949V0"></script>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-VKW89949V0"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-VKW89949V0');
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-VKW89949V0");
</script>
<meta charset="utf-8" />
<title>ArtCraft. AI Video and Images. Fast and Open Desktop App.</title>
<meta name="description" content="ArtCraft is an Open Desktop app for generating AI Video and Images. You own ArtCraft!" />
<meta name="keywords" content="ArtCraft, AI, AI Video, AI Images, Open Source" />
<meta
name="description"
content="ArtCraft is an Open Desktop app for generating AI Video and Images. You own ArtCraft!"
/>
<meta
name="keywords"
content="ArtCraft, AI, AI Video, AI Images, Open Source"
/>
<base href="/" />
@@ -23,10 +34,9 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
href="https://fonts.googleapis.com/css2?family=Outfit:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
+13 -1
View File
@@ -1,4 +1,4 @@
import { Route, Routes } from "react-router-dom";
import { Route, Routes, Navigate } from "react-router-dom";
import Download from "../pages/download";
import Media from "../pages/media";
import PressKit from "../pages/press-kit";
@@ -15,6 +15,7 @@ import Login from "../pages/login";
import Signup from "../pages/signup";
import ForgotPassword from "../pages/forgot-password";
import Welcome from "../pages/welcome";
import { CheckoutSuccess, CheckoutCancel } from "../pages/checkout";
export function App() {
return (
@@ -38,6 +39,17 @@ export function App() {
<Route path="/signup" element={<Signup />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/welcome" element={<Welcome />} />
<Route path="/checkout/success" element={<CheckoutSuccess />} />
<Route path="/checkout/cancel" element={<CheckoutCancel />} />
{/* Redirects for underscore-based URLs (legacy Stripe config) */}
<Route
path="/checkout_success"
element={<Navigate to="/checkout/success" replace />}
/>
<Route
path="/checkout_cancel"
element={<Navigate to="/checkout/cancel" replace />}
/>
</Routes>
</div>
);
@@ -1,3 +1,4 @@
export * from "./navbar";
export * from "./footer";
export * from "./download-button";
export * from "./pricing-table";
@@ -0,0 +1 @@
export { default as PricingTable } from "./pricing-table";
@@ -0,0 +1,412 @@
import { faCheck } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@storyteller/ui-button";
import {
SUBSCRIPTION_PLANS,
SubscriptionPlanDetails,
} from "@storyteller/subscription";
import { twMerge } from "tailwind-merge";
import { useState, useEffect } from "react";
import { TabSelector } from "@storyteller/ui-tab-selector";
import {
UsersApi,
BillingApi,
UserInfo,
StorytellerApiHostStore,
} from "@storyteller/api";
import { useNavigate } from "react-router-dom";
const billingTabs = [
{ id: "yearly", label: "Yearly" },
{ id: "monthly", label: "Monthly" },
];
// Mapping from our plan slugs to API plan slugs
const PLAN_SLUG_MAP: Record<string, string> = {
artcraft_basic: "artcraft_basic",
artcraft_pro: "artcraft_pro",
artcraft_max: "artcraft_max",
};
interface PricingTableProps {
includeFree?: boolean;
showHeader?: boolean;
title?: string;
subtitle?: string;
className?: string;
compact?: boolean;
}
const PricingTable = ({
includeFree = false,
showHeader = true,
title = "Choose Your Plan",
subtitle = "Support open-source development. Your subscription keeps ArtCraft free and open for everyone.",
className = "",
compact = false,
}: PricingTableProps) => {
const navigate = useNavigate();
const [billingType, setBillingType] = useState("yearly");
const isYearly = billingType === "yearly";
// User and subscription state
const [user, setUser] = useState<UserInfo | undefined>(undefined);
const [activePlanSlug, setActivePlanSlug] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [processingPlan, setProcessingPlan] = useState<string | null>(null);
// Fetch user session and subscriptions on mount
useEffect(() => {
const fetchUserData = async () => {
try {
const usersApi = new UsersApi();
const sessionResponse = await usersApi.GetSession();
if (
sessionResponse.success &&
sessionResponse.data?.loggedIn &&
sessionResponse.data?.user
) {
setUser(sessionResponse.data.user);
// Fetch active subscriptions
const billingApi = new BillingApi();
const subscriptionsResponse =
await billingApi.ListActiveSubscriptions();
if (
subscriptionsResponse.success &&
subscriptionsResponse.data?.active_subscriptions
) {
// Find ArtCraft subscription
const artcraftSub =
subscriptionsResponse.data.active_subscriptions.find(
(sub) => sub.namespace === "artcraft",
);
if (artcraftSub) {
setActivePlanSlug(artcraftSub.product_slug);
}
}
}
} catch (error) {
console.error("Error fetching user data:", error);
} finally {
setIsLoading(false);
}
};
fetchUserData();
}, []);
const plans = includeFree
? SUBSCRIPTION_PLANS
: SUBSCRIPTION_PLANS.filter((plan) => plan.slug !== "free");
const getColorSchemeClasses = (
colorScheme: SubscriptionPlanDetails["colorScheme"],
) => {
const baseClasses =
"relative rounded-3xl p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md";
switch (colorScheme) {
case "dark":
return twMerge(
baseClasses,
"bg-[#1C1C20] border-white/10 hover:border-white/20",
);
case "green":
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#002D23]/80 via-[#006B54]/50 to-[#00D28B]/10 border-[#00a873]/50",
"hover:border-[#00a873] hover:shadow-[0_0_30px_rgba(0,210,139,0.2)]",
);
case "purple":
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#2D004D]/80 via-[#6400A8]/50 to-[#C03FFF]/10 border-[#9D4CFF]/50",
"hover:border-[#9D4CFF] hover:shadow-[0_0_30px_rgba(192,63,255,0.2)]",
);
case "orange":
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#332100]/80 via-[#B35C00]/50 to-[#FFB347]/10 border-[#FF8C00]/50",
"hover:border-[#FF8C00] hover:shadow-[0_0_30px_rgba(255,179,71,0.2)]",
);
default:
return twMerge(
baseClasses,
"bg-white/5 border-white/10 hover:border-white/20",
);
}
};
const isCurrentPlan = (planSlug: string) => {
return planSlug === activePlanSlug;
};
const getButtonText = (planSlug: string) => {
if (planSlug === "free") return "Get Started";
if (isCurrentPlan(planSlug)) {
return "Current Plan";
}
// User has an active paid subscription - show "Switch Plan"
if (activePlanSlug && activePlanSlug !== "free") {
return "Switch Plan";
}
// No subscription - show "Select Plan"
return "Select Plan";
};
const handlePlanClick = async (planSlug: string) => {
// Free plan - redirect to download
if (planSlug === "free") {
navigate("/download");
return;
}
// If current plan, do nothing
if (isCurrentPlan(planSlug)) {
return;
}
// Not logged in - redirect to signup
if (!user) {
navigate("/signup");
return;
}
// Set loading state for this plan
setProcessingPlan(planSlug);
try {
const cadence = isYearly ? "yearly" : "monthly";
const apiPlanSlug = PLAN_SLUG_MAP[planSlug] || planSlug;
// Determine if we need checkout (new subscription) or portal (switch plan)
const hasActiveSub = activePlanSlug && activePlanSlug !== "free";
const endpoint = hasActiveSub
? "/v1/stripe_artcraft/portal/switch_plan"
: "/v1/stripe_artcraft/checkout/subscription";
const apiHost =
StorytellerApiHostStore.getInstance().getApiSchemeAndHost();
const response = await fetch(`${apiHost}${endpoint}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
plan: apiPlanSlug,
cadence: cadence,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Redirect to Stripe
const redirectUrl =
data.stripe_checkout_redirect_url || data.stripe_portal_url;
if (redirectUrl) {
window.location.href = redirectUrl;
} else {
console.error("No redirect URL returned from API");
}
} catch (error) {
console.error("Error initiating checkout:", error);
} finally {
setProcessingPlan(null);
}
};
const formatPrice = (plan: SubscriptionPlanDetails) => {
if (plan.monthlyPrice === 0) {
return { current: 0, original: null };
}
if (isYearly) {
const val = Math.round(plan.yearlyPrice / 12);
const original = plan.originalYearlyPrice
? Math.round(plan.originalYearlyPrice / 12)
: null;
return { current: val, original };
} else {
const val = plan.originalMonthlyPrice || plan.monthlyPrice;
return { current: val, original: null };
}
};
// Determine grid columns based on number of plans
const gridCols =
plans.length === 3
? "grid-cols-1 md:grid-cols-3"
: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4";
return (
<div className={twMerge("w-full", className)}>
{showHeader && (
<div className={`text-center mx-auto ${compact ? "mb-6" : "mb-10"}`}>
<h2
className={`font-bold mb-4 ${compact ? "text-2xl md:text-3xl" : "text-3xl sm:text-4xl md:text-5xl"}`}
>
{title}
</h2>
<p
className={`text-white/70 leading-relaxed ${compact ? "text-base mb-4" : "text-xl mb-8"}`}
>
{subtitle}
</p>
</div>
)}
{/* Billing Toggle */}
<div
className={`flex items-center justify-center gap-4 relative w-fit mx-auto ${compact ? "mb-14" : "mb-14"}`}
>
<TabSelector
tabs={billingTabs}
activeTab={billingType}
onTabChange={setBillingType}
className="w-fit border border-white/20 rounded-lg bg-white/5"
tabClassName="w-24 text-md"
indicatorClassName="bg-primary/30 border border-primary"
selectedTabClassName="text-white"
/>
<span className="bg-primary text-white px-3 py-0.5 rounded-full text-sm font-medium -top-3 -right-10 md:-left-6 md:right-auto absolute pointer-events-none transform md:-rotate-12 rotate-12">
-20%
</span>
</div>
<div className={`max-w-7xl mx-auto grid ${gridCols} gap-4 md:gap-6`}>
{plans.map((plan) => {
const isPopular = plan.slug === "artcraft_pro";
const isCurrent = isCurrentPlan(plan.slug);
const { current: price, original: originalPrice } = formatPrice(plan);
const isProcessing = processingPlan === plan.slug;
return (
<div
key={plan.slug}
className={
getColorSchemeClasses(plan.colorScheme) +
(isPopular ? " transform md:-translate-y-4 shadow-2xl" : "") +
(isCurrent ? " ring-2 ring-white/50" : "")
}
>
{isPopular && !isCurrent && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary px-4 py-1 rounded-full text-sm font-bold shadow-lg whitespace-nowrap">
MOST POPULAR
</div>
)}
{isCurrent && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white text-black px-4 py-1 rounded-full text-sm font-bold shadow-lg whitespace-nowrap">
CURRENT PLAN
</div>
)}
<h3 className="text-xl md:text-2xl font-bold mb-2 text-white">
{plan.name}
</h3>
<div className="mb-1 flex items-baseline gap-2">
{originalPrice !== null && (
<span className="text-white/40 line-through text-lg md:text-xl decoration-white/40">
${originalPrice}
</span>
)}
<span className="text-3xl md:text-4xl font-bold">${price}</span>
<span className="text-white/60">/month</span>
</div>
<div className="text-xs text-white/40 mb-4 md:mb-6 uppercase tracking-wider font-semibold min-h-[1rem]">
{plan.monthlyPrice === 0
? "Free forever"
: isYearly
? "Billed yearly"
: "Billed monthly"}
</div>
<Button
className={`w-full justify-center border-transparent mb-6 md:mb-8 ${
isCurrent
? "bg-white/20 cursor-default"
: isPopular
? "bg-primary hover:bg-primary-600"
: "bg-white hover:bg-white/80 text-black"
}`}
onClick={() => handlePlanClick(plan.slug)}
disabled={isCurrent || isProcessing || isLoading}
>
{isProcessing ? (
<span className="flex items-center gap-2">
<svg
className="animate-spin h-4 w-4"
viewBox="0 0 24 24"
fill="none"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Processing...
</span>
) : (
getButtonText(plan.slug)
)}
</Button>
<ul className="space-y-3 md:space-y-4 flex-1">
{plan.features.map((feature, idx) => (
<Feature
key={idx}
text={feature.text}
highlighted={isPopular}
/>
))}
</ul>
</div>
);
})}
</div>
</div>
);
};
const Feature = ({
text,
highlighted = false,
}: {
text: string;
highlighted?: boolean;
}) => (
<li className="flex items-start gap-3">
<div
className={`mt-1 w-5 h-5 rounded-full flex items-center justify-center shrink-0 ${highlighted ? "bg-white/20 text-white" : "bg-white/10 text-white/70"}`}
>
<FontAwesomeIcon icon={faCheck} className="text-xs" />
</div>
<span className={`text-sm ${highlighted ? "text-white" : "text-white/80"}`}>
{text}
</span>
</li>
);
export default PricingTable;
@@ -0,0 +1,98 @@
import { faXmarkCircle, faArrowLeft } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@storyteller/ui-button";
import { Link } from "react-router-dom";
import Seo from "../../components/seo";
const CheckoutCancel = () => {
return (
<div className="relative min-h-screen overflow-hidden bg-[#101014] text-white">
<Seo
title="Checkout Cancelled - ArtCraft"
description="Your checkout was cancelled. No payment was made."
/>
{/* Background gradient - subtle/neutral */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
<div className="w-[900px] h-[900px] rounded-full bg-gradient-to-br from-gray-500/20 via-gray-600/10 to-gray-700/5 opacity-40 blur-[120px]"></div>
</div>
<main className="relative z-10 pt-28 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
{/* Cancel Card */}
<div className="max-w-lg w-full">
<div className="bg-[#1A1A1E] border border-white/10 rounded-3xl p-8 md:p-12 text-center">
{/* Cancel Icon */}
<div className="mb-6">
<div className="w-20 h-20 mx-auto rounded-full bg-white/10 flex items-center justify-center">
<FontAwesomeIcon
icon={faXmarkCircle}
className="text-5xl text-white/50"
/>
</div>
</div>
{/* Header */}
<h1 className="text-3xl md:text-4xl font-bold mb-4 text-white">
Checkout Cancelled
</h1>
<p className="text-lg text-white/60 mb-8 max-w-md mx-auto">
No worries! Your checkout was cancelled and no payment was made.
You can try again whenever you're ready.
</p>
{/* Info Box */}
<div className="bg-[#252529] rounded-2xl p-5 mb-8 text-left">
<p className="text-white/70 text-sm">
<span className="text-white font-medium">
Changed your mind?
</span>{" "}
No problem. You can return to the pricing page to complete your
purchase at any time.
</p>
</div>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
as="link"
href="/pricing"
className="bg-primary hover:bg-primary-600 px-8 py-3 text-sm font-bold rounded-xl justify-center"
>
View Plans Again
</Button>
<Button
as="link"
href="/"
className="bg-white/10 hover:bg-white/20 px-8 py-3 text-sm font-bold rounded-xl justify-center border-transparent"
>
<FontAwesomeIcon icon={faArrowLeft} />
Back to Home
</Button>
</div>
</div>
{/* Footer Links */}
<div className="text-center mt-8 flex justify-center gap-6">
<Link
to="/faq"
className="text-white/40 hover:text-white text-sm font-medium transition-colors"
>
FAQ
</Link>
<span className="text-white/20">•</span>
<a
href="https://discord.gg/artcraft"
target="_blank"
rel="noopener noreferrer"
className="text-white/40 hover:text-white text-sm font-medium transition-colors"
>
Get Help
</a>
</div>
</div>
</main>
</div>
);
};
export default CheckoutCancel;
@@ -0,0 +1,2 @@
export { default as CheckoutSuccess } from "./success";
export { default as CheckoutCancel } from "./cancel";
@@ -0,0 +1,149 @@
import {
faCheckCircle,
faDownload,
faDesktop,
faRocket,
faArrowLeft,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@storyteller/ui-button";
import { Link } from "react-router-dom";
import { isMobile, isMacOs } from "react-device-detect";
import { DOWNLOAD_LINKS } from "../../config/downloads";
import Seo from "../../components/seo";
const CheckoutSuccess = () => {
const downloadUrl = isMacOs ? DOWNLOAD_LINKS.MACOS : DOWNLOAD_LINKS.WINDOWS;
return (
<div className="relative min-h-screen overflow-hidden bg-[#101014] text-white">
<Seo
title="Payment Successful - ArtCraft"
description="Your ArtCraft subscription is now active. Download and start creating!"
/>
{/* Background gradient - celebratory green/purple */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
<div className="w-[900px] h-[900px] rounded-full bg-gradient-to-br from-green-500/40 via-primary/30 to-purple-600/20 opacity-40 blur-[120px]"></div>
</div>
<main className="relative z-10 pt-28 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
{/* Success Card */}
<div className="max-w-2xl w-full">
<div className="bg-[#1A1A1E] border border-white/10 rounded-3xl p-8 md:p-12 text-center">
{/* Success Icon */}
<div className="mb-6">
<div className="w-20 h-20 mx-auto rounded-full bg-green-500/20 flex items-center justify-center">
<FontAwesomeIcon
icon={faCheckCircle}
className="text-5xl text-green-400"
/>
</div>
</div>
{/* Header */}
<h1 className="text-3xl md:text-4xl font-bold mb-4 text-white">
Thank You for Your Support!
</h1>
<p className="text-lg text-white/70 mb-8 max-w-md mx-auto">
Your subscription is now active. Thank you for helping keep
ArtCraft open-source and free for everyone. You're now ready to
create amazing AI-generated art!
</p>
{/* Next Steps */}
<div className="bg-[#252529] rounded-2xl p-6 mb-8 text-left">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center">
<FontAwesomeIcon
icon={faRocket}
className="text-primary text-sm"
/>
</div>
<h2 className="text-lg font-bold text-white">Next Steps</h2>
</div>
<div className="space-y-3">
{[
"Download ArtCraft if you haven't already",
"Log in with your account",
"Your subscription is automatically activated",
"Start creating with all premium features!",
].map((step, idx) => (
<div key={idx} className="flex items-start gap-3">
<div className="w-6 h-6 shrink-0 rounded-full bg-primary/50 flex items-center justify-center text-xs font-bold text-white mt-0.5">
{idx + 1}
</div>
<span className="text-white/80">{step}</span>
</div>
))}
</div>
</div>
{/* CTA Buttons */}
{!isMobile ? (
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
as="link"
href={downloadUrl}
className="bg-primary hover:bg-primary-600 px-8 py-3 text-sm font-bold rounded-xl justify-center"
>
<FontAwesomeIcon icon={faDownload} className="mr-2" />
Download ArtCraft
</Button>
<Button
as="link"
href="/"
className="bg-white/10 hover:bg-white/20 px-8 py-3 text-sm font-bold rounded-xl justify-center border-transparent"
>
<FontAwesomeIcon icon={faArrowLeft} />
Back to Home
</Button>
</div>
) : (
<div className="bg-[#431407] border border-orange-900/50 rounded-2xl p-6 text-orange-200 text-sm leading-relaxed">
<div className="flex items-center justify-center mb-3 text-orange-400">
<FontAwesomeIcon icon={faDesktop} className="text-2xl" />
</div>
ArtCraft is a desktop application. <br />
Please head to your computer to download and start creating.
</div>
)}
{/* Download Links */}
{!isMobile && (
<div className="mt-6 flex justify-center gap-6 text-sm font-medium text-white/40">
<a
href={DOWNLOAD_LINKS.WINDOWS}
className="hover:text-white transition-colors flex items-center gap-2"
>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
Windows
</a>
<a
href={DOWNLOAD_LINKS.MACOS}
className="hover:text-white transition-colors flex items-center gap-2"
>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
Mac
</a>
</div>
)}
</div>
{/* Footer Link */}
<div className="text-center mt-8">
<Link
to="/pricing"
className="text-white/40 hover:text-white text-sm font-medium transition-colors"
>
View Plan Details
</Link>
</div>
</div>
</main>
</div>
);
};
export default CheckoutSuccess;
@@ -1,100 +1,8 @@
import { faCheck } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@storyteller/ui-button";
import Footer from "../../components/footer";
import {
SUBSCRIPTION_PLANS,
SubscriptionPlanDetails,
} from "@storyteller/subscription";
import { twMerge } from "tailwind-merge";
import Seo from "../../components/seo";
import { useState } from "react";
import { TabSelector } from "@storyteller/ui-tab-selector";
const billingTabs = [
{ id: "yearly", label: "Yearly" },
{ id: "monthly", label: "Monthly" },
];
import { PricingTable } from "../../components/pricing-table";
const Pricing = () => {
const [billingType, setBillingType] = useState("yearly");
const isYearly = billingType === "yearly";
const getColorSchemeClasses = (
colorScheme: SubscriptionPlanDetails["colorScheme"],
isCurrent: boolean,
) => {
const baseClasses =
"relative rounded-3xl p-8 border flex flex-col transition-all duration-300 backdrop-blur-md";
switch (colorScheme) {
case "dark":
return twMerge(
baseClasses,
"bg-[#1C1C20] border-white/10 hover:border-white/20",
);
case "green":
// Basic
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#002D23]/80 via-[#006B54]/50 to-[#00D28B]/10 border-[#00a873]/50",
"hover:border-[#00a873] hover:shadow-[0_0_30px_rgba(0,210,139,0.2)]",
);
case "purple":
// Pro
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#2D004D]/80 via-[#6400A8]/50 to-[#C03FFF]/10 border-[#9D4CFF]/50",
"hover:border-[#9D4CFF] hover:shadow-[0_0_30px_rgba(192,63,255,0.2)]",
);
case "orange":
// Max
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#332100]/80 via-[#B35C00]/50 to-[#FFB347]/10 border-[#FF8C00]/50",
"hover:border-[#FF8C00] hover:shadow-[0_0_30px_rgba(255,179,71,0.2)]",
);
default:
return twMerge(
baseClasses,
"bg-white/5 border-white/10 hover:border-white/20",
);
}
};
const getButtonText = (planSlug: string) => {
// Since this is the website, all buttons should probably lead to download or signup/login
// If we want to mirror the app, we can't really do "Switch Plan" because we don't know the logged in user's state easily here (unless we fetch it).
// For now, let's keep it simple: "Get Started" or "Start Free Trial"
if (planSlug === "free") return "Get Started";
return "Select Plan";
};
const getButtonHref = (planSlug: string) => {
if (planSlug === "free") return "/download";
return "/signup";
};
const formatPrice = (plan: SubscriptionPlanDetails) => {
if (plan.monthlyPrice === 0) {
return { current: 0, original: null };
}
if (isYearly) {
// Annual price divided by 12
const val = Math.round(plan.yearlyPrice / 12);
// Check if there's an original price to show savings
const original = plan.originalYearlyPrice
? Math.round(plan.originalYearlyPrice / 12)
: null;
return { current: val, original };
} else {
// Monthly price
const val = plan.originalMonthlyPrice || plan.monthlyPrice;
return { current: val, original: null };
}
};
return (
<div className="relative min-h-screen bg-[#101014] text-white bg-dots">
<Seo
@@ -107,94 +15,10 @@ const Pricing = () => {
</div>
<main className="relative z-10 pt-32 pb-20 px-4 sm:px-6 lg:px-8">
<div className="text-center max-w-3xl mx-auto mb-10">
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold mb-6">
Simple, Transparent Pricing
</h1>
<p className="text-xl text-white/70 leading-relaxed mb-8">
Start for free and scale as you grow. No hidden fees.
</p>
{/* Billing Toggle */}
<div className="flex items-center justify-center gap-4 mb-8 relative w-fit mx-auto">
<TabSelector
tabs={billingTabs}
activeTab={billingType}
onTabChange={setBillingType}
className="w-fit border border-white/20 rounded-lg bg-white/5"
tabClassName="w-24 text-md"
indicatorClassName="bg-primary/30 border border-primary"
selectedTabClassName="text-white"
/>
<span className="bg-primary text-white px-3 py-0.5 rounded-full text-sm font-medium -top-3 -right-10 md:-left-6 md:right-auto absolute pointer-events-none transform md:-rotate-12 rotate-12">
-20%
</span>
</div>
</div>
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{SUBSCRIPTION_PLANS.map((plan) => {
const isPopular = plan.slug === "artcraft_pro";
const { current: price, original: originalPrice } =
formatPrice(plan);
return (
<div
key={plan.slug}
className={
getColorSchemeClasses(plan.colorScheme, false) +
(isPopular ? " transform md:-translate-y-4 shadow-2xl" : "")
}
>
{isPopular && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-primary px-4 py-1 rounded-full text-sm font-bold shadow-lg whitespace-nowrap">
MOST POPULAR
</div>
)}
<h3
className={`text-2xl font-bold mb-2 ${isPopular ? "text-primary" : "text-white"}`}
>
{plan.name}
</h3>
<div className="mb-1 flex items-baseline gap-2">
{originalPrice !== null && (
<span className="text-white/40 line-through text-xl decoration-white/40">
${originalPrice}
</span>
)}
<span className="text-4xl font-bold">${price}</span>
<span className="text-white/60">/month</span>
</div>
<div className="text-xs text-white/40 mb-6 uppercase tracking-wider font-semibold min-h-[1rem]">
{plan.monthlyPrice === 0
? "Free forever"
: isYearly
? "Billed yearly"
: "Billed monthly"}
</div>
<Button
className={`w-full justify-center border-transparent mb-8 ${isPopular ? "bg-primary hover:bg-primary-600" : "bg-white/10 hover:bg-white/20"}`}
as="link"
href={getButtonHref(plan.slug)}
>
{getButtonText(plan.slug)}
</Button>
<ul className="space-y-4 flex-1">
{plan.features.map((feature, idx) => (
<Feature
key={idx}
text={feature.text}
highlighted={isPopular}
/>
))}
</ul>
</div>
);
})}
</div>
<PricingTable
title="Simple, Transparent Pricing"
subtitle="Support open-source development. Your subscription keeps ArtCraft free and open for everyone."
/>
</main>
<Footer />
@@ -202,23 +26,4 @@ const Pricing = () => {
);
};
const Feature = ({
text,
highlighted = false,
}: {
text: string;
highlighted?: boolean;
}) => (
<li className="flex items-start gap-3">
<div
className={`mt-1 w-5 h-5 rounded-full flex items-center justify-center shrink-0 ${highlighted ? "bg-primary/20 text-primary" : "bg-white/10 text-white/50"}`}
>
<FontAwesomeIcon icon={faCheck} className="text-xs" />
</div>
<span className={`text-sm ${highlighted ? "text-white" : "text-white/70"}`}>
{text}
</span>
</li>
);
export default Pricing;
@@ -2,6 +2,7 @@ import {
faCheckCircle,
faDownload,
faDesktop,
faRocket,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@storyteller/ui-button";
@@ -9,103 +10,124 @@ import { Link } from "react-router-dom";
import { isMobile, isMacOs } from "react-device-detect";
import { DOWNLOAD_LINKS } from "../../config/downloads";
import Seo from "../../components/seo";
import { PricingTable } from "../../components/pricing-table";
const Welcome = () => {
const downloadUrl = isMacOs ? DOWNLOAD_LINKS.MACOS : DOWNLOAD_LINKS.WINDOWS;
return (
<div className="relative min-h-[calc(100vh-1px)] pt-20 md:pt-0 overflow-hidden bg-[#101014] text-white flex flex-col items-center justify-center p-4">
<Seo title="Welcome - ArtCraft" description="Welcome to ArtCraft." />
<main className="relative z-10 w-full max-w-2xl">
<div className="bg-[#28282C] rounded-[32px] overflow-hidden shadow-2xl border border-white/5">
<div className="p-8 pt-10 text-center flex flex-col items-center">
<h1 className="text-4xl font-bold mb-4 text-white tracking-tight gap-4 flex items-center">
<FontAwesomeIcon
icon={faCheckCircle}
className="text-4xl bg-[#28282C] text-primary rounded-full"
/>
<div>
Welcome to <span className="text-primary">ArtCraft</span>
</div>
</h1>
<div className="relative min-h-screen overflow-hidden bg-[#101014] text-white">
<Seo
title="Welcome - ArtCraft"
description="Welcome to ArtCraft. Get started with your subscription."
/>
<p className="text-lg text-white/70 mb-8 max-w-xl mx-auto leading-relaxed font-medium">
Your account is ready. Let's get you set up to create.
</p>
{/* Background gradient */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
<div className="w-[900px] h-[900px] rounded-full bg-gradient-to-br from-primary/40 via-purple-600/30 to-blue-500/20 opacity-30 blur-[120px]"></div>
</div>
<div className="w-full mb-8">
{/* Information Column */}
<div className="bg-[#1E1E22] border border-white/5 rounded-3xl p-6">
<div className="flex flex-col items-center text-center mb-6">
<h3 className="font-bold text-white text-xl">Next Steps</h3>
</div>
<main className="relative z-10 pt-28 pb-20 px-4 sm:px-6 lg:px-8">
{/* Success Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center gap-3 bg-primary/10 border border-primary/30 rounded-full px-6 py-3 mb-6">
<FontAwesomeIcon
icon={faCheckCircle}
className="text-xl text-primary"
/>
<span className="text-white font-medium">
Account created successfully!
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-left">
<div className="bg-[#252529] p-3 rounded-xl flex items-center gap-3">
<div className="w-7 h-7 shrink-0 rounded-full bg-primary/60 flex items-center justify-center text-xs font-bold text-white/80">
1
</div>
<div className="text-white/80 text-sm font-medium">
Open the installer
</div>
</div>
<div className="bg-[#252529] p-3 rounded-xl flex items-center gap-3">
<div className="w-7 h-7 shrink-0 rounded-full bg-primary/60 flex items-center justify-center text-xs font-bold text-white/80">
2
</div>
<div className="text-white/80 text-sm font-medium">
Follow setup instructions
</div>
</div>
<div className="bg-[#252529] p-3 rounded-xl flex items-center gap-3">
<div className="w-7 h-7 shrink-0 rounded-full bg-primary/60 flex items-center justify-center text-xs font-bold text-white/80">
3
</div>
<div className="text-white/80 text-sm font-medium">
Log in to account
</div>
</div>
<div className="bg-[#252529] p-3 rounded-xl flex items-center gap-3">
<div className="w-7 h-7 shrink-0 rounded-full bg-primary/60 flex items-center justify-center text-xs font-bold text-white/80">
4
</div>
<div className="text-white/80 text-sm font-medium">
Start creating!
</div>
</div>
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold mb-4 tracking-tight">
Welcome to <span className="text-primary">ArtCraft</span>
</h1>
<p className="text-xl text-white/70 max-w-2xl mx-auto">
Support open-source development and unlock premium AI art features.
Your subscription keeps ArtCraft free for everyone.
</p>
</div>
{/* Pricing Table */}
<div className="mb-16">
<PricingTable
showHeader={false}
compact={true}
className="max-w-5xl mx-auto"
/>
</div>
{/* Next Steps Section */}
<div className="max-w-3xl mx-auto">
<div className="bg-[#1A1A1E] border border-white/10 rounded-3xl p-8 md:p-10">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center">
<FontAwesomeIcon
icon={faRocket}
className="text-primary text-lg"
/>
</div>
<h2 className="text-2xl font-bold text-white">Getting Started</h2>
</div>
{/* Contingency / Action Area */}
{!isMobile && (
<div className="flex flex-col items-center pt-6 border-t border-white/5">
<p className="text-white/40 text-sm mb-4 font-medium uppercase tracking-wider">
Download didn't start?
</p>
<Button
as="link"
href={downloadUrl}
className="bg-white text-black hover:bg-gray-100 dark:bg-white dark:text-black dark:hover:bg-gray-200 px-10 py-3 text-sm font-bold rounded-xl"
<p className="text-white/60 mb-6">
Your download should have started automatically. Follow these
steps to begin creating:
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
{[
{ step: 1, text: "Open the installer" },
{ step: 2, text: "Follow setup instructions" },
{ step: 3, text: "Log in to your account" },
{ step: 4, text: "Start creating!" },
].map((item) => (
<div
key={item.step}
className="bg-[#252529] p-4 rounded-xl flex items-center gap-4"
>
<FontAwesomeIcon icon={faDownload} className="mr-2" />
Download for {isMacOs ? "Mac" : "Windows"}
</Button>
<div className="mt-5 flex gap-6 text-sm font-medium text-white/30">
<a
href={DOWNLOAD_LINKS.WINDOWS}
className="hover:text-white transition-colors flex items-center gap-2"
>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
Windows .exe
</a>
<a
href={DOWNLOAD_LINKS.MACOS}
className="hover:text-white transition-colors flex items-center gap-2"
>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
Mac .dmg
</a>
<div className="w-8 h-8 shrink-0 rounded-full bg-primary/60 flex items-center justify-center text-sm font-bold text-white">
{item.step}
</div>
<div className="text-white/90 font-medium">{item.text}</div>
</div>
))}
</div>
{/* Download Contingency */}
{!isMobile && (
<div className="pt-6 border-t border-white/10">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-white/50 text-sm">
Download didn't start?
</span>
<div className="flex flex-wrap items-center gap-4">
<Button
as="link"
href={downloadUrl}
className="bg-white text-black hover:bg-gray-100 dark:bg-white dark:text-black dark:hover:bg-gray-200 px-6 py-2.5 text-sm font-bold rounded-xl"
>
<FontAwesomeIcon icon={faDownload} className="mr-2" />
Download for {isMacOs ? "Mac" : "Windows"}
</Button>
<div className="flex gap-4 text-sm font-medium text-white/30">
<a
href={DOWNLOAD_LINKS.WINDOWS}
className="hover:text-white transition-colors flex items-center gap-2"
>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
Windows
</a>
<a
href={DOWNLOAD_LINKS.MACOS}
className="hover:text-white transition-colors flex items-center gap-2"
>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
Mac
</a>
</div>
</div>
</div>
</div>
)}
@@ -121,8 +143,8 @@ const Welcome = () => {
)}
</div>
{/* Footer of Card */}
<div className="bg-[#1F1F22] p-4 text-center border-t border-white/5">
{/* Footer Link */}
<div className="text-center mt-8">
<Link
to="/"
className="text-white/40 hover:text-white text-sm font-medium transition-colors"
+20 -1
View File
@@ -3,8 +3,27 @@
@tailwind utilities;
@layer base {
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Outfit", sans-serif;
}
body {
font-family: "Poppins", sans-serif;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
color: #ffffff;
background-color: #1b1b1f;
}
@@ -37,6 +37,12 @@
"src/components/footer"
],
"references": [
{
"path": "../../libs/components/tab-selector/tsconfig.lib.json"
},
{
"path": "../../libs/state/subscription/tsconfig.lib.json"
},
{
"path": "../../libs/model-list/tsconfig.lib.json"
},
@@ -52,14 +58,14 @@
{
"path": "../../libs/components/loading-spinner/tsconfig.lib.json"
},
{
"path": "../../libs/markdown-content/tsconfig.lib.json"
},
{
"path": "../../libs/api/tsconfig.lib.json"
},
{
"path": "../../libs/components/button/tsconfig.lib.json"
},
{
"path": "../../libs/markdown-content/tsconfig.lib.json"
}
]
}
+9 -3
View File
@@ -2,6 +2,12 @@
"files": [],
"include": [],
"references": [
{
"path": "../../libs/components/tab-selector"
},
{
"path": "../../libs/state/subscription"
},
{
"path": "../../libs/model-list"
},
@@ -17,15 +23,15 @@
{
"path": "../../libs/components/loading-spinner"
},
{
"path": "../../libs/markdown-content"
},
{
"path": "../../libs/api"
},
{
"path": "../../libs/components/button"
},
{
"path": "../../libs/markdown-content"
},
{
"path": "./tsconfig.app.json"
},
@@ -1,5 +1,6 @@
export enum SubscriptionNamespace {
FAKEYOU = "fakeyou",
ARTCRAFT = "artcraft",
}
export enum SubscriptionProduct {
PLUS = "fakeyou_plus",
@@ -1,5 +1,6 @@
export enum SubscriptionNamespace {
FAKEYOU = "fakeyou",
ARTCRAFT = "artcraft",
}
export enum SubscriptionProduct {
PLUS = "fakeyou_plus",