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 posthog from "posthog-js";
|
||||||
import App from "../../App";
|
import App from "../../App";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { AlertDialog } from "@radix-ui/react-alert-dialog";
|
|
||||||
import { AlertDialogContent } from "../ui/alert-dialog";
|
|
||||||
import FullPageSpinner from "../custom-ui/FullPageSpinner";
|
import FullPageSpinner from "../custom-ui/FullPageSpinner";
|
||||||
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
|
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
|
||||||
import { useStore } from "../../store/store";
|
import { useStore } from "../../store/store";
|
||||||
import AvatarDropdown from "./AvatarDropdown";
|
import AvatarDropdown from "./AvatarDropdown";
|
||||||
import { UserResponse } from "./types";
|
import { UserResponse } from "./types";
|
||||||
import { POSTHOG_HOST, POSTHOG_KEY, SAAS_BACKEND_URL } from "../../config";
|
import { POSTHOG_HOST, POSTHOG_KEY, SAAS_BACKEND_URL } from "../../config";
|
||||||
|
import LandingPage from "./LandingPage";
|
||||||
|
|
||||||
function AppContainer() {
|
function AppContainer() {
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
|
||||||
const { isSignedIn, isLoaded } = useUser();
|
const { isSignedIn, isLoaded } = useUser();
|
||||||
|
|
||||||
const setSubscriberTier = useStore((state) => state.setSubscriberTier);
|
const setSubscriberTier = useStore((state) => state.setSubscriberTier);
|
||||||
@ -21,14 +19,7 @@ function AppContainer() {
|
|||||||
const authenticatedFetch = useAuthenticatedFetch();
|
const authenticatedFetch = useAuthenticatedFetch();
|
||||||
const isInitRequestInProgress = useRef(false);
|
const isInitRequestInProgress = useRef(false);
|
||||||
|
|
||||||
// If Clerk is loaded and the user is not signed in, show the sign up popup
|
// Get information from our backend about the user (subscription status)
|
||||||
useEffect(() => {
|
|
||||||
if (isLoaded && !isSignedIn) {
|
|
||||||
setShowPopup(true);
|
|
||||||
}
|
|
||||||
}, [isSignedIn, isLoaded]);
|
|
||||||
|
|
||||||
// Get the current user
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
// Make sure there's only one request in progress
|
// Make sure there's only one request in progress
|
||||||
@ -36,12 +27,17 @@ function AppContainer() {
|
|||||||
if (isInitRequestInProgress.current) return;
|
if (isInitRequestInProgress.current) return;
|
||||||
isInitRequestInProgress.current = true;
|
isInitRequestInProgress.current = true;
|
||||||
|
|
||||||
// TODO: Handle when the user is not signed in
|
|
||||||
const user: UserResponse = await authenticatedFetch(
|
const user: UserResponse = await authenticatedFetch(
|
||||||
SAAS_BACKEND_URL + "/users/create",
|
SAAS_BACKEND_URL + "/users/create",
|
||||||
"POST"
|
"POST"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If the user is not signed in, authenticatedFetch will return undefined
|
||||||
|
if (!user) {
|
||||||
|
isInitRequestInProgress.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.subscriber_tier) {
|
if (!user.subscriber_tier) {
|
||||||
setSubscriberTier("free");
|
setSubscriberTier("free");
|
||||||
} else {
|
} else {
|
||||||
@ -66,6 +62,10 @@ function AppContainer() {
|
|||||||
// If Clerk is still loading, show a spinner
|
// If Clerk is still loading, show a spinner
|
||||||
if (!isLoaded) return <FullPageSpinner />;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<App
|
<App
|
||||||
@ -75,31 +75,6 @@ function AppContainer() {
|
|||||||
</div>
|
</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 React from "react";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
// 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<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
@ -24,8 +24,8 @@ const DialogOverlay = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{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" />
|
<Cross2Icon className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close> */}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
@ -62,8 +62,8 @@ const DialogHeader = ({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogHeader.displayName = "DialogHeader"
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
@ -76,8 +76,8 @@ const DialogFooter = ({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogFooter.displayName = "DialogFooter"
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
@ -91,8 +91,8 @@ const DialogTitle = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
|
|||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -117,4 +117,4 @@ export {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
}
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user