diff --git a/frontend/apps/artcraft-website/src/app/app.tsx b/frontend/apps/artcraft-website/src/app/app.tsx index 0acbde1389..2811118451 100644 --- a/frontend/apps/artcraft-website/src/app/app.tsx +++ b/frontend/apps/artcraft-website/src/app/app.tsx @@ -8,7 +8,8 @@ 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"; diff --git a/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx b/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx index 5c7a23ec51..51ec92d45c 100644 --- a/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx +++ b/frontend/apps/artcraft-website/src/components/navbar/navbar.tsx @@ -169,7 +169,10 @@ export default function Navbar() { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - +

{user.display_name || user.username} 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 index 97d76efe30..1c116d90dd 100644 --- a/frontend/apps/artcraft-website/src/pages/forgot-password/forgot-password.tsx +++ b/frontend/apps/artcraft-website/src/pages/forgot-password/forgot-password.tsx @@ -4,65 +4,78 @@ 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

-
+
+

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} -

- -
- )} + {!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" + /> +
-
- - Back to Log in - +
+ ) : ( +
+
+ +
+

Check your email

+

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

+ +
+ )} +
+ + Back to Log in + +
diff --git a/frontend/apps/artcraft-website/src/pages/login/login.tsx b/frontend/apps/artcraft-website/src/pages/login/login.tsx index 95643aa385..e7af32386e 100644 --- a/frontend/apps/artcraft-website/src/pages/login/login.tsx +++ b/frontend/apps/artcraft-website/src/pages/login/login.tsx @@ -5,125 +5,147 @@ 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); + setError(null); + setIsLoading(true); - const api = new UsersApi(); - const response = await api.Login({ - usernameOrEmail: email, - password: password - }); + const api = new UsersApi(); + const response = await api.Login({ + usernameOrEmail: email, + password: password, + }); - setIsLoading(false); + setIsLoading(false); - if (response.success) { - window.dispatchEvent(new Event('auth-change')); - navigate("/"); - } else { - setError(response.errorMessage || "Invalid credentials"); - } + if (response.success) { + window.dispatchEvent(new Event("auth-change")); + navigate("/"); + } else { + setError(response.errorMessage || "Invalid credentials"); + } }; return (
+
- +
-
-

Welcome Back

-

Log in to your account

-
+
+

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 - + + 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. + © {new Date().getFullYear()} ArtCraft. All rights reserved.
); 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/pricing.tsx b/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx index 1af9555eae..8d1b9ab2f2 100644 --- a/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx +++ b/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx @@ -2,107 +2,198 @@ 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" }, +]; 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 (
+
-
+

Simple, Transparent Pricing

-

+

Start for free and scale as you grow. No hidden fees.

+ + {/* Billing Toggle */} +
+ + + -20% + +
-
- {/* Free Tier */} -
-

Hobbyist

-
- $0 - /month -
-

- Perfect for getting started and learning the basics of ArtCraft. -

- -
    - - - - -
-
+
+ {SUBSCRIPTION_PLANS.map((plan) => { + const isPopular = plan.slug === "artcraft_pro"; + const { current: price, original: originalPrice } = + formatPrice(plan); - {/* Pro Tier (Highlighted) */} -
-
- MOST POPULAR -
-

Pro

-
- $20 - /month -
-

- For professional artists and creators who need more power. -

- -
    - - - - - - -
-
+ return ( +
+ {isPopular && ( +
+ MOST POPULAR +
+ )} - {/* Studio Tier */} -
-

Studio

-
- $100 - /month -
-

- For teams and studios requiring collaboration tools. -

- -
    - - - - - -
-
+

+ {plan.name} +

+
+ {originalPrice !== null && ( + + ${originalPrice} + + )} + ${price} + /month +
+
+ {plan.monthlyPrice === 0 + ? "Free forever" + : isYearly + ? "Billed yearly" + : "Billed monthly"} +
+ + +
    + {plan.features.map((feature, idx) => ( + + ))} +
+
+ ); + })}
@@ -111,12 +202,22 @@ const Pricing = () => { ); }; -const Feature = ({ text, highlighted = false }: { text: string; highlighted?: boolean }) => ( +const Feature = ({ + text, + highlighted = false, +}: { + text: string; + highlighted?: boolean; +}) => (
  • -
    - +
    +
    - {text} + + {text} +
  • ); diff --git a/frontend/apps/artcraft-website/src/pages/signup/signup.tsx b/frontend/apps/artcraft-website/src/pages/signup/signup.tsx index 6a2a1b5a20..c1db425f21 100644 --- a/frontend/apps/artcraft-website/src/pages/signup/signup.tsx +++ b/frontend/apps/artcraft-website/src/pages/signup/signup.tsx @@ -1,37 +1,44 @@ 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" - /> +
    +

    Create an Account

    +

    Join thousands of creators

    +
    -
    - Already have an account?{" "} - - Log in - -
    + navigate("/welcome")} + signupSource="artcraft" + /> +
    + Already have an account?{" "} + + Log in + +
    - +
    - © {new Date().getFullYear()} ArtCraft. All rights reserved. + © {new Date().getFullYear()} ArtCraft. All rights reserved.
    ); diff --git a/frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx b/frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx index f3f3ab90a0..0697d84718 100644 --- a/frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx +++ b/frontend/apps/artcraft-website/src/pages/welcome/welcome.tsx @@ -2,19 +2,20 @@ 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"; const Welcome = () => { const downloadUrl = isMacOs ? DOWNLOAD_LINKS.MACOS : DOWNLOAD_LINKS.WINDOWS; return (
    +