add a new landing page
This commit is contained in:
parent
b04dba001d
commit
ac858e252b
BIN
frontend/public/demos/youtube.mp4
Normal file
BIN
frontend/public/demos/youtube.mp4
Normal file
Binary file not shown.
@ -1,18 +1,16 @@
|
||||
import { SignUp, useUser } from "@clerk/clerk-react";
|
||||
import { useUser } from "@clerk/clerk-react";
|
||||
import posthog from "posthog-js";
|
||||
import App from "../../App";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AlertDialog } from "@radix-ui/react-alert-dialog";
|
||||
import { AlertDialogContent } from "../ui/alert-dialog";
|
||||
import { useEffect, useRef } from "react";
|
||||
import FullPageSpinner from "../custom-ui/FullPageSpinner";
|
||||
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
|
||||
import { useStore } from "../../store/store";
|
||||
import AvatarDropdown from "./AvatarDropdown";
|
||||
import { UserResponse } from "./types";
|
||||
import { POSTHOG_HOST, POSTHOG_KEY, SAAS_BACKEND_URL } from "../../config";
|
||||
import LandingPage from "./LandingPage";
|
||||
|
||||
function AppContainer() {
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const { isSignedIn, isLoaded } = useUser();
|
||||
|
||||
const setSubscriberTier = useStore((state) => state.setSubscriberTier);
|
||||
@ -21,14 +19,7 @@ function AppContainer() {
|
||||
const authenticatedFetch = useAuthenticatedFetch();
|
||||
const isInitRequestInProgress = useRef(false);
|
||||
|
||||
// If Clerk is loaded and the user is not signed in, show the sign up popup
|
||||
useEffect(() => {
|
||||
if (isLoaded && !isSignedIn) {
|
||||
setShowPopup(true);
|
||||
}
|
||||
}, [isSignedIn, isLoaded]);
|
||||
|
||||
// Get the current user
|
||||
// Get information from our backend about the user (subscription status)
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
// Make sure there's only one request in progress
|
||||
@ -36,12 +27,17 @@ function AppContainer() {
|
||||
if (isInitRequestInProgress.current) return;
|
||||
isInitRequestInProgress.current = true;
|
||||
|
||||
// TODO: Handle when the user is not signed in
|
||||
const user: UserResponse = await authenticatedFetch(
|
||||
SAAS_BACKEND_URL + "/users/create",
|
||||
"POST"
|
||||
);
|
||||
|
||||
// If the user is not signed in, authenticatedFetch will return undefined
|
||||
if (!user) {
|
||||
isInitRequestInProgress.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.subscriber_tier) {
|
||||
setSubscriberTier("free");
|
||||
} else {
|
||||
@ -66,6 +62,10 @@ function AppContainer() {
|
||||
// If Clerk is still loading, show a spinner
|
||||
if (!isLoaded) return <FullPageSpinner />;
|
||||
|
||||
// If the user is not signed in, show the landing page
|
||||
if (isLoaded && !isSignedIn) return <LandingPage />;
|
||||
|
||||
// If the user is signed in, show the app
|
||||
return (
|
||||
<>
|
||||
<App
|
||||
@ -75,31 +75,6 @@ function AppContainer() {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<AlertDialog open={showPopup}>
|
||||
<AlertDialogContent className="flex justify-center">
|
||||
<SignUp
|
||||
appearance={{
|
||||
elements: {
|
||||
card: {
|
||||
boxShadow: "none",
|
||||
borderRadius: "0",
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
footer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
textAlign: "center",
|
||||
},
|
||||
footerAction: {
|
||||
marginBottom: "5px",
|
||||
},
|
||||
},
|
||||
layout: { privacyPageUrl: "https://a.picoapps.xyz/camera-write" },
|
||||
}}
|
||||
/>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
141
frontend/src/components/hosted/LandingPage.tsx
Normal file
141
frontend/src/components/hosted/LandingPage.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import Footer from "./LandingPage/Footer";
|
||||
import { Button } from "../ui/button";
|
||||
import { SignUp } from "@clerk/clerk-react";
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent } from "../ui/dialog";
|
||||
|
||||
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
|
||||
|
||||
function LandingPage() {
|
||||
const [isAuthPopupOpen, setIsAuthPopupOpen] = useState(false);
|
||||
|
||||
const signIn = () => {
|
||||
setIsAuthPopupOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full xl:w-[1000px] mx-auto mt-4">
|
||||
{/* Auth dialog */}
|
||||
<Dialog
|
||||
open={isAuthPopupOpen}
|
||||
onOpenChange={(value) => setIsAuthPopupOpen(value)}
|
||||
>
|
||||
<DialogContent className="flex justify-center">
|
||||
<SignUp
|
||||
appearance={{
|
||||
elements: {
|
||||
card: {
|
||||
boxShadow: "none",
|
||||
borderRadius: "0",
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
footer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
textAlign: "center",
|
||||
},
|
||||
footerAction: {
|
||||
marginBottom: "5px",
|
||||
},
|
||||
},
|
||||
layout: { privacyPageUrl: "https://a.picoapps.xyz/camera-write" },
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Navbar */}
|
||||
<nav className="border-b border-gray-200 px-4 py-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-lg font-semibold">Screenshot to Code</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="secondary" onClick={signIn}>
|
||||
Sign in
|
||||
</Button>
|
||||
<Button onClick={signIn}>Get started</Button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero */}
|
||||
<header className="px-4 py-16">
|
||||
<div className="mx-auto">
|
||||
<h2 className="text-5xl font-bold leading-tight mb-6">
|
||||
Build User Interfaces 10x Faster
|
||||
</h2>
|
||||
<p className="text-gray-600 text-xl mb-6">
|
||||
Convert any screenshot or design to clean code (with support for
|
||||
most frameworks)
|
||||
</p>
|
||||
<div className="flex space-x-4 flex-col sm:flex-row">
|
||||
<Button size="lg" className="text-lg py-6 px-8" onClick={signIn}>
|
||||
Get started
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://github.com/abi/screenshot-to-code",
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
className="flex items-center space-x-2 text-gray-600 hover:text-gray-900 py-6 px-8"
|
||||
>
|
||||
<FaGithub size={24} />
|
||||
<span>GitHub</span>
|
||||
<span className="text-sm bg-gray-200 rounded-full px-2 py-1">
|
||||
34,458
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Logo wall */}
|
||||
<div className="mx-auto mt-12 px-4 sm:px-0">
|
||||
<p className="text-gray-600 text-xl mb-10 text-center">
|
||||
#1 tool used by developers and designers from leading companies. Fully
|
||||
open source with 34k+ stars on GitHub.
|
||||
</p>
|
||||
<div
|
||||
className="mx-auto grid max-w-lg items-center gap-x-2
|
||||
gap-y-10 sm:max-w-xl grid-cols-3 lg:mx-0 lg:max-w-none mt-10"
|
||||
>
|
||||
{LOGOS.map((companyName) => (
|
||||
<img
|
||||
key={companyName}
|
||||
className="col-span-1 max-h-8 w-full object-contain
|
||||
grayscale opacity-50 hover:opacity-100"
|
||||
src={`https://picoapps.xyz/logos/${companyName}.png`}
|
||||
alt={companyName}
|
||||
width={120}
|
||||
height={48}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video section */}
|
||||
<div className="px-4 mt-20 mb-10 text-center">
|
||||
<video
|
||||
src="/demos/youtube.mp4"
|
||||
className="max-w-lg mx-auto rounded-md w-full sm:w-auto"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
<div className="mt-6">
|
||||
Watch Screenshot to Code convert a screenshot of YouTube to
|
||||
HTML/Tailwind
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LandingPage;
|
||||
35
frontend/src/components/hosted/LandingPage/Footer.tsx
Normal file
35
frontend/src/components/hosted/LandingPage/Footer.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="flex justify-between border-t border-gray-200 pt-4 mb-6 px-4 sm:px-0">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl mb-2">Screenshot to Code</span>
|
||||
<span className="text-xs">
|
||||
© {new Date().getFullYear()} WhimsyWorks, Inc. All rights reserved.
|
||||
</span>
|
||||
{/* <div
|
||||
className="bg-gray-800 text-white text-sm px-2 py-2
|
||||
rounded-full flex items-center space-x-2"
|
||||
>
|
||||
<span>Built with</span>
|
||||
<i className="fas fa-bolt text-yellow-400"></i>
|
||||
<span>Screenshot to Code</span>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex flex-col text-sm text-gray-600 mr-4">
|
||||
<span className="uppercase">Company</span>
|
||||
<div>WhimsyWorks Inc.</div>
|
||||
<div>Made in NYC 🗽</div>
|
||||
<a href="https://github.com/abi/screenshot-to-code" target="_blank">
|
||||
Github
|
||||
</a>
|
||||
<a href="mailto:support@picoapps.xyz" target="_blank">
|
||||
Contact
|
||||
</a>
|
||||
<a href="https://a.picoapps.xyz/camera-write" target="_blank">
|
||||
Terms of Service
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
export default Footer;
|
||||
@ -1,16 +1,16 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
// import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
@ -24,8 +24,8 @@ const DialogOverlay = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
{/* <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Close> */}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
@ -62,8 +62,8 @@ const DialogHeader = ({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
@ -76,8 +76,8 @@ const DialogFooter = ({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
@ -91,8 +91,8 @@ const DialogTitle = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
@ -117,4 +117,4 @@ export {
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user