feat: overall improvements

# updated login flow
# removed sign-up
# use next-seo
This commit is contained in:
AykutSarac 2024-09-01 15:27:41 +03:00
parent a679149d03
commit 3582c1e063
No known key found for this signature in database
31 changed files with 621 additions and 662 deletions

View File

@ -37,6 +37,7 @@
"jxon": "2.0.0-beta.5", "jxon": "2.0.0-beta.5",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"next": "14.2.3", "next": "14.2.3",
"next-seo": "^6.5.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-compare-slider": "^3.1.0", "react-compare-slider": "^3.1.0",
"react-countup": "^6.5.3", "react-countup": "^6.5.3",

16
pnpm-lock.yaml generated
View File

@ -80,6 +80,9 @@ importers:
next: next:
specifier: 14.2.3 specifier: 14.2.3
version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-seo:
specifier: ^6.5.0
version: 6.5.0(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: react:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1 version: 18.3.1
@ -2004,6 +2007,13 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next-seo@6.5.0:
resolution: {integrity: sha512-MfzUeWTN/x/rsKp/1n0213eojO97lIl0unxqbeCY+6pAucViHDA8GSLRRcXpgjsSmBxfCFdfpu7LXbt4ANQoNQ==}
peerDependencies:
next: ^8.1.1-canary.54 || >=9.0.0
react: '>=16.0.0'
react-dom: '>=16.0.0'
next@14.2.3: next@14.2.3:
resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==}
engines: {node: '>=18.17.0'} engines: {node: '>=18.17.0'}
@ -4901,6 +4911,12 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
next-seo@6.5.0(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@next/env': 14.2.3 '@next/env': 14.2.3

View File

@ -3,6 +3,6 @@ Allow: /
User-agent: * User-agent: *
Disallow: /forgot-password Disallow: /forgot-password
Disallow: /sign-in Disallow: /widget
Disallow: /sign-up
Disallow: /widget Sitemap: https://jsoncrack.com/sitemap.txt

View File

@ -1,5 +1,8 @@
https://jsoncrack.com https://jsoncrack.com
https://jsoncrack.com/pricing https://jsoncrack.com/pricing
https://jsoncrack.com/sign-in
https://jsoncrack.com/sign-up
https://jsoncrack.com/oauth
https://jsoncrack.com/forgot-password https://jsoncrack.com/forgot-password
https://jsoncrack.com/editor https://jsoncrack.com/editor
https://jsoncrack.com/docs https://jsoncrack.com/docs

View File

@ -1,21 +0,0 @@
export function generateJsonld() {
return {
__html: `{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "JSON Crack",
"applicationCategory": "DeveloperApplication",
"screenshot": "https://jsoncrack.com/assets/preview/1.png",
"description": "JSON Crack is a powerful JSON visualization tool that offers a wide range of features including formatting, validation, conversion, and more.",
"url": "https://jsoncrack.com",
"browserRequirements": "Requires JavaScript. Requires HTML5.",
"operatingSystem": "All",
"sameAs": [
"https://www.x.com/jsoncrack",
"https://www.linkedin.com/company/jsoncrack",
"https://github.com/AykutSarac/jsoncrack.com"
]
}
`,
};
}

View File

@ -1,6 +1,3 @@
export const metaDescription =
"JSON Crack Editor is a tool for visualizing into graphs, analyzing, editing, formatting, querying, transforming and validating JSON, CSV, YAML, XML, and more.";
export const images = Object.freeze([ export const images = Object.freeze([
{ {
id: 1, id: 1,

29
src/constants/seo.ts Normal file
View File

@ -0,0 +1,29 @@
import type { NextSeoProps } from "next-seo";
export const SEO: NextSeoProps = {
title: "JSON Crack | Transform your data into interactive graphs",
description:
"JSON Crack Editor is a tool for visualizing into graphs, analyzing, editing, formatting, querying, transforming and validating JSON, CSV, YAML, XML, and more.",
themeColor: "#36393E",
openGraph: {
type: "website",
images: [
{
url: "https://jsoncrack.com/assets/jsoncrack.png",
width: 1200,
height: 627,
},
],
},
additionalLinkTags: [
{
rel: "manifest",
href: "/manifest.json",
},
{
rel: "icon",
href: "/favicon.ico",
sizes: "48x48",
},
],
};

View File

@ -0,0 +1,129 @@
import React from "react";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import {
Alert,
Anchor,
Center,
Container,
LoadingOverlay,
MantineProvider,
Text,
} from "@mantine/core";
import styled, { ThemeProvider } from "styled-components";
import { FaInfoCircle } from "react-icons/fa";
import { lightTheme } from "src/constants/theme";
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
const Toaster = dynamic(() => import("react-hot-toast").then(c => c.Toaster));
const StyledWrapper = styled.div`
position: relative;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
&:before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-size: 40px 40px;
background-image: linear-gradient(to right, #f7f7f7 1px, transparent 1px),
linear-gradient(to bottom, #f7f7f7 1px, transparent 1px);
image-rendering: pixelated;
-webkit-mask-image: linear-gradient(to bottom, transparent, 0%, white, 98%, transparent);
mask-image: linear-gradient(to bottom, transparent, 0%, white, 98%, transparent);
}
`;
const StyledPaper = styled.div`
border-radius: 0px;
max-width: 500px;
width: 100%;
padding: 24px;
background: rgba(255, 255, 255, 0.09);
border-radius: 12px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.3);
`;
function Loading() {
const router = useRouter();
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const handleStart = (url: string) => url !== router.asPath && setLoading(true);
const handleComplete = (url: string) => url === router.asPath && setLoading(false);
router.events.on("routeChangeStart", handleStart);
router.events.on("routeChangeComplete", handleComplete);
router.events.on("routeChangeError", handleComplete);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleComplete);
router.events.off("routeChangeError", handleComplete);
};
});
if (loading) return <LoadingOverlay visible />;
return null;
}
export const AuthLayout = ({ children }: React.PropsWithChildren) => {
return (
<StyledWrapper>
<MantineProvider forceColorScheme="light">
<ThemeProvider theme={lightTheme}>
<Toaster
position="bottom-right"
containerStyle={{
bottom: 34,
right: 8,
fontSize: 14,
}}
toastOptions={{
style: {
background: "#4D4D4D",
color: "#B9BBBE",
borderRadius: 4,
},
}}
/>
<Container>
<Center mb="xl">
<JSONCrackLogo fontSize="1.5rem" />
</Center>
<Alert py="sm" mb="md" color="indigo" icon={<FaInfoCircle />}>
Premium editor has been moved to{" "}
<Anchor href="https://todiagram.com" inherit>
todiagram.com
</Anchor>
.
</Alert>
<StyledPaper>
{children}
<Loading />
</StyledPaper>
<Text maw={250} ta="center" mx="auto" pos="relative" mt="md" fz="xs" c="gray.6">
By continuing you are agreeing to our{" "}
<Anchor fz="xs" component="a" href="/legal/terms" target="_blank">
Terms of Service
</Anchor>{" "}
and{" "}
<Anchor fz="xs" component="a" href="/legal/privacy" target="_blank">
Privacy Policy
</Anchor>
</Text>
</Container>
</ThemeProvider>
</MantineProvider>
</StyledWrapper>
);
};

View File

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import Head from "next/head";
import { Flex, Popover, Text } from "@mantine/core"; import { Flex, Popover, Text } from "@mantine/core";
import styled from "styled-components"; import styled from "styled-components";
import { AiOutlineLink, AiOutlineLock, AiOutlineUnlock } from "react-icons/ai"; import { AiOutlineLink, AiOutlineLock, AiOutlineUnlock } from "react-icons/ai";
@ -106,15 +105,11 @@ export const BottomBar = () => {
React.useEffect(() => { React.useEffect(() => {
setIsPrivate(data?.private ?? true); setIsPrivate(data?.private ?? true);
if (data?.name) window.document.title = `${data.name} | JSON Crack`;
}, [data]); }, [data]);
return ( return (
<StyledBottomBar> <StyledBottomBar>
{data?.name && (
<Head>
<title>{data.name} | JSON Crack</title>
</Head>
)}
<StyledLeft> <StyledLeft>
<StyledBottomBarItem onClick={toggleEditor}> <StyledBottomBarItem onClick={toggleEditor}>
<BiSolidDockLeft /> <BiSolidDockLeft />

View File

@ -1,14 +1,10 @@
import React from "react"; import React from "react";
import type { ModalProps } from "@mantine/core"; import type { ModalProps } from "@mantine/core";
import { Modal, Group, Button, Avatar, Text, Divider, Paper, Badge } from "@mantine/core"; import { Modal, Group, Button, Avatar, Text, Divider, Paper, Badge, Anchor } from "@mantine/core";
import { IoRocketSharp } from "react-icons/io5";
import { gaEvent } from "src/lib/utils/gaEvent";
import useModal from "src/store/useModal";
import useUser from "src/store/useUser"; import useUser from "src/store/useUser";
export const AccountModal = ({ opened, onClose }: ModalProps) => { export const AccountModal = ({ opened, onClose }: ModalProps) => {
const user = useUser(state => state.user); const user = useUser(state => state.user);
const setVisible = useModal(state => state.setVisible);
const logout = useUser(state => state.logout); const logout = useUser(state => state.logout);
const username = const username =
@ -47,20 +43,17 @@ export const AccountModal = ({ opened, onClose }: ModalProps) => {
</div> </div>
</Group> </Group>
</Paper> </Paper>
<Text fz="xs" c="dimmed">
<Divider py="xs" /> If you&apos;re already a premium user, please login at{" "}
<Anchor inherit href="https://todiagram.com" target="_blank">
ToDiagram
</Anchor>
.
</Text>
<Divider my="xs" />
<Group justify="right"> <Group justify="right">
<Button <Button
variant="default" variant="light"
leftSection={<IoRocketSharp />}
onClick={() => {
setVisible("upgrade")(true);
gaEvent("Account Modal", "click upgrade premium");
}}
>
Upgrade to Premium
</Button>
<Button
color="red" color="red"
onClick={() => { onClick={() => {
logout(); logout();

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import Link from "next/link";
import type { ModalProps } from "@mantine/core"; import type { ModalProps } from "@mantine/core";
import { Modal, Stack, Button } from "@mantine/core"; import { Modal, Stack, Button } from "@mantine/core";
@ -8,8 +9,9 @@ export const LoginModal = ({ opened, onClose }: ModalProps) => {
<Stack py="sm"> <Stack py="sm">
<Button <Button
variant="default" variant="default"
component="a" component={Link}
href="https://app.jsoncrack.com/sign-in" prefetch={false}
href="/sign-in"
rel="noreferrer" rel="noreferrer"
size="md" size="md"
fullWidth fullWidth

View File

@ -1,180 +1,46 @@
import React from "react"; import React from "react";
import Link from "next/link";
import type { ModalProps } from "@mantine/core"; import type { ModalProps } from "@mantine/core";
import { import { Text, Divider, List, Button, Modal } from "@mantine/core";
Text,
Flex,
Divider,
List,
Drawer,
Stack,
Radio,
Badge,
Button,
Group,
Anchor,
} from "@mantine/core";
import styled from "styled-components";
import { IoMdCheckmarkCircleOutline } from "react-icons/io"; import { IoMdCheckmarkCircleOutline } from "react-icons/io";
import { MdChevronRight, MdOutlineTimer } from "react-icons/md"; import { MdChevronRight } from "react-icons/md";
import { gaEvent } from "src/lib/utils/gaEvent";
import { PRICING, purchaseLinks } from "src/pages/pricing";
import useUser from "src/store/useUser";
const StyledRadioCard = styled(Radio.Card)`
border-width: 2px;
border-color: #efefef;
min-width: 450px;
transition: 0.2s;
&[data-checked] {
border-color: #2cb200;
background: #f9fff7;
}
&:hover:not([data-checked]) {
border-color: #d5d5d5;
background: #fafafa;
}
`;
export const UpgradeModal = ({ opened, onClose }: ModalProps) => { export const UpgradeModal = ({ opened, onClose }: ModalProps) => {
const user = useUser(state => state.user);
const [plan, setPlan] = React.useState("annual");
const handleUpgrade = () => {
const link = new URL(purchaseLinks[plan]);
if (user?.email) {
link.searchParams.append("checkout[email]", user.email);
}
if (user?.user_metadata.display_name) {
link.searchParams.append("checkout[name]", user.user_metadata.display_name);
}
gaEvent("Premium Modal", "click select", plan);
window.open(link.toString(), "_blank");
};
return ( return (
<Drawer <Modal
title={
<Text c="bright" fz="h2" fw={600}>
Upgrade
</Text>
}
size="md" size="md"
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
zIndex={1001} zIndex={1001}
position="bottom" centered
radius="lg"
styles={{
body: { background: "#ffffff" },
header: { background: "#ffffff" },
content: { background: "#ffffff" },
}}
> >
<Flex mx="auto" w="fit-content" miw={600} gap="3vw" justify="space-between"> <Divider mb="xs" fz="md" labelPosition="left" label="Included features" color="dimmed" />
<Stack> <List spacing="xs" c="gray" icon={<IoMdCheckmarkCircleOutline size="24" color="#16a34a" />}>
<Text c="black" fz="h2" fw={600}> <List.Item>Larger data support up to 4 MB</List.Item>
Upgrade <List.Item>Edit data directly on visualizations</List.Item>
</Text> <List.Item>Compare data differences on graphs</List.Item>
<Divider fz="md" labelPosition="left" label="Included features" color="gray.4" /> <List.Item>AI-powered data filter</List.Item>
<List <List.Item>Customizable graph colors</List.Item>
spacing="xs" <List.Item>Tabs for multiple documents</List.Item>
c="gray.7" <List.Item>...and more</List.Item>
icon={<IoMdCheckmarkCircleOutline size="24" color="#16a34a" />} </List>
> <Link href="https://todiagram.com/#preview" target="_blank" passHref>
<List.Item>Larger data support up to 4 MB</List.Item> <Button
<List.Item>Edit data directly on visualizations</List.Item> color="green"
<List.Item>Compare data differences on graphs</List.Item> fullWidth
<List.Item>AI-powered data filter</List.Item> mt="md"
<List.Item>Customizable graph colors</List.Item> size="lg"
<List.Item>Tabs for multiple documents</List.Item> radius="md"
<List.Item> rightSection={<MdChevronRight size="24" />}
<Anchor c="inherit" td="underline" href="/#pricing" target="_blank"> >
..see all features See more
</Anchor> </Button>
</List.Item> </Link>
</List> </Modal>
</Stack>
<Radio.Group value={plan} onChange={setPlan}>
<Stack>
<StyledRadioCard value="monthly" radius="lg" px="xl" py="md">
<Group align="center" justify="space-between">
<Flex align="center" gap="xs">
<Text fz="xl" c="gray.7" fw={600}>
Monthly
</Text>
</Flex>
<Flex fw={500} align="baseline" fz="sm" c="gray.5">
<Text fw={600} fz="xl" c="gray.7" mr="2">
${PRICING.MONTHLY}
</Text>
<Text inherit mr="2">
/
</Text>
month
</Flex>
</Group>
</StyledRadioCard>
<StyledRadioCard value="annual" radius="lg" px="xl" py="md">
<Group align="center" justify="space-between">
<Flex align="center" gap="xs">
<Text fz="xl" c="gray.7" fw={600}>
Yearly
</Text>
</Flex>
<Flex fw={500} align="baseline" fz="sm" c="gray.5">
<Text fw={600} fz="xl" c="gray.7" mr="2">
${PRICING.ANNUAL * 12}
</Text>
<Text inherit mr="2">
/
</Text>
year
</Flex>
</Group>
</StyledRadioCard>
{PRICING.LTD && (
<StyledRadioCard value="ltd" radius="lg" px="xl" py="md">
<Group align="center" justify="space-between">
<Flex align="center" gap="xs">
<Text fz="xl" c="gray.7" fw={600}>
Lifetime
</Text>
<Badge
variant="light"
size="md"
radius="lg"
color="#f00"
leftSection={<MdOutlineTimer size="12" />}
>
Limited
</Badge>
</Flex>
<Flex fw={500} align="baseline" fz="sm" c="gray.5">
<Text fw={600} fz="xl" c="gray.7" mr="2">
${PRICING.LTD}
</Text>
<Text inherit mr="2">
/
</Text>
lifetime
</Flex>
</Group>
</StyledRadioCard>
)}
</Stack>
<Button
color="green"
fullWidth
mt="xl"
size="xl"
radius="md"
onClick={handleUpgrade}
rightSection={<MdChevronRight size="24" />}
>
Upgrade
</Button>
</Radio.Group>
</Flex>
</Drawer>
); );
}; };

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import Link from "next/link";
import { Menu, Avatar, Text } from "@mantine/core"; import { Menu, Avatar, Text } from "@mantine/core";
import { VscSignIn, VscFeedback, VscSignOut } from "react-icons/vsc"; import { VscSignIn, VscFeedback, VscSignOut } from "react-icons/vsc";
import useModal from "src/store/useModal"; import useModal from "src/store/useModal";
@ -31,11 +32,11 @@ export const AccountMenu = () => {
<Text size="xs">{username ?? "Account"}</Text> <Text size="xs">{username ?? "Account"}</Text>
</Menu.Item> </Menu.Item>
) : ( ) : (
<a href="https://app.jsoncrack.com/sign-in"> <Link href="/sign-in" prefetch={false}>
<Menu.Item leftSection={<VscSignIn />}> <Menu.Item leftSection={<VscSignIn />}>
<Text size="xs">Sign in</Text> <Text size="xs">Sign in</Text>
</Menu.Item> </Menu.Item>
</a> </Link>
)} )}
{user && ( {user && (
<> <>

View File

@ -91,10 +91,7 @@ export const OptionsMenu = () => {
<Menu.Item <Menu.Item
closeMenuOnClick closeMenuOnClick
leftSection={<VscLock />} leftSection={<VscLock />}
onClick={() => { onClick={() => setVisible("upgrade")(true)}
setVisible("upgrade")(true);
gaEvent("Options Menu", "toggle dark mode", darkmodeEnabled ? "on" : "off");
}}
> >
<Text size="xs">Customize Graph Colors</Text> <Text size="xs">Customize Graph Colors</Text>
</Menu.Item> </Menu.Item>

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import Link from "next/link";
import { Button } from "@mantine/core"; import { Button } from "@mantine/core";
import styled from "styled-components"; import styled from "styled-components";
import { JSONCrackLogo } from "./JsonCrackLogo"; import { JSONCrackLogo } from "./JsonCrackLogo";
@ -95,8 +96,8 @@ export const Navbar = () => {
<Button <Button
variant="subtle" variant="subtle"
color="black" color="black"
component="a" component={Link}
href="https://app.jsoncrack.com/sign-in" href="/sign-in"
visibleFrom="sm" visibleFrom="sm"
size="md" size="md"
radius="md" radius="md"

View File

@ -1,9 +1,8 @@
import React from "react"; import React from "react";
import { Button, Title } from "@mantine/core"; import Link from "next/link";
import { Button, Flex, Title, Image } from "@mantine/core";
import styled from "styled-components"; import styled from "styled-components";
import { MdChevronRight } from "react-icons/md"; import { MdChevronRight } from "react-icons/md";
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
import useModal from "src/store/useModal";
const StyledNotSupported = styled.div` const StyledNotSupported = styled.div`
position: relative; position: relative;
@ -134,9 +133,9 @@ const StyledNotSupported = styled.div`
`; `;
const StyledInfo = styled.p` const StyledInfo = styled.p`
width: 60%; max-width: 500px;
font-weight: 600; font-weight: 600;
font-size: 20px; font-size: 26px;
text-align: center; text-align: center;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL}; color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
`; `;
@ -155,30 +154,31 @@ const StyledContent = styled.div`
`; `;
export const NotSupported = () => { export const NotSupported = () => {
const setVisible = useModal(state => state.setVisible);
return ( return (
<StyledNotSupported> <StyledNotSupported>
<StyledContent> <StyledContent>
<Title mb="lg" style={{ pointerEvents: "none" }}> <Flex align="center" justify="center" gap="16" mb="lg">
<JSONCrackLogo fontSize="4rem" style={{ color: "gray" }} hideLogo /> <Image src="https://todiagram.com/logo.svg" alt="ToDiagram" w="48" h="48" />
</Title> <Title fz="48" style={{ pointerEvents: "none", mixBlendMode: "difference" }}>
ToDiagram
</Title>
</Flex>
<StyledInfo> <StyledInfo>
Upgrade to premium for larger data size support. The free version is incapable of handling Use ToDiagram for larger data size, faster performance, and more features.
data of this size!
</StyledInfo> </StyledInfo>
<Link href="https://todiagram.com" target="_blank" passHref>
<Button <Button
mt="lg" mt="lg"
size="lg" size="lg"
fw="bolder" fw="bolder"
color="green" color="#FE634E"
radius="md" autoContrast
rightSection={<MdChevronRight size="24" />} radius="md"
onClick={() => setVisible("upgrade")(true)} rightSection={<MdChevronRight size="24" />}
> >
Upgrade Premium Go to ToDiagram
</Button> </Button>
</Link>
</StyledContent> </StyledContent>
<div className="glowing"> <div className="glowing">

View File

@ -1,16 +1,14 @@
import React from "react"; import React from "react";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { Button, Stack, Text, Title } from "@mantine/core"; import { Button, Stack, Text, Title } from "@mantine/core";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import Layout from "src/layout/Layout"; import Layout from "src/layout/Layout";
const NotFound = () => { const NotFound = () => {
return ( return (
<Layout> <Layout>
<Head> <NextSeo {...SEO} title="404 | ToDiagram" noindex nofollow />
<title>404 | JSON Crack</title>
<meta name="robots" content="noindex,nofollow" />
</Head>
<Stack mt={100} justify="center" align="center"> <Stack mt={100} justify="center" align="center">
<Title fz={150} style={{ fontFamily: "monospace" }}> <Title fz={150} style={{ fontFamily: "monospace" }}>
404 404

View File

@ -1,14 +1,15 @@
import React from "react"; import React from "react";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { createTheme, MantineProvider } from "@mantine/core"; import { createTheme, MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/code-highlight/styles.css"; import "@mantine/code-highlight/styles.css";
import { ThemeProvider } from "styled-components"; import { ThemeProvider } from "styled-components";
import { NextSeo } from "next-seo";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import GlobalStyle from "src/constants/globalStyle"; import GlobalStyle from "src/constants/globalStyle";
import { SEO } from "src/constants/seo";
import { lightTheme } from "src/constants/theme"; import { lightTheme } from "src/constants/theme";
import { supabase } from "src/lib/api/supabase"; import { supabase } from "src/lib/api/supabase";
import useUser from "src/store/useUser"; import useUser from "src/store/useUser";
@ -81,9 +82,7 @@ function JsonCrack({ Component, pageProps }: AppProps) {
return ( return (
<> <>
<Head> <NextSeo {...SEO} />
<title>JSON Crack | Transform your data into interactive graphs</title>
</Head>
<MantineProvider defaultColorScheme="light" theme={theme}> <MantineProvider defaultColorScheme="light" theme={theme}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<Toaster <Toaster

View File

@ -3,11 +3,6 @@ import Document, { Html, Head, Main, NextScript } from "next/document";
import { ColorSchemeScript } from "@mantine/core"; import { ColorSchemeScript } from "@mantine/core";
import { ServerStyleSheet } from "styled-components"; import { ServerStyleSheet } from "styled-components";
const metatags = Object.freeze({
title: "JSON Crack | Transform your data into interactive graphs",
image: "https://jsoncrack.com/assets/jsoncrack.png",
});
class MyDocument extends Document { class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> { static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
const sheet = new ServerStyleSheet(); const sheet = new ServerStyleSheet();
@ -39,19 +34,6 @@ class MyDocument extends Document {
return ( return (
<Html lang="en"> <Html lang="en">
<Head> <Head>
<meta name="theme-color" content="#36393E" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/favicon.ico" />
<meta property="og:url" content="https://jsoncrack.com" key="ogurl" />
<meta property="og:type" content="website" key="ogtype" />
<meta property="og:title" content={metatags.title} key="ogtitle" />
<meta property="og:image" content={metatags.image} key="ogimage" />
<meta name="twitter:card" content="summary_large_image" key="twcard" />
<meta property="twitter:domain" content="https://jsoncrack.com" key="twdomain" />
<meta property="twitter:url" content="https://jsoncrack.com" key="twurl" />
<meta name="twitter:title" content={metatags.title} key="twtitle" />
<meta name="twitter:image" content={metatags.image} key="twimage" />
<ColorSchemeScript /> <ColorSchemeScript />
</Head> </Head>
<body> <body>

View File

@ -1,7 +1,8 @@
import React from "react"; import React from "react";
import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Button, Stack, Text, Title } from "@mantine/core"; import { Button, Stack, Text, Title } from "@mantine/core";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import Layout from "src/layout/Layout"; import Layout from "src/layout/Layout";
const Custom500 = () => { const Custom500 = () => {
@ -9,10 +10,7 @@ const Custom500 = () => {
return ( return (
<Layout> <Layout>
<Head> <NextSeo {...SEO} title="Unexpected Error Occured | ToDiagram" />
<title>Unexpected Error Occured | JSON Crack</title>
<meta name="robots" content="noindex,nofollow" />
</Head>
<Stack mt={100} justify="center" align="center"> <Stack mt={100} justify="center" align="center">
<Title fz={150} style={{ fontFamily: "monospace" }}> <Title fz={150} style={{ fontFamily: "monospace" }}>
500 500

View File

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import Head from "next/head";
import { Group, Paper, Stack, Text, Title } from "@mantine/core"; import { Group, Paper, Stack, Text, Title } from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight"; import { CodeHighlight } from "@mantine/code-highlight";
import styled from "styled-components"; import styled from "styled-components";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import Layout from "src/layout/Layout"; import Layout from "src/layout/Layout";
const StyledFrame = styled.iframe` const StyledFrame = styled.iframe`
@ -37,11 +38,12 @@ const StyledHighlight = styled.span<{ $link?: boolean; $alert?: boolean }>`
const Docs = () => { const Docs = () => {
return ( return (
<Layout> <Layout>
<Head> <NextSeo
<title>Embed - JSON Crack</title> {...SEO}
<meta name="description" content="Integrate JSON Crack widgets into your website." /> title="Documentation - JSON Crack"
<link rel="canonical" href="https://jsoncrack.com/docs" /> description="Integrate JSON Crack widgets into your website."
</Head> canonical="https://jsoncrack.com/docs"
/>
<Stack mx="auto" maw="90%"> <Stack mx="auto" maw="90%">
<Group mb="lg" mt={40}> <Group mb="lg" mt={40}>
<Title order={1} c="dark"> <Title order={1} c="dark">

View File

@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useMantineColorScheme } from "@mantine/core"; import { useMantineColorScheme } from "@mantine/core";
import "@mantine/dropzone/styles.css"; import "@mantine/dropzone/styles.css";
import styled, { ThemeProvider } from "styled-components"; import styled, { ThemeProvider } from "styled-components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { metaDescription } from "src/constants/landing"; import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import { darkTheme, lightTheme } from "src/constants/theme"; import { darkTheme, lightTheme } from "src/constants/theme";
import { Editor } from "src/containers/Editor"; import { Editor } from "src/containers/Editor";
import { BottomBar } from "src/containers/Editor/BottomBar"; import { BottomBar } from "src/containers/Editor/BottomBar";
@ -57,13 +57,12 @@ const EditorPage = () => {
return ( return (
<> <>
<Head> <NextSeo
<title>Editor | JSON Crack</title> {...SEO}
<meta name="description" content={metaDescription} key="description" /> title="Editor | JSON Crack"
<meta property="og:description" content={metaDescription} key="ogdescription" /> description="JSON Crack Editor is a tool for visualizing into graphs, analyzing, editing, formatting, querying, transforming and validating JSON, CSV, YAML, XML, and more."
<meta name="twitter:description" content={metaDescription} key="twdescription" />{" "} canonical="https://jsoncrack.com/editor"
<link rel="canonical" href="https://jsoncrack.com/editor" /> />
</Head>
<ThemeProvider theme={darkmodeEnabled ? darkTheme : lightTheme}> <ThemeProvider theme={darkmodeEnabled ? darkTheme : lightTheme}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ExternalMode /> <ExternalMode />

View File

@ -1,16 +1,31 @@
import React from "react"; import React from "react";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Button, Group, Paper, Stack, TextInput, Text, Anchor, PasswordInput } from "@mantine/core"; import {
import { toast } from "react-hot-toast"; Button,
import Layout from "src/layout/Layout"; Group,
Stack,
TextInput,
Text,
Anchor,
PasswordInput,
Title,
FocusTrap,
Alert,
} from "@mantine/core";
import { NextSeo } from "next-seo";
import { MdErrorOutline } from "react-icons/md";
import { SEO } from "src/constants/seo";
import { AuthLayout } from "src/containers/AuthLayout";
import { supabase } from "src/lib/api/supabase"; import { supabase } from "src/lib/api/supabase";
function ResetPassword() { function ResetPassword() {
const { push } = useRouter();
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [password, setPassword] = React.useState(""); const [password, setPassword] = React.useState("");
const [password2, setPassword2] = React.useState(""); const [password2, setPassword2] = React.useState("");
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState(false);
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
try { try {
@ -22,21 +37,42 @@ function ResetPassword() {
const { error } = await supabase.auth.updateUser({ password }); const { error } = await supabase.auth.updateUser({ password });
if (error) throw new Error(error.message); if (error) throw new Error(error.message);
toast.success("Successfully updated password!"); setSuccess(true);
setTimeout(() => window.location.assign("/sign-in"), 2000); push("/sign-in");
} catch (error) { } catch (error) {
if (error instanceof Error) toast.error(error.message); if (error instanceof Error) setErrorMessage(error.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
return ( if (success) {
<Paper mx="auto" mt={70} maw={400} p="lg" withBorder> return (
<Text size="lg" w={500} mb="lg"> <Text>
Create New Password Password updated successfully.{" "}
<Anchor component={Link} href="/sign-in">
Sign in
</Anchor>
</Text> </Text>
);
}
return (
<FocusTrap>
{errorMessage && (
<Alert
onClose={() => setErrorMessage(null)}
color="red"
py="xs"
mb="lg"
icon={<MdErrorOutline color="red" />}
withCloseButton
>
<Text fz="sm" c="red">
{errorMessage}
</Text>
</Alert>
)}
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Stack> <Stack>
<PasswordInput <PasswordInput
@ -48,13 +84,16 @@ function ResetPassword() {
radius="sm" radius="sm"
placeholder="" placeholder=""
style={{ color: "black" }} style={{ color: "black" }}
autoComplete="new-password"
data-autofocus
/> />
<PasswordInput <PasswordInput
name="password" name="password"
value={password2} value={password2}
onChange={e => setPassword2(e.target.value)} onChange={e => setPassword2(e.target.value)}
required required
label="Validate Password" label="Confirm Password"
autoComplete="new-password"
radius="sm" radius="sm"
placeholder="" placeholder=""
style={{ color: "black" }} style={{ color: "black" }}
@ -67,7 +106,15 @@ function ResetPassword() {
</Button> </Button>
</Group> </Group>
</form> </form>
</Paper> <Stack mt="lg" align="center">
<Anchor component={Link} prefetch={false} href="/sign-up" c="dark" fz="sm">
Don&apos;t have an account?
<Text component="span" fz="sm" c="blue" ml={4}>
Sign up
</Text>
</Anchor>
</Stack>
</FocusTrap>
); );
} }
@ -76,6 +123,7 @@ const ForgotPassword = () => {
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [email, setEmail] = React.useState(""); const [email, setEmail] = React.useState("");
const [success, setSuccess] = React.useState(false); const [success, setSuccess] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const isPasswordReset = query?.type === "recovery" && !query?.error; const isPasswordReset = query?.type === "recovery" && !query?.error;
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@ -83,12 +131,14 @@ const ForgotPassword = () => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
const { error } = await supabase.auth.resetPasswordForEmail(email); const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/forgot-password`,
});
if (error) throw new Error(error.message); if (error) throw new Error(error.message);
setSuccess(true); setSuccess(true);
} catch (error) { } catch (error) {
if (error instanceof Error) toast.error(error.message); if (error instanceof Error) setErrorMessage(error.message);
console.error(error); console.error(error);
} finally { } finally {
setLoading(false); setLoading(false);
@ -96,24 +146,45 @@ const ForgotPassword = () => {
}; };
return ( return (
<Layout> <AuthLayout>
<Head> <NextSeo
<title>Reset Password - JSON Crack</title> {...SEO}
<link rel="canonical" href="https://app.jsoncrack.com/forgot-password" /> title="Reset Password - JSON Crack"
<meta name="robots" content="noindex,nofollow" /> noindex
</Head> nofollow
canonical="https://jsoncrack.com/forgot-password"
/>
<Title c="dark.4" order={2} fz="xl" mb={25} fw={600}>
{isPasswordReset ? "Create New Password" : "Forgot Password"}
</Title>
{isPasswordReset ? ( {isPasswordReset ? (
<ResetPassword /> <ResetPassword />
) : ( ) : (
<Paper mx="auto" mt={100} maw={400} p="lg" withBorder> <>
<Text size="lg" w={500} c="dark"> {success ? (
Reset Password <>
</Text>
<Paper pt="lg">
{success ? (
<Text>We&apos;ve sent an email to you, please check your inbox.</Text> <Text>We&apos;ve sent an email to you, please check your inbox.</Text>
) : ( <Button component={Link} href="/sign-in" color="dark" radius="sm" mt="lg" fullWidth>
<form onSubmit={onSubmit}> Back to Login
</Button>
</>
) : (
<form onSubmit={onSubmit}>
{errorMessage && (
<Alert
onClose={() => setErrorMessage(null)}
color="red"
py="xs"
mb="lg"
icon={<MdErrorOutline color="red" />}
withCloseButton
>
<Text fz="sm" c="red">
{errorMessage}
</Text>
</Alert>
)}
<FocusTrap>
<Stack> <Stack>
<TextInput <TextInput
name="email" name="email"
@ -125,25 +196,29 @@ const ForgotPassword = () => {
placeholder="hello@jsoncrack.com" placeholder="hello@jsoncrack.com"
radius="sm" radius="sm"
style={{ color: "black" }} style={{ color: "black" }}
data-autofocus
/> />
</Stack> </Stack>
</FocusTrap>
<Group justify="apart" mt="xl"> <Group justify="apart" mt="md">
<Button color="dark" type="submit" radius="sm" loading={loading} fullWidth> <Button color="dark" type="submit" radius="sm" loading={loading} fullWidth>
Reset Password Reset Password
</Button> </Button>
</Group> </Group>
<Stack mt="lg" align="center"> </form>
<Anchor component={Link} href="/sign-in" c="dark" size="xs"> )}
Don&apos;t have an account? Sign Up <Stack mt="lg" align="center">
</Anchor> <Anchor component={Link} prefetch={false} href="/sign-up" c="dark" fz="sm">
</Stack> Don&apos;t have an account?
</form> <Text component="span" fz="sm" c="blue" ml={4}>
)} Sign up
</Paper> </Text>
</Paper> </Anchor>
</Stack>
</>
)} )}
</Layout> </AuthLayout>
); );
}; };

View File

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import Head from "next/head"; import { NextSeo, SoftwareAppJsonLd } from "next-seo";
import { generateJsonld } from "src/constants/jsonld"; import { SEO } from "src/constants/seo";
import { metaDescription } from "src/constants/landing";
import { FAQ } from "src/containers/Landing/FAQ"; import { FAQ } from "src/containers/Landing/FAQ";
import { Features } from "src/containers/Landing/Features"; import { Features } from "src/containers/Landing/Features";
import { HeroPreview } from "src/containers/Landing/HeroPreview"; import { HeroPreview } from "src/containers/Landing/HeroPreview";
@ -13,18 +12,16 @@ import Layout from "src/layout/Layout";
export const HomePage = () => { export const HomePage = () => {
return ( return (
<Layout> <Layout>
<Head> <NextSeo {...SEO} canonical="https://jsoncrack.com" />
<title>JSON Crack | Transform your data into interactive graphs</title> <SoftwareAppJsonLd
<meta name="description" content={metaDescription} key="description" /> name="JSON Crack"
<meta property="og:description" content={metaDescription} key="ogdescription" /> price="0"
<meta name="twitter:description" content={metaDescription} key="twdescription" /> priceCurrency="USD"
<link rel="canonical" href="https://jsoncrack.com" /> type="DeveloperApplication"
<script operatingSystem="All"
type="application/ld+json" keywords="json, json editor, json viewer, json formatter, json beautifier, json validator, json minifier, json compressor, json decompressor, json parser, json converter, json to yaml, json to xml, json to csv, json to tsv, json to html, json to markdown, json to base64, json to url, json to query string, json to form data, json to javascript object, json to php array, json to python dictionary, json to ruby hash, json to java object, json to c# object, json to go object, json to rust object, json to swift object, json to kotlin object, json to typescript object, json to graphql, json to sql, json to mongodb, json to yaml, yaml to json, xml to json, csv to json, tsv to json, html to json, markdown to json, base64 to json, url to json, query string to json, form data to json, javascript object to json, php array to json, python dictionary to json, ruby hash to json, java object to json, c# object to json, go object to json, rust object to json, swift object to json, kotlin object to json, typescript object to json, graphql to json, sql to json, mongodb to json, yaml to json, json to yaml, xml to json, csv to json, tsv to json, html to json, markdown to json, base64 to json, url to json, query string to json, form data to json, javascript object to json, php array to json, python dictionary to json, ruby hash to json, java object to json, c# object to json, go object to json, rust object to json, swift object to json, kotlin object to json, typescript object to json, graphql to json, sql to json, mongodb to json"
dangerouslySetInnerHTML={generateJsonld()} applicationCategory="DeveloperApplication"
key="product-jsonld" />
/>
</Head>
<HeroSection /> <HeroSection />
<HeroPreview /> <HeroPreview />
<Features /> <Features />

View File

@ -1,16 +1,19 @@
import React from "react"; import React from "react";
import Head from "next/head";
import { Box, Container, Paper, Stack, Text, Title } from "@mantine/core"; import { Box, Container, Paper, Stack, Text, Title } from "@mantine/core";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import Layout from "src/layout/Layout"; import Layout from "src/layout/Layout";
import privacy from "../../constants/privacy.json"; import privacy from "../../constants/privacy.json";
const Privacy = () => { const Privacy = () => {
return ( return (
<Layout> <Layout>
<Head> <NextSeo
<title>Privacy Policy - JSON Crack</title> {...SEO}
<link rel="canonical" href="https://jsoncrack.com/legal/privacy" /> title="Privacy Policy - JSON Crack"
</Head> description="JSON Crack Privacy Policy"
canonical="https://jsoncrack.com/legal/privacy"
/>
<Container my={50} size="md" pb="lg"> <Container my={50} size="md" pb="lg">
<Paper bg="transparent"> <Paper bg="transparent">
<Title ta="center" c="gray.8"> <Title ta="center" c="gray.8">

View File

@ -1,16 +1,19 @@
import React from "react"; import React from "react";
import Head from "next/head";
import { Box, Container, Paper, Stack, Text, Title } from "@mantine/core"; import { Box, Container, Paper, Stack, Text, Title } from "@mantine/core";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import Layout from "src/layout/Layout"; import Layout from "src/layout/Layout";
import terms from "../../constants/terms.json"; import terms from "../../constants/terms.json";
const Terms = () => { const Terms = () => {
return ( return (
<Layout> <Layout>
<Head> <NextSeo
<title>Terms of Service - JSON Crack</title> {...SEO}
<link rel="canonical" href="https://jsoncrack.com/legal/terms" /> title="Terms of Service - JSON Crack"
</Head> description="JSON Crack Terms of Service"
canonical="https://jsoncrack.com/legal/terms"
/>
<Container my={50} size="md" pb="lg"> <Container my={50} size="md" pb="lg">
<Paper bg="transparent"> <Paper bg="transparent">
<Title ta="center" c="gray.8"> <Title ta="center" c="gray.8">

25
src/pages/oauth.tsx Normal file
View File

@ -0,0 +1,25 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import { supabase } from "src/lib/api/supabase";
const Oauth = () => {
const { push } = useRouter();
useEffect(() => {
supabase.auth.getSession().then(({ data, error }) => {
if (error) return console.error(error);
if (!data.session) return;
push("/editor");
});
}, [push]);
return (
<>
<NextSeo {...SEO} canonical="https://jsoncrack.com/oauth" nofollow noindex />
</>
);
};
export default Oauth;

View File

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import Head from "next/head";
import { import {
Flex, Flex,
Stack, Stack,
@ -14,8 +13,10 @@ import {
Box, Box,
} from "@mantine/core"; } from "@mantine/core";
import styled from "styled-components"; import styled from "styled-components";
import { NextSeo } from "next-seo";
import { AiOutlineInfoCircle } from "react-icons/ai"; import { AiOutlineInfoCircle } from "react-icons/ai";
import { FaArrowRightLong, FaCircleCheck } from "react-icons/fa6"; import { FaArrowRightLong, FaCircleCheck } from "react-icons/fa6";
import { SEO } from "src/constants/seo";
import Layout from "src/layout/Layout"; import Layout from "src/layout/Layout";
import { gaEvent } from "src/lib/utils/gaEvent"; import { gaEvent } from "src/lib/utils/gaEvent";
@ -278,10 +279,12 @@ export const PricingCards = () => {
const Pricing = () => { const Pricing = () => {
return ( return (
<> <>
<Head> <NextSeo
<title>Pricing - JSON Crack</title> {...SEO}
<link rel="canonical" href="https://jsoncrack.com/pricing" /> title="Pricing - JSON Crack"
</Head> description="Upgrade to JSON Crack Premium for more features and better performance."
canonical="https://jsoncrack.com/pricing"
/>
<Layout> <Layout>
<PricingCards /> <PricingCards />
</Layout> </Layout>

View File

@ -1,31 +1,34 @@
import React from "react"; import React, { useEffect } from "react";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import type { PaperProps } from "@mantine/core";
import { import {
TextInput, TextInput,
PasswordInput, PasswordInput,
Paper,
Button, Button,
Divider,
Anchor, Anchor,
Stack, Stack,
Center, Center,
Text, Text,
FocusTrap,
Alert,
Box,
Flex,
} from "@mantine/core"; } from "@mantine/core";
import { toast } from "react-hot-toast"; import { NextSeo } from "next-seo";
import { AiOutlineGithub, AiOutlineGoogle } from "react-icons/ai"; import { AiOutlineGithub } from "react-icons/ai";
import Layout from "src/layout/Layout"; import { FcGoogle } from "react-icons/fc";
import { MdErrorOutline } from "react-icons/md";
import { SEO } from "src/constants/seo";
import { AuthLayout } from "src/containers/AuthLayout";
import { supabase } from "src/lib/api/supabase"; import { supabase } from "src/lib/api/supabase";
import { isIframe } from "src/lib/utils/widget";
import useUser from "src/store/useUser"; import useUser from "src/store/useUser";
export function AuthenticationForm(props: PaperProps) { export function AuthenticationForm() {
const { push } = useRouter(); const { push } = useRouter();
const setSession = useUser(state => state.setSession); const setSession = useUser(state => state.setSession);
const isAuthenticated = useUser(state => state.isAuthenticated); const isAuthenticated = useUser(state => state.isAuthenticated);
const [sessionLoading, setSessionLoading] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [loading, setLoading] = React.useState(false);
const [userData, setUserData] = React.useState({ const [userData, setUserData] = React.useState({
name: "", name: "",
email: "", email: "",
@ -34,7 +37,7 @@ export function AuthenticationForm(props: PaperProps) {
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setSessionLoading(true); setLoading(true);
const { data, error } = await supabase.auth.signInWithPassword({ const { data, error } = await supabase.auth.signInWithPassword({
email: userData.email, email: userData.email,
@ -42,148 +45,153 @@ export function AuthenticationForm(props: PaperProps) {
}); });
if (error) { if (error) {
setSessionLoading(false); setLoading(false);
return toast.error(error.message); return setErrorMessage("Incorrect email or password.");
} }
await setSession(data.session); setSession(data.session);
push("/editor"); push("/editor");
setSessionLoading(false);
}; };
const handleLoginClick = async (provider: "github" | "google") => { const handleLoginClick = async (provider: "github" | "google") => {
setSessionLoading(true);
await supabase.auth.signInWithOAuth({ await supabase.auth.signInWithOAuth({
provider, provider,
options: { redirectTo: `${window.location.origin}/editor` }, options: { redirectTo: `${window.location.origin}/oauth` },
}); });
setSessionLoading(false);
}; };
if (isAuthenticated) { if (isAuthenticated) {
return ( return (
<Paper p="lg" maw={400} style={{ textAlign: "center" }}> <Box mt="lg">
<Text fz="sm" c="dark"> <Text fz="sm" c="dark">
You are already signed in. Click the button below to go to the editor. You are already signed in. Click the button below to go to the editor.
</Text> </Text>
<Link href="/editor"> <Link href="/editor">
<Button mt="lg" color="dark" size="lg"> <Button mt="lg" color="dark" size="md" radius="md" fullWidth>
GO TO EDITOR Go to Editor
</Button> </Button>
</Link> </Link>
</Paper> </Box>
); );
} }
return ( return (
<Paper {...props} style={{ textAlign: "left" }}> <Box>
<form onSubmit={onSubmit}> {errorMessage && (
<Stack> <Alert
<TextInput onClose={() => setErrorMessage(null)}
name="email" color="red"
required py="xs"
label="Email" mb="lg"
placeholder="hello@jsoncrack.com" icon={<MdErrorOutline color="red" />}
value={userData.email} withCloseButton
onChange={event => setUserData(d => ({ ...d, email: event.target.value }))} >
radius="sm" <Text fz="sm" c="red">
style={{ color: "black" }} {errorMessage}
/> </Text>
</Alert>
)}
<FocusTrap>
<form onSubmit={onSubmit}>
<Stack>
<TextInput
name="email"
type="email"
required
label="Email"
placeholder="hello@jsoncrack.com"
value={userData.email}
onChange={event => setUserData(d => ({ ...d, email: event.target.value }))}
radius="sm"
style={{ color: "black" }}
withAsterisk={false}
/>
<PasswordInput <PasswordInput
name="password" name="password"
required required
label="Password" label="Password"
placeholder="" placeholder=""
value={userData.password} value={userData.password}
onChange={event => setUserData(d => ({ ...d, password: event.target.value }))} onChange={event => setUserData(d => ({ ...d, password: event.target.value }))}
radius="sm" radius="sm"
style={{ color: "black" }} style={{ color: "black" }}
/> withAsterisk={false}
/>
<Button color="dark" type="submit" radius="sm" loading={sessionLoading}> <Anchor
Sign in component={Link}
</Button> prefetch={false}
href="/forgot-password"
<Stack gap="sm" mx="auto" align="center"> c="dimmed"
<Anchor component={Link} href="/forgot-password" c="dark" size="xs"> size="xs"
Forgot your password? ta="right"
mt="-sm"
>
Forgot password?
</Anchor> </Anchor>
<Button color="dark" type="submit" radius="sm" loading={loading}>
Sign in
</Button>
</Stack> </Stack>
</Stack> </form>
</form> </FocusTrap>
<Divider my="lg" /> <Flex mt="lg" gap="sm">
<Stack mb="md" mt="md">
<Button <Button
radius="sm" radius="sm"
leftSection={<AiOutlineGoogle size="20" />} fullWidth
leftSection={<FcGoogle size="20" />}
onClick={() => handleLoginClick("google")} onClick={() => handleLoginClick("google")}
color="red" variant="default"
variant="outline" disabled={loading}
> >
Sign in with Google Google
</Button> </Button>
<Button <Button
radius="sm" radius="sm"
leftSection={<AiOutlineGithub size="20" />} leftSection={<AiOutlineGithub size="20" />}
onClick={() => handleLoginClick("github")} onClick={() => handleLoginClick("github")}
color="dark" variant="default"
variant="outline" fullWidth
disabled={loading}
> >
Sign in with GitHub GitHub
</Button> </Button>
</Stack> </Flex>
</Paper> </Box>
); );
} }
const SignIn = () => { const SignIn = () => {
const { isReady, push, query } = useRouter(); const { push, query, pathname } = useRouter();
const hasSession = useUser(state => !!state.user);
const setSession = useUser(state => state.setSession);
const isPasswordReset = query?.type === "recovery" && !query?.error;
React.useEffect(() => { useEffect(() => {
if (isIframe()) { supabase.auth.getSession().then(({ data, error }) => {
push("/"); if (error) return console.error(error);
return; if (!data.session) return;
} push("/editor");
});
if (!isReady) return; }, [pathname, push, query.code]);
if (query?.access_token && query?.refresh_token) {
(async () => {
const refresh_token = query.refresh_token as string;
const access_token = query.access_token as string;
const { data, error } = await supabase.auth.setSession({ refresh_token, access_token });
if (error) return toast.error(error.message);
if (data.session) setSession(data.session);
})();
}
if (hasSession && !isPasswordReset) push("/editor");
}, [isReady, hasSession, push, isPasswordReset, query, setSession]);
if (!isReady) return null;
return ( return (
<Layout> <AuthLayout>
<Head> <NextSeo
<title>Sign In - JSON Crack</title> {...SEO}
<link rel="canonical" href="https://app.jsoncrack.com/sign-in" /> title="Sign In - JSON Crack"
</Head> description="Sign in to your JSON Crack account to create and edit diagrams with ease."
<Paper mt={100} mx="auto" maw={400} p="lg" withBorder> canonical="https://jsoncrack.com/sign-in"
<AuthenticationForm /> />
</Paper> <AuthenticationForm />
<Center my="xl"> <Center mt={80}>
<Anchor component={Link} href="/sign-up" c="gray.5" fw="bold"> <Anchor component={Link} prefetch={false} href="/sign-up" c="dark" fz="sm">
Don&apos;t have an account? Don&apos;t have an account?
<Text component="span" fz="sm" c="blue" ml={4}>
Sign up
</Text>
</Anchor> </Anchor>
</Center> </Center>
</Layout> </AuthLayout>
); );
}; };

View File

@ -1,193 +1,53 @@
import React from "react"; import React, { useEffect } from "react";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { import { useRouter } from "next/router";
Anchor, import { Anchor, Button, Center, Text } from "@mantine/core";
Button, import { NextSeo } from "next-seo";
Center, import { SEO } from "src/constants/seo";
Divider, import { AuthLayout } from "src/containers/AuthLayout";
Flex,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
} from "@mantine/core";
import toast from "react-hot-toast";
import { AiOutlineGithub, AiOutlineGoogle } from "react-icons/ai";
import Layout from "src/layout/Layout";
import { supabase } from "src/lib/api/supabase"; import { supabase } from "src/lib/api/supabase";
const SignUp = () => { const SignUp = () => {
const [loading, setLoading] = React.useState(false); const { push } = useRouter();
const [done, setDone] = React.useState(false);
const [userData, setUserData] = React.useState({
display_name: "",
email: "",
password: "",
});
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { useEffect(() => {
e.preventDefault(); supabase.auth.getSession().then(({ data }) => {
setLoading(true); if (data.session) push("/editor");
supabase.auth
.signUp({
email: userData.email,
password: userData.password,
options: {
data: { display_name: userData.display_name },
},
})
.then(({ error }) => {
if (error) return toast.error(error.message);
toast.success("Please check your inbox to confirm mail address!", { duration: 7000 });
setDone(true);
})
.finally(() => setLoading(false));
};
const handleLoginClick = (provider: "github" | "google") => {
supabase.auth.signInWithOAuth({
provider,
options: { redirectTo: `${window.location.origin}/editor` },
}); });
}; }, [push]);
return ( return (
<Layout> <AuthLayout>
<Head> <NextSeo
<title>Sign Up - JSON Crack</title> {...SEO}
<link rel="canonical" href="https://app.jsoncrack.com/sign-up" /> title="Sign Up - JSON Crack"
</Head> description="Create an account to start creating graphs and visualizing your data."
{done ? ( canonical="https://jsoncrack.com/sign-up"
<Paper mx="auto" maw={400} mt={100} p="lg" withBorder> />
<Text mt="lg" style={{ textAlign: "center" }}> <Text fw={500}>JSON Crack is no longer accepting new sign-ups.</Text>
Registration successul! <Text fz="sm" mt="md" c="gray.6">
<br /> For advanced features, please visit{" "}
Please check your inbox for email confirmation. <Anchor td="underline" href="https://todiagram.com" inherit>
ToDiagram
</Anchor>{" "}
or you can continue to use JSON Crack without an account.
</Text>
<Center my="md">
<Link href="/editor" prefetch={false} passHref>
<Button size="md" color="dark" radius="md">
Go to editor
</Button>
</Link>
</Center>
<Center mt={50}>
<Anchor component={Link} prefetch={false} href="/sign-in" c="dark" fz="sm">
Already have an account?
<Text component="span" fz="sm" c="blue" ml={4}>
Sign in
</Text> </Text>
<Anchor component={Link} href="/sign-in"> </Anchor>
<Button color="dark" radius="sm" mt="lg" fullWidth> </Center>
Back to login </AuthLayout>
</Button>
</Anchor>
</Paper>
) : (
<>
<Paper mx="auto" maw={400} mt={100} p="lg" withBorder>
<form onSubmit={onSubmit}>
<Stack>
<TextInput
name="name"
onChange={e => setUserData(d => ({ ...d, display_name: e.target.value }))}
required
label="Name"
placeholder="John Doe"
radius="sm"
style={{ color: "black" }}
/>
<TextInput
name="email"
onChange={e => setUserData(d => ({ ...d, email: e.target.value }))}
type="email"
required
label="Email"
placeholder="hello@jsoncrack.com"
radius="sm"
style={{ color: "black" }}
/>
<PasswordInput
name="password"
onChange={e => setUserData(d => ({ ...d, password: e.target.value }))}
min={6}
required
label="Password"
placeholder=""
radius="sm"
style={{ color: "black" }}
/>
<Button color="dark" type="submit" loading={loading}>
Sign up for free
</Button>
<Divider color="dimmed" label="OR CONTINUE WITH" labelPosition="center" />
<Flex gap="sm">
<Button
radius="sm"
fullWidth
leftSection={<AiOutlineGoogle size="20" />}
onClick={() => handleLoginClick("google")}
color="red"
variant="outline"
>
Google
</Button>
<Button
radius="sm"
leftSection={<AiOutlineGithub size="20" />}
onClick={() => handleLoginClick("github")}
color="dark"
variant="outline"
fullWidth
loading={loading}
>
GitHub
</Button>
</Flex>
<Divider mx={-20} />
<Text fz="xs" c="gray">
By signing up, you agree to our{" "}
<Anchor
fz="xs"
component={Link}
prefetch={false}
href="/legal/terms"
c="gray"
fw={500}
>
Terms of Service
</Anchor>{" "}
and{" "}
<Anchor
fz="xs"
component={Link}
prefetch={false}
href="/legal/privacy"
c="gray"
fw={500}
>
Privacy Policy
</Anchor>
. Need help?{" "}
<Anchor
fz="xs"
component={Link}
href="mailto:contact@jsoncrack.com"
c="gray"
fw={500}
>
Get in touch.
</Anchor>
</Text>
</Stack>
</form>
</Paper>
<Center my="xl">
<Anchor component={Link} href="/sign-in" c="gray.5" fw="bold">
Already have an account?
</Anchor>
</Center>
</>
)}
</Layout>
); );
}; };

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useMantineColorScheme } from "@mantine/core"; import { useMantineColorScheme } from "@mantine/core";
import { ThemeProvider } from "styled-components"; import { ThemeProvider } from "styled-components";
import { NextSeo } from "next-seo";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { darkTheme, lightTheme } from "src/constants/theme"; import { darkTheme, lightTheme } from "src/constants/theme";
import { Toolbar } from "src/containers/Toolbar"; import { Toolbar } from "src/containers/Toolbar";
@ -68,9 +68,7 @@ const WidgetPage = () => {
return ( return (
<> <>
<Head> <NextSeo noindex nofollow />
<meta name="robots" content="noindex,nofollow" />
</Head>
<ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}> <ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}>
<Toolbar isWidget /> <Toolbar isWidget />
<GraphView isWidget /> <GraphView isWidget />