From 1469b4fcff85979dd41ea4b2cb15d5f335c64884 Mon Sep 17 00:00:00 2001 From: BFlat Date: Tue, 27 Jan 2026 17:34:34 +0700 Subject: [PATCH] Feature/flow update (#1007) - added login / sign up and pricing flow - download page in nav and signup and pricing links/buttons currently missing features: - forget password function - google social login --------- Co-authored-by: Brandon Thomas --- frontend/apps/artcraft-website/index.html | 28 +- .../apps/artcraft-website/src/app/app.tsx | 27 +- .../src/components/auth/index.ts | 1 + .../src/components/auth/signup-form.tsx | 153 +++++++ .../discord-button/discord-button.tsx | 2 +- .../src/components/download-modal.tsx | 153 +++++++ .../artcraft-website/src/components/index.ts | 1 + .../src/components/navbar/navbar.tsx | 166 ++++++- .../src/components/pricing-table/index.ts | 1 + .../pricing-table/pricing-table.tsx | 412 ++++++++++++++++++ .../src/pages/checkout/cancel.tsx | 98 +++++ .../src/pages/checkout/index.ts | 2 + .../src/pages/checkout/success.tsx | 149 +++++++ .../src/pages/download/download.tsx | 5 + .../pages/forgot-password/forgot-password.tsx | 85 ++++ .../src/pages/forgot-password/index.ts | 2 + .../src/pages/landing2/landing2.tsx | 96 +++- .../artcraft-website/src/pages/login/index.ts | 2 + .../src/pages/login/login.tsx | 154 +++++++ .../src/pages/media/media.tsx | 202 ++++++--- .../src/pages/news/news-index.tsx | 16 + .../src/pages/news/news-post.tsx | 25 ++ .../src/pages/pricing/index.ts | 2 + .../src/pages/pricing/pricing.tsx | 33 ++ .../src/pages/signup/index.ts | 2 + .../src/pages/signup/signup.tsx | 47 ++ .../src/pages/welcome/index.ts | 2 + .../src/pages/welcome/welcome.tsx | 161 +++++++ frontend/apps/artcraft-website/src/styles.css | 21 +- .../apps/artcraft-website/tailwind.config.js | 3 +- .../apps/artcraft-website/tsconfig.app.json | 12 +- frontend/apps/artcraft-website/tsconfig.json | 12 +- frontend/apps/artcraft-website/vite.config.ts | 3 + .../apps/artcraft/app/src/enums/Billing.ts | 1 + frontend/libs/api/src/lib/UsersApi.ts | 18 +- frontend/libs/api/src/lib/enums/Billing.ts | 1 + .../src/lib/subscription-plans.ts | 24 +- 37 files changed, 1970 insertions(+), 152 deletions(-) create mode 100644 frontend/apps/artcraft-website/src/components/auth/index.ts create mode 100644 frontend/apps/artcraft-website/src/components/auth/signup-form.tsx create mode 100644 frontend/apps/artcraft-website/src/components/download-modal.tsx create mode 100644 frontend/apps/artcraft-website/src/components/pricing-table/index.ts create mode 100644 frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/checkout/cancel.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/checkout/index.ts create mode 100644 frontend/apps/artcraft-website/src/pages/checkout/success.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/forgot-password/forgot-password.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/forgot-password/index.ts create mode 100644 frontend/apps/artcraft-website/src/pages/login/index.ts create mode 100644 frontend/apps/artcraft-website/src/pages/login/login.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/news/news-index.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/news/news-post.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/pricing/index.ts create mode 100644 frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/signup/index.ts create mode 100644 frontend/apps/artcraft-website/src/pages/signup/signup.tsx create mode 100644 frontend/apps/artcraft-website/src/pages/welcome/index.ts create mode 100644 frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx diff --git a/frontend/apps/artcraft-website/index.html b/frontend/apps/artcraft-website/index.html index df3be4cbd5..70e0dff208 100644 --- a/frontend/apps/artcraft-website/index.html +++ b/frontend/apps/artcraft-website/index.html @@ -1,19 +1,30 @@ - + - + ArtCraft. AI Video and Images. Fast and Open Desktop App. - - + + @@ -23,10 +34,9 @@ -
diff --git a/frontend/apps/artcraft-website/src/app/app.tsx b/frontend/apps/artcraft-website/src/app/app.tsx index d4517b1c4d..dafdede0d9 100644 --- a/frontend/apps/artcraft-website/src/app/app.tsx +++ b/frontend/apps/artcraft-website/src/app/app.tsx @@ -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"; @@ -8,7 +8,14 @@ import TutorialsPage from "../pages/tutorials"; import TutorialsArticle from "../pages/tutorials/article"; import FaqIndex from "../pages/faq/index"; import FaqArticle from "../pages/faq/article"; -import { NewsIndex, NewsPost } from "@storyteller/markdown-content"; +import NewsIndex from "../pages/news/news-index"; +import NewsPost from "../pages/news/news-post"; +import Pricing from "../pages/pricing"; +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 ( @@ -27,6 +34,22 @@ export function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Redirects for underscore-based URLs (legacy Stripe config) */} + } + /> + } + /> ); diff --git a/frontend/apps/artcraft-website/src/components/auth/index.ts b/frontend/apps/artcraft-website/src/components/auth/index.ts new file mode 100644 index 0000000000..6ad600f9af --- /dev/null +++ b/frontend/apps/artcraft-website/src/components/auth/index.ts @@ -0,0 +1 @@ +export * from "./signup-form"; diff --git a/frontend/apps/artcraft-website/src/components/auth/signup-form.tsx b/frontend/apps/artcraft-website/src/components/auth/signup-form.tsx new file mode 100644 index 0000000000..1a06c7b523 --- /dev/null +++ b/frontend/apps/artcraft-website/src/components/auth/signup-form.tsx @@ -0,0 +1,153 @@ +import { faGoogle } from "@fortawesome/free-brands-svg-icons"; +import { faEye, faEyeSlash, faSpinner } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Button } from "@storyteller/ui-button"; +import { useState } from "react"; +import { UsersApi } from "@storyteller/api"; + +interface SignupFormProps { + onSuccess: () => void; + signupSource: string; + className?: string; + showGoogleButton?: boolean; + autoFocus?: boolean; +} + +export const SignupForm = ({ + onSuccess, + signupSource, + className = "", + showGoogleButton = true, + autoFocus = false, +}: SignupFormProps) => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSignup = async () => { + setError(null); + + if (!email || !password) { + setError("All fields are required"); + return; + } + + if (password.length < 8) { + setError("Password must be at least 8 characters"); + return; + } + + setIsLoading(true); + + const api = new UsersApi(); + + // Generate a username from the email + const emailPrefix = email.split("@")[0].replace(/[^a-zA-Z0-9]/g, ""); + // Ensure we don't exceed max length (7 chars from email + 3 digits = 10 total) + const truncatedPrefix = emailPrefix.substring(0, 7); + const randomSuffix = Math.floor(Math.random() * 900) + 100; // 100-999 + const generatedUsername = `${truncatedPrefix}${randomSuffix}`; + + const response = await api.Signup({ + username: generatedUsername, + email, + password, + passwordConfirmation: password, + signupSource, + }); + + setIsLoading(false); + + if (response.success) { + window.dispatchEvent(new Event("auth-change")); + onSuccess(); + } else { + setError(response.errorMessage || "Failed to create account"); + } + }; + + return ( +
+ {showGoogleButton && ( + <> + + +
+
+
+
+ + or + +
+ + )} + + {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + placeholder="you@example.com" + autoFocus={autoFocus} + className="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors" + onKeyDown={(e) => e.key === "Enter" && handleSignup()} + /> +
+ +
+ +
+ setPassword(e.target.value)} + placeholder="Min. 8 characters" + className="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12" + onKeyDown={(e) => e.key === "Enter" && handleSignup()} + /> + +
+
+ + +
+ ); +}; diff --git a/frontend/apps/artcraft-website/src/components/discord-button/discord-button.tsx b/frontend/apps/artcraft-website/src/components/discord-button/discord-button.tsx index cb1d49564f..c75a791511 100644 --- a/frontend/apps/artcraft-website/src/components/discord-button/discord-button.tsx +++ b/frontend/apps/artcraft-website/src/components/discord-button/discord-button.tsx @@ -17,7 +17,7 @@ export const DiscordButton = ({ return ( + + {view === "signup" && ( + + )} + +
+
+ + + +
+

+ Downloading ArtCraft... +

+

+ Your download has started. +
+ While you wait, create an account to store your creations. +

+
+ + {view === "menu" ? ( +
+ + + +
+ ) : ( + { + onClose(); + navigate("/welcome"); + }} + signupSource="artcraft" + showGoogleButton={false} + autoFocus={true} + /> + )} + +
+ +
+ + , + document.body, + ); +}; diff --git a/frontend/apps/artcraft-website/src/components/index.ts b/frontend/apps/artcraft-website/src/components/index.ts index c152451d62..16ad97296b 100644 --- a/frontend/apps/artcraft-website/src/components/index.ts +++ b/frontend/apps/artcraft-website/src/components/index.ts @@ -1,3 +1,4 @@ export * from "./navbar"; export * from "./footer"; export * from "./download-button"; +export * from "./pricing-table"; diff --git a/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx b/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx index afb56eeb4c..51ec92d45c 100644 --- a/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx +++ b/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx @@ -1,8 +1,16 @@ -import { Disclosure } from "@headlessui/react"; +import { + Disclosure, + Menu, + MenuButton, + MenuItem, + MenuItems, + Transition, +} from "@headlessui/react"; import { twMerge } from "tailwind-merge"; -import { useEffect, useState } from "react"; +import { useEffect, useState, Fragment } from "react"; import { Link, useLocation } from "react-router-dom"; -import { DiscordButton } from "../discord-button"; +import { Button } from "@storyteller/ui-button"; +import { UsersApi, UserInfo } from "@storyteller/api"; const NAV_ITEMS = [ { name: "Home", href: "/" }, @@ -10,12 +18,51 @@ const NAV_ITEMS = [ { name: "News", href: "/news" }, { name: "FAQ", href: "/faq" }, { name: "Press Kit", href: "/press-kit" }, + { name: "Download", href: "/download" }, ]; export default function Navbar() { const [scrolled, setScrolled] = useState(false); const location = useLocation(); - // Underline hover handled purely via CSS + const [user, setUser] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + + // Check session on mount + // Check session on mount and when auth changes or location changes + useEffect(() => { + const checkSession = async () => { + const api = new UsersApi(); + const response = await api.GetSession(); + if ( + response.success && + response.data && + response.data.loggedIn && + response.data.user + ) { + setUser(response.data.user); + } else { + setUser(undefined); + } + setIsLoading(false); + }; + + checkSession(); + + const handleAuthChange = () => { + setIsLoading(true); + checkSession(); + }; + + window.addEventListener("auth-change", handleAuthChange); + return () => window.removeEventListener("auth-change", handleAuthChange); + }, [location.pathname]); + + const handleLogout = async () => { + const api = new UsersApi(); + await api.Logout(); + // Reload to clear state/cache + window.location.href = "/"; + }; useEffect(() => { const handleScroll = () => { @@ -89,21 +136,102 @@ export default function Navbar() {
-
- {/* */} - -
-
- {/* Mobile menu button */} - + {isLoading ? ( + // Loading placeholder +
+ ) : user ? ( + // Logged In State +
+ + Pricing + + + +
+ + Open user menu + + +
+ + +
+

+ {user.display_name || user.username} +

+
+ + {({ active }) => ( + + )} + +
+
+
+
+ ) : ( + // Logged Out State +
+ + Pricing + + +
+ )} + + {/* Mobile Menu Button - simplified for now */} +
+ {!user && ( + + )} + {user && ( + + )}
diff --git a/frontend/apps/artcraft-website/src/components/pricing-table/index.ts b/frontend/apps/artcraft-website/src/components/pricing-table/index.ts new file mode 100644 index 0000000000..3dfb9818d7 --- /dev/null +++ b/frontend/apps/artcraft-website/src/components/pricing-table/index.ts @@ -0,0 +1 @@ +export { default as PricingTable } from "./pricing-table"; diff --git a/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx b/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx new file mode 100644 index 0000000000..8550c1a360 --- /dev/null +++ b/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx @@ -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 = { + 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(undefined); + const [activePlanSlug, setActivePlanSlug] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [processingPlan, setProcessingPlan] = useState(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 ( +
+ {showHeader && ( +
+

+ {title} +

+

+ {subtitle} +

+
+ )} + + {/* Billing Toggle */} +
+ + + -20% + +
+ +
+ {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 ( +
+ {isPopular && !isCurrent && ( +
+ MOST POPULAR +
+ )} + + {isCurrent && ( +
+ CURRENT PLAN +
+ )} + +

+ {plan.name} +

+
+ {originalPrice !== null && ( + + ${originalPrice} + + )} + ${price} + /month +
+
+ {plan.monthlyPrice === 0 + ? "Free forever" + : isYearly + ? "Billed yearly" + : "Billed monthly"} +
+ + + +
    + {plan.features.map((feature, idx) => ( + + ))} +
+
+ ); + })} +
+
+ ); +}; + +const Feature = ({ + text, + highlighted = false, +}: { + text: string; + highlighted?: boolean; +}) => ( +
  • +
    + +
    + + {text} + +
  • +); + +export default PricingTable; diff --git a/frontend/apps/artcraft-website/src/pages/checkout/cancel.tsx b/frontend/apps/artcraft-website/src/pages/checkout/cancel.tsx new file mode 100644 index 0000000000..d68b2ee601 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/checkout/cancel.tsx @@ -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 ( +
    + + + {/* Background gradient - subtle/neutral */} +
    +
    +
    + +
    + {/* Cancel Card */} +
    +
    + {/* Cancel Icon */} +
    +
    + +
    +
    + + {/* Header */} +

    + Checkout Cancelled +

    +

    + No worries! Your checkout was cancelled and no payment was made. + You can try again whenever you're ready. +

    + + {/* Info Box */} +
    +

    + + Changed your mind? + {" "} + No problem. You can return to the pricing page to complete your + purchase at any time. +

    +
    + + {/* CTA Buttons */} +
    + + +
    +
    + + {/* Footer Links */} +
    + + FAQ + + • + + Get Help + +
    +
    +
    +
    + ); +}; + +export default CheckoutCancel; diff --git a/frontend/apps/artcraft-website/src/pages/checkout/index.ts b/frontend/apps/artcraft-website/src/pages/checkout/index.ts new file mode 100644 index 0000000000..6cfdec89f8 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/checkout/index.ts @@ -0,0 +1,2 @@ +export { default as CheckoutSuccess } from "./success"; +export { default as CheckoutCancel } from "./cancel"; diff --git a/frontend/apps/artcraft-website/src/pages/checkout/success.tsx b/frontend/apps/artcraft-website/src/pages/checkout/success.tsx new file mode 100644 index 0000000000..8da500f28c --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/checkout/success.tsx @@ -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 ( +
    + + + {/* Background gradient - celebratory green/purple */} +
    +
    +
    + +
    + {/* Success Card */} +
    +
    + {/* Success Icon */} +
    +
    + +
    +
    + + {/* Header */} +

    + Thank You for Your Support! +

    +

    + 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! +

    + + {/* Next Steps */} +
    +
    +
    + +
    +

    Next Steps

    +
    + +
    + {[ + "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) => ( +
    +
    + {idx + 1} +
    + {step} +
    + ))} +
    +
    + + {/* CTA Buttons */} + {!isMobile ? ( +
    + + +
    + ) : ( +
    +
    + +
    + ArtCraft is a desktop application.
    + Please head to your computer to download and start creating. +
    + )} + + {/* Download Links */} + {!isMobile && ( + + )} +
    + + {/* Footer Link */} +
    + + View Plan Details + +
    +
    +
    +
    + ); +}; + +export default CheckoutSuccess; diff --git a/frontend/apps/artcraft-website/src/pages/download/download.tsx b/frontend/apps/artcraft-website/src/pages/download/download.tsx index ddda8d8590..366d4665ac 100644 --- a/frontend/apps/artcraft-website/src/pages/download/download.tsx +++ b/frontend/apps/artcraft-website/src/pages/download/download.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button } from "@storyteller/ui-button"; import { DOWNLOAD_LINKS } from "../../config/downloads"; import { isMobile } from "react-device-detect"; +import Seo from "../../components/seo"; const Download = () => { const systemRequirements = [ @@ -29,6 +30,10 @@ const Download = () => { return (
    +
    diff --git a/frontend/apps/artcraft-website/src/pages/forgot-password/forgot-password.tsx b/frontend/apps/artcraft-website/src/pages/forgot-password/forgot-password.tsx new file mode 100644 index 0000000000..1c116d90dd --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/forgot-password/forgot-password.tsx @@ -0,0 +1,85 @@ +import { faArrowLeft, faEnvelope } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Button } from "@storyteller/ui-button"; +import { useState } from "react"; +import { Link } from "react-router-dom"; + +import Seo from "../../components/seo"; + +const ForgotPassword = () => { + const [email, setEmail] = useState(""); + const [submitted, setSubmitted] = useState(false); + + return ( +
    + +
    + +
    +
    +
    +

    Reset Password

    +

    + Enter your email to receive reset instructions +

    +
    + + {!submitted ? ( +
    +
    + + setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors" + /> +
    + + +
    + ) : ( +
    +
    + +
    +

    Check your email

    +

    + We've sent a password reset link to
    + {email} +

    + +
    + )} + +
    + + Back to Log in + +
    +
    +
    +
    + ); +}; + +export default ForgotPassword; diff --git a/frontend/apps/artcraft-website/src/pages/forgot-password/index.ts b/frontend/apps/artcraft-website/src/pages/forgot-password/index.ts new file mode 100644 index 0000000000..6fa87516c8 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/forgot-password/index.ts @@ -0,0 +1,2 @@ +import ForgotPassword from "./forgot-password"; +export default ForgotPassword; diff --git a/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx b/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx index 920a782730..5d2836a2cc 100644 --- a/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx +++ b/frontend/apps/artcraft-website/src/pages/landing2/landing2.tsx @@ -1,6 +1,6 @@ import { DiscordButton } from "../../components/discord-button"; import { Button } from "@storyteller/ui-button"; -import { isMobile } from "react-device-detect"; +import { isMobile, isMacOs } from "react-device-detect"; import { faWindows, faApple, faGithub } from "@fortawesome/free-brands-svg-icons"; import { faVolumeMute, @@ -25,6 +25,8 @@ import ModelBadgeGrid from "../../components/model-badge-grid"; import Seo from "../../components/seo"; import { DOWNLOAD_LINKS } from "../../config/downloads"; import { OwnershipComparison } from "../../components/ownership-comparison/ownership-comparison"; +import { DownloadModal } from "../../components/download-modal"; +import { UsersApi } from "@storyteller/api"; gsap.registerPlugin(ScrollTrigger); @@ -52,8 +54,33 @@ const Landing = () => { const [isHovering, setIsHovering] = useState(false); const [activeFeature, setActiveFeature] = useState(0); const [activeVideo, setActiveVideo] = useState(null); + const [showDownloadModal, setShowDownloadModal] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(false); const videoRef = useRef(null); + const downloadUrl = isMacOs ? MAC_LINK : WINDOWS_LINK; + + useEffect(() => { + const checkSession = async () => { + const api = new UsersApi(); + const response = await api.GetSession(); + if (response.success && response.data?.loggedIn) { + setIsLoggedIn(true); + } + }; + checkSession(); + }, []); + + const onDownloadClick = (e: React.MouseEvent) => { + if (isLoggedIn) { + return; + } + + setShowDownloadModal(true); + + localStorage.setItem("artcraft_download_initiated", "true"); + }; + const features = [ { icon: faMapMarkerAlt, @@ -263,26 +290,17 @@ const Landing = () => { Download on a desktop ) : ( - <> - - - + )} - Try Free + Try Free
    {/* Glassy Video Mockup */} @@ -773,13 +791,13 @@ const Landing = () => {
    {/* Discord CTA Section */}
    -

    +

    Join the Discord

    -

    +

    Connect with other creators, share your work, get feedback, and stay updated.

    @@ -807,9 +825,41 @@ const Landing = () => { Star on GitHub
    + + + {/* Download CTA Section - New */} +
    +
    +
    + +

    + Ready to Create? +

    +

    + Join thousands of artists and filmmakers using ArtCraft to bring their vision to life. +

    + +
    + +
    Free to use.
    +
    Unlimited Nano Banana Pro for a limited time!
    +
    +
    + setShowDownloadModal(false)} />
    ); diff --git a/frontend/apps/artcraft-website/src/pages/login/index.ts b/frontend/apps/artcraft-website/src/pages/login/index.ts new file mode 100644 index 0000000000..e5d0293db8 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/login/index.ts @@ -0,0 +1,2 @@ +import LogIn from "./login"; +export default LogIn; diff --git a/frontend/apps/artcraft-website/src/pages/login/login.tsx b/frontend/apps/artcraft-website/src/pages/login/login.tsx new file mode 100644 index 0000000000..e7af32386e --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/login/login.tsx @@ -0,0 +1,154 @@ +import { faGoogle } from "@fortawesome/free-brands-svg-icons"; +import { faEye, faEyeSlash, faSpinner } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Button } from "@storyteller/ui-button"; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { UsersApi } from "@storyteller/api"; +import Seo from "../../components/seo"; + +const Login = () => { + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleLogin = async () => { + setError(null); + setIsLoading(true); + + const api = new UsersApi(); + const response = await api.Login({ + usernameOrEmail: email, + password: password, + }); + + setIsLoading(false); + + if (response.success) { + window.dispatchEvent(new Event("auth-change")); + navigate("/"); + } else { + setError(response.errorMessage || "Invalid credentials"); + } + }; + + return ( +
    + +
    + +
    +
    +
    +

    Welcome Back

    +

    Log in to your account

    +
    + +
    + + +
    +
    +
    +
    + + or + +
    + +
    + {error && ( +
    + {error} +
    + )} + +
    + + setEmail(e.target.value)} + placeholder="you@example.com or username" + className="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors" + /> +
    +
    +
    + + + Forgot password? + +
    +
    + setPassword(e.target.value)} + placeholder="Min. 8 characters" + className="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12" + /> + +
    +
    + + +
    +
    + +
    + Don't have an account?{" "} + + Sign up + +
    +
    +
    + +
    + © {new Date().getFullYear()} ArtCraft. All rights reserved. +
    +
    + ); +}; + +export default Login; diff --git a/frontend/apps/artcraft-website/src/pages/media/media.tsx b/frontend/apps/artcraft-website/src/pages/media/media.tsx index 764ccadc71..4ce95f2b39 100644 --- a/frontend/apps/artcraft-website/src/pages/media/media.tsx +++ b/frontend/apps/artcraft-website/src/pages/media/media.tsx @@ -30,14 +30,15 @@ import { getProviderDisplayName, getProviderIconByName, } from "@storyteller/model-list"; +import Seo from "../../components/seo"; -const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v']; +const VIDEO_EXTENSIONS = [".mp4", ".webm", ".mov", ".avi", ".mkv", ".m4v"]; const COPY_FEEDBACK_DURATION = 1500; -const SHARE_URL_BASE = 'https://getartcraft.com/media/'; +const SHARE_URL_BASE = "https://getartcraft.com/media/"; const isVideoUrl = (url: string): boolean => { const urlLower = url.toLowerCase(); - return VIDEO_EXTENSIONS.some(ext => urlLower.includes(ext)); + return VIDEO_EXTENSIONS.some((ext) => urlLower.includes(ext)); }; interface ContextImage { @@ -78,7 +79,7 @@ const EMPTY_PROMPT_DATA: PromptData = { const createPromptData = ( data: any, hasToken: boolean, - loading: boolean = false + loading: boolean = false, ): PromptData => ({ text: data?.maybe_positive_prompt || null, loading, @@ -114,7 +115,13 @@ const useCopyFeedback = () => { return { copied, triggerCopy }; }; -const InfoRow = ({ label, value }: { label: string; value: React.ReactNode }) => ( +const InfoRow = ({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) => (
    {label} @@ -155,7 +162,7 @@ export default function MediaPage() { if (mediaResponse.success && mediaResponse.data) { const file = mediaResponse.data; const url = file.media_links?.cdn_url || null; - + setMedia((prev) => { const isSameUrl = prev.url === url; return { @@ -171,8 +178,8 @@ export default function MediaPage() { }); if (file.maybe_prompt_token) { - setPromptData(prev => ({ ...prev, hasToken: true, loading: true })); - + setPromptData((prev) => ({ ...prev, hasToken: true, loading: true })); + try { const promptsApi = new PromptsApi(); const promptResponse = await promptsApi.GetPromptsByToken({ @@ -182,17 +189,31 @@ export default function MediaPage() { const data = promptResponse.success ? promptResponse.data : null; setPromptData(createPromptData(data, true, false)); } catch { - setPromptData(prev => ({ ...prev, loading: false })); + setPromptData((prev) => ({ ...prev, loading: false })); } } else { setPromptData(EMPTY_PROMPT_DATA); } } else { - setMedia({ url: null, token: null, createdAt: null, isVideo: false, isLoaded: false, creator: null }); + setMedia({ + url: null, + token: null, + createdAt: null, + isVideo: false, + isLoaded: false, + creator: null, + }); toast.error("Media not found"); } } catch { - setMedia({ url: null, token: null, createdAt: null, isVideo: false, isLoaded: false, creator: null }); + setMedia({ + url: null, + token: null, + createdAt: null, + isVideo: false, + isLoaded: false, + creator: null, + }); toast.error("Failed to load media"); } finally { setMediaRecordLoading(false); @@ -205,8 +226,6 @@ export default function MediaPage() { } }, [mediaIdParam, loadMedia]); - - const handleCopyPrompt = useCallback(async () => { if (!promptData.text) return; try { @@ -231,10 +250,12 @@ export default function MediaPage() { const shareButtonIcon = shareCopy.copied ? faCheck : faLink; - - return (
    +
    {/* Media Preview Area */} @@ -254,14 +275,14 @@ export default function MediaPage() { loop muted playsInline - onError={() => console.error('Video failed to load')} + onError={() => console.error("Video failed to load")} onLoadedData={(e) => { const el = e.currentTarget; - setMedia(prev => ({ - ...prev, + setMedia((prev) => ({ + ...prev, isLoaded: true, width: el.videoWidth, - height: el.videoHeight + height: el.videoHeight, })); }} /> @@ -273,18 +294,22 @@ export default function MediaPage() { className="h-full w-full object-contain transition-opacity duration-300" style={{ opacity: media.isLoaded ? 1 : 0 }} onError={(e) => { - (e.currentTarget as HTMLImageElement).src = PLACEHOLDER_IMAGES.DEFAULT; - (e.currentTarget as HTMLImageElement).style.opacity = "0.3"; - (e.currentTarget as HTMLImageElement).dataset.brokenurl = media.url || ""; - setMedia(prev => ({ ...prev, isLoaded: true })); + (e.currentTarget as HTMLImageElement).src = + PLACEHOLDER_IMAGES.DEFAULT; + (e.currentTarget as HTMLImageElement).style.opacity = + "0.3"; + ( + e.currentTarget as HTMLImageElement + ).dataset.brokenurl = media.url || ""; + setMedia((prev) => ({ ...prev, isLoaded: true })); }} onLoad={(e) => { const el = e.currentTarget; - setMedia(prev => ({ - ...prev, + setMedia((prev) => ({ + ...prev, isLoaded: true, width: el.naturalWidth, - height: el.naturalHeight + height: el.naturalHeight, })); }} /> @@ -327,7 +352,10 @@ export default function MediaPage() {
    {Array.from({ length: 5 }).map((_, i) => ( -
    +
    ))}
    @@ -341,8 +369,12 @@ export default function MediaPage() { size={36} username={media.creator.username} email_hash={media.creator.email_gravatar_hash} - avatarIndex={media.creator.core_info.default_avatar.image_index} - backgroundIndex={media.creator.core_info.default_avatar.color_index} + avatarIndex={ + media.creator.core_info.default_avatar.image_index + } + backgroundIndex={ + media.creator.core_info.default_avatar.color_index + } className="rounded-xl border-white/10" /> ) : ( @@ -351,8 +383,12 @@ export default function MediaPage() {
    )}
    - {media.creator.display_name} - Author + + {media.creator.display_name} + + + Author +
    )} @@ -381,11 +417,13 @@ export default function MediaPage() { onClick={handleCopyPrompt} className="flex items-center gap-1.5 text-xs text-white/60 hover:text-white transition-colors" > - + {promptCopy.copied ? "Copied" : "Copy"}
    - +
    {promptData.loading ? (
    @@ -404,52 +442,59 @@ export default function MediaPage() {
    - {promptData.contextImages && promptData.contextImages.length > 0 && ( -
    -
    - - Reference Images + {promptData.contextImages && + promptData.contextImages.length > 0 && ( +
    +
    + + Reference Images +
    +
    + {promptData.contextImages.map( + (contextImage, index) => { + const { thumbnail } = + getContextImageThumbnail(contextImage, { + size: THUMBNAIL_SIZES.SMALL, + }); + return ( +
    + {`Reference +
    + ); + }, + )} +
    -
    - {promptData.contextImages.map((contextImage, index) => { - const { thumbnail } = getContextImageThumbnail( - contextImage, - { size: THUMBNAIL_SIZES.SMALL } - ); - return ( -
    - {`Reference -
    - ); - })} -
    -
    - )} + )} - {(promptData.provider || promptData.modelType || media.createdAt) && ( + {(promptData.provider || + promptData.modelType || + media.createdAt) && (
    Information
    - +
    - - {promptData.modelType && ( {getModelCreatorIcon(promptData.modelType)} - {getModelDisplayName(promptData.modelType)} + + {getModelDisplayName( + promptData.modelType, + )} + } /> @@ -460,8 +505,15 @@ export default function MediaPage() { label="Provider" value={ <> - {getProviderIconByName(promptData.provider, "h-4 w-4 invert")} - {getProviderDisplayName(promptData.provider)} + {getProviderIconByName( + promptData.provider, + "h-4 w-4 invert", + )} + + {getProviderDisplayName( + promptData.provider, + )} + } /> @@ -473,11 +525,13 @@ export default function MediaPage() { value={`${media.width} × ${media.height}`} /> )} - + {media.createdAt && ( )}
    @@ -504,11 +558,15 @@ export default function MediaPage() { "w-full inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors border border-ui-panel-border", media.url ? "bg-ui-controls/40 hover:bg-ui-controls/60 text-white" - : "bg-ui-controls/20 text-white/60 cursor-not-allowed pointer-events-none" + : "bg-ui-controls/20 text-white/60 cursor-not-allowed pointer-events-none", )} - href={media.url ? addCorsParam(media.url) || media.url : undefined} + href={ + media.url ? addCorsParam(media.url) || media.url : undefined + } download={ - media.url ? `artcraft-${media.token || (media.isVideo ? "video" : "image")}` : undefined + media.url + ? `artcraft-${media.token || (media.isVideo ? "video" : "image")}` + : undefined } aria-disabled={!media.url} target="_blank" diff --git a/frontend/apps/artcraft-website/src/pages/news/news-index.tsx b/frontend/apps/artcraft-website/src/pages/news/news-index.tsx new file mode 100644 index 0000000000..ce5379fd3a --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/news/news-index.tsx @@ -0,0 +1,16 @@ +import { NewsIndex as LibNewsIndex } from "@storyteller/markdown-content"; +import Seo from "../../components/seo"; + +const NewsIndex = ({ basePath }: { basePath: string }) => { + return ( + <> + + + + ); +}; + +export default NewsIndex; diff --git a/frontend/apps/artcraft-website/src/pages/news/news-post.tsx b/frontend/apps/artcraft-website/src/pages/news/news-post.tsx new file mode 100644 index 0000000000..4eca168303 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/news/news-post.tsx @@ -0,0 +1,25 @@ +import { + NewsPost as LibNewsPost, + getNewsPostBySlug, +} from "@storyteller/markdown-content"; +import Seo from "../../components/seo"; +import { useParams } from "react-router-dom"; + +const NewsPost = ({ basePath }: { basePath: string }) => { + const { slug } = useParams(); + const post = slug ? getNewsPostBySlug(slug) : null; + + const title = post + ? `${post.title} - ArtCraft` + : "Article Not Found - ArtCraft"; + const desc = post ? post.description : ""; + + return ( + <> + + + + ); +}; + +export default NewsPost; diff --git a/frontend/apps/artcraft-website/src/pages/pricing/index.ts b/frontend/apps/artcraft-website/src/pages/pricing/index.ts new file mode 100644 index 0000000000..0d05fe37de --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/pricing/index.ts @@ -0,0 +1,2 @@ +import Pricing from "./pricing"; +export default Pricing; diff --git a/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx b/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx new file mode 100644 index 0000000000..296c63485a --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx @@ -0,0 +1,33 @@ +import Footer from "../../components/footer"; +import Seo from "../../components/seo"; +import { PricingTable } from "../../components/pricing-table"; + +const Pricing = () => { + return ( +
    + +
    +
    +
    +
    + +
    + +
    + +

    + Note: Pricing table is wrong, but we'll fix this shortly. +

    + +
    +
    + ); +}; + +export default Pricing; diff --git a/frontend/apps/artcraft-website/src/pages/signup/index.ts b/frontend/apps/artcraft-website/src/pages/signup/index.ts new file mode 100644 index 0000000000..53aa30bc54 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/signup/index.ts @@ -0,0 +1,2 @@ +import SignUp from "./signup"; +export default SignUp; diff --git a/frontend/apps/artcraft-website/src/pages/signup/signup.tsx b/frontend/apps/artcraft-website/src/pages/signup/signup.tsx new file mode 100644 index 0000000000..c1db425f21 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/signup/signup.tsx @@ -0,0 +1,47 @@ +import { Link, useNavigate } from "react-router-dom"; +import { SignupForm } from "../../components/auth"; +import Seo from "../../components/seo"; + +const Signup = () => { + const navigate = useNavigate(); + + return ( +
    + +
    + +
    +
    +
    +

    Create an Account

    +

    Join thousands of creators

    +
    + + navigate("/welcome")} + signupSource="artcraft" + /> + +
    + Already have an account?{" "} + + Log in + +
    +
    +
    + +
    + © {new Date().getFullYear()} ArtCraft. All rights reserved. +
    +
    + ); +}; + +export default Signup; diff --git a/frontend/apps/artcraft-website/src/pages/welcome/index.ts b/frontend/apps/artcraft-website/src/pages/welcome/index.ts new file mode 100644 index 0000000000..00ff1ec952 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/welcome/index.ts @@ -0,0 +1,2 @@ +import Welcome from "./welcome"; +export default Welcome; diff --git a/frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx b/frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx new file mode 100644 index 0000000000..de7aedc1e2 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx @@ -0,0 +1,161 @@ +import { + faCheckCircle, + faDownload, + faDesktop, + faRocket, +} 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"; +import { PricingTable } from "../../components/pricing-table"; + +const Welcome = () => { + const downloadUrl = isMacOs ? DOWNLOAD_LINKS.MACOS : DOWNLOAD_LINKS.WINDOWS; + + return ( +
    + + + {/* Background gradient */} +
    +
    +
    + +
    + {/* Success Header */} +
    +
    + + + Account created successfully! + +
    + +

    + Welcome to ArtCraft +

    +

    + Support open-source development and unlock premium AI art features. + Your subscription keeps ArtCraft free for everyone. +

    +
    + + {/* Pricing Table */} +
    + +
    + + {/* Next Steps Section */} +
    +
    +
    +
    + +
    +

    Getting Started

    +
    + +

    + Your download should have started automatically. Follow these + steps to begin creating: +

    + +
    + {[ + { 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) => ( +
    +
    + {item.step} +
    +
    {item.text}
    +
    + ))} +
    + + {/* Download Contingency */} + {!isMobile && ( +
    +
    + + Download didn't start? + +
    + + +
    +
    +
    + )} + + {isMobile && ( +
    +
    + +
    + ArtCraft is a powerful desktop experience.
    + Please head to your computer to download and install. +
    + )} +
    + + {/* Footer Link */} +
    + + Back to Home + +
    +
    +
    +
    + ); +}; + +export default Welcome; diff --git a/frontend/apps/artcraft-website/src/styles.css b/frontend/apps/artcraft-website/src/styles.css index 3882cbe23b..3475c9dbcf 100644 --- a/frontend/apps/artcraft-website/src/styles.css +++ b/frontend/apps/artcraft-website/src/styles.css @@ -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; } diff --git a/frontend/apps/artcraft-website/tailwind.config.js b/frontend/apps/artcraft-website/tailwind.config.js index 13f05a4542..338f8fca06 100644 --- a/frontend/apps/artcraft-website/tailwind.config.js +++ b/frontend/apps/artcraft-website/tailwind.config.js @@ -7,7 +7,7 @@ module.exports = { content: [ join( __dirname, - "{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}" + "{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}", ), ...createGlobPatternsForDependencies(__dirname), "./app/index.html", @@ -23,6 +23,7 @@ module.exports = { white: colors.white, gray: colors.gray, black: colors.black, + red: colors.red, blue: colors.blue, orange: colors.orange, green: colors.green, diff --git a/frontend/apps/artcraft-website/tsconfig.app.json b/frontend/apps/artcraft-website/tsconfig.app.json index 0e82a67661..00a86d267e 100644 --- a/frontend/apps/artcraft-website/tsconfig.app.json +++ b/frontend/apps/artcraft-website/tsconfig.app.json @@ -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" } ] } diff --git a/frontend/apps/artcraft-website/tsconfig.json b/frontend/apps/artcraft-website/tsconfig.json index 48053db779..9b1b3cdf03 100644 --- a/frontend/apps/artcraft-website/tsconfig.json +++ b/frontend/apps/artcraft-website/tsconfig.json @@ -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" }, diff --git a/frontend/apps/artcraft-website/vite.config.ts b/frontend/apps/artcraft-website/vite.config.ts index 0c25fe6437..4df77c2536 100644 --- a/frontend/apps/artcraft-website/vite.config.ts +++ b/frontend/apps/artcraft-website/vite.config.ts @@ -32,6 +32,9 @@ export default defineConfig(() => ({ target: 'https://api.storyteller.ai', changeOrigin: true, secure: true, + headers: { + Origin: 'https://api.storyteller.ai', + }, }, }, }, diff --git a/frontend/apps/artcraft/app/src/enums/Billing.ts b/frontend/apps/artcraft/app/src/enums/Billing.ts index c602de1b8a..08ea1d95fc 100644 --- a/frontend/apps/artcraft/app/src/enums/Billing.ts +++ b/frontend/apps/artcraft/app/src/enums/Billing.ts @@ -1,5 +1,6 @@ export enum SubscriptionNamespace { FAKEYOU = "fakeyou", + ARTCRAFT = "artcraft", } export enum SubscriptionProduct { PLUS = "fakeyou_plus", diff --git a/frontend/libs/api/src/lib/UsersApi.ts b/frontend/libs/api/src/lib/UsersApi.ts index 72ce5e9aaf..1bfc1e880d 100644 --- a/frontend/libs/api/src/lib/UsersApi.ts +++ b/frontend/libs/api/src/lib/UsersApi.ts @@ -8,7 +8,7 @@ interface SignupRequest { password: string; password_confirmation: string; signup_source?: string; -}; +} export class UsersApi extends ApiManager { private async authFetch( @@ -19,7 +19,7 @@ export class UsersApi extends ApiManager { }: { method: string; body?: B; - } + }, ): Promise { const bodyInString = JSON.stringify(body); @@ -33,8 +33,14 @@ export class UsersApi extends ApiManager { body: bodyInString, }); - const responseData = await response.json(); - return responseData as T; + const text = await response.text(); + try { + const responseData = JSON.parse(text); + return responseData as T; + } catch (e) { + console.error("Failed to parse response as JSON:", text); + throw new Error(text || `Request failed with status ${response.status}`); + } } public GetSession(): Promise< @@ -164,7 +170,7 @@ export class UsersApi extends ApiManager { signupSource?: string; }): Promise> { const endpoint = `${this.getApiSchemeAndHost()}/v1/create_account`; - const body : SignupRequest = { + const body: SignupRequest = { email_address: email, password, password_confirmation: passwordConfirmation, @@ -207,4 +213,4 @@ export class UsersApi extends ApiManager { } } -(window as any).UsersApi = new UsersApi(); \ No newline at end of file +(window as any).UsersApi = new UsersApi(); diff --git a/frontend/libs/api/src/lib/enums/Billing.ts b/frontend/libs/api/src/lib/enums/Billing.ts index c602de1b8a..08ea1d95fc 100644 --- a/frontend/libs/api/src/lib/enums/Billing.ts +++ b/frontend/libs/api/src/lib/enums/Billing.ts @@ -1,5 +1,6 @@ export enum SubscriptionNamespace { FAKEYOU = "fakeyou", + ARTCRAFT = "artcraft", } export enum SubscriptionProduct { PLUS = "fakeyou_plus", diff --git a/frontend/libs/state/subscription/src/lib/subscription-plans.ts b/frontend/libs/state/subscription/src/lib/subscription-plans.ts index 8b12330089..cf1dce0695 100644 --- a/frontend/libs/state/subscription/src/lib/subscription-plans.ts +++ b/frontend/libs/state/subscription/src/lib/subscription-plans.ts @@ -26,10 +26,10 @@ export const SUBSCRIPTION_PLANS: SubscriptionPlanDetails[] = [ originalMonthlyPrice: 10, originalYearlyPrice: 120, features: [ - { text: "~1,000 Flux images", included: true }, - { text: "~100 GPT-Image-1 images", included: true }, - { text: "~10 minutes Kling video", included: true }, - { text: "Commercial license", included: true }, + { text: "~10,000 Nano Banana Pro 4K images", included: true }, + { text: "~10,000 GPT-Image-1.5 images", included: true }, + { text: "~60 minutes Kling video", included: true }, + { text: "You Own ArtCraft Forever", included: true }, ], colorScheme: "green" as const, }, @@ -42,10 +42,10 @@ export const SUBSCRIPTION_PLANS: SubscriptionPlanDetails[] = [ originalMonthlyPrice: 35, originalYearlyPrice: 420, features: [ - { text: "~5,000 Flux images", included: true }, - { text: "~300 GPT-Image-1 images", included: true }, - { text: "~30 minutes Kling video", included: true }, - { text: "Commercial license", included: true }, + { text: "~50,000 Nano Banana Pro 4K images", included: true }, + { text: "~50,000 GPT-Image-1.5 images", included: true }, + { text: "~2 hours Kling video", included: true }, + { text: "You Own ArtCraft Forever", included: true }, ], colorScheme: "purple" as const, }, @@ -58,10 +58,10 @@ export const SUBSCRIPTION_PLANS: SubscriptionPlanDetails[] = [ originalMonthlyPrice: 60, originalYearlyPrice: 720, features: [ - { text: "~15,000 Flux images", included: true }, - { text: "~500 GPT-Image-1 images", included: true }, - { text: "~60 minutes Kling video", included: true }, - { text: "Commercial license", included: true }, + { text: "~100,000 Nano Banana Pro 4K images", included: true }, + { text: "~100,000 GPT-Image-1.5 images", included: true }, + { text: "~6 hours Kling video", included: true }, + { text: "You Own ArtCraft Forever", included: true }, ], colorScheme: "orange" as const, },