Compare commits

..

28 Commits

Author SHA1 Message Date
Valtteri Karesto
a0303ecfa4 Merge pull request #32 from eqlabs/feat/fix-scrollbars
Fix scrollbars
2021-11-26 15:23:13 +02:00
Valtteri Karesto
5a79e07c2d Fix scrollbars 2021-11-26 15:20:30 +02:00
Valtteri Karesto
1107bb8196 Merge pull request #31 from eqlabs/feat/add-gist-integration
Feat/add gist integration
2021-11-26 00:11:24 +02:00
Valtteri Karesto
405aafed7e Removed unused code 2021-11-26 00:02:39 +02:00
Valtteri Karesto
03156474f3 Move build button and add loading states 2021-11-25 23:55:45 +02:00
Valtteri Karesto
7982209732 Move deploy button to footer 2021-11-25 23:55:21 +02:00
Valtteri Karesto
0f9963b972 Refactor global navigation and editor navigation 2021-11-25 23:55:12 +02:00
Valtteri Karesto
650243279a Add sync to gist issue #19 feature and other fixes to global state 2021-11-25 23:54:47 +02:00
Valtteri Karesto
6f183049d5 Add some safeguards to data and new button size 2021-11-25 23:54:03 +02:00
Valtteri Karesto
48706effc1 Style tweaks to button component styles 2021-11-25 23:32:09 +02:00
Valtteri Karesto
c9740b1e8a Minor tweaks to dropdown menu 2021-11-25 23:31:49 +02:00
Valtteri Karesto
166300b8d5 Custom alert dialog page 2021-11-25 23:31:33 +02:00
Valtteri Karesto
99968855e0 Add alert dialog from radix 2021-11-25 23:31:24 +02:00
Valtteri Karesto
a62a9c3700 Separate login page so we maintain the app state, related to issue #17 2021-11-25 23:31:10 +02:00
Valtteri Karesto
4e971ce119 Conditional chaining to map 2021-11-25 22:02:02 +02:00
Valtteri Karesto
72ba2072ec Uppercase variant for Heading 2021-11-25 22:01:27 +02:00
Valtteri Karesto
bd8d3c39c2 New size for input component 2021-11-25 22:01:13 +02:00
Valtteri Karesto
7142f5b5e2 Added buttongroup component 2021-11-25 22:00:56 +02:00
Valtteri Karesto
37516c602d Add some dependencies and theme style change 2021-11-25 17:30:11 +02:00
Valtteri Karesto
cfa7a3bd30 update dialog styles 2021-11-25 17:29:52 +02:00
Valtteri Karesto
266e4c3e6c Create panel box component 2021-11-25 17:29:40 +02:00
Valtteri Karesto
4cf6d376f0 Update themechanger styles 2021-11-25 17:29:31 +02:00
Valtteri Karesto
b2f49625db Add truncate string util 2021-11-25 17:29:20 +02:00
Valtteri Karesto
0b9fd172ce Add patterns to modal 2021-11-25 17:29:08 +02:00
Valtteri Karesto
2582981d85 Open modal if no gist id or files 2021-11-25 17:28:41 +02:00
Valtteri Karesto
a0eda59982 Added some tsconfigs 2021-11-25 17:28:26 +02:00
Valtteri Karesto
9ae9db984d Renamed pages 2021-11-25 17:28:15 +02:00
Valtteri Karesto
8f2c78b08b Merge pull request #30 from eqlabs/feat/button-loading-state
Feat/button loading state
2021-11-20 01:37:26 +02:00
30 changed files with 1183 additions and 369 deletions

View File

@@ -0,0 +1,88 @@
import React from "react";
import { blackA } from "@radix-ui/colors";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { styled, keyframes } from "../stitches.config";
const overlayShow = keyframes({
"0%": { opacity: 0 },
"100%": { opacity: 1 },
});
const contentShow = keyframes({
"0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
"100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
});
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
zIndex: 1000,
backgroundColor: blackA.blackA9,
position: "fixed",
inset: 0,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
".dark &": {
backgroundColor: blackA.blackA11,
},
});
const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({
children,
...rest
}) => {
return (
<AlertDialogPrimitive.Root {...rest}>
<StyledOverlay />
{children}
</AlertDialogPrimitive.Root>
);
};
const StyledContent = styled(AlertDialogPrimitive.Content, {
zIndex: 1000,
backgroundColor: "$mauve2",
color: "$mauve12",
borderRadius: "$md",
boxShadow:
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "90vw",
maxWidth: "450px",
maxHeight: "85vh",
padding: 25,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
"&:focus": { outline: "none" },
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
},
});
const StyledTitle = styled(AlertDialogPrimitive.Title, {
margin: 0,
color: "$mauve12",
fontWeight: 500,
fontSize: "$lg",
});
const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20,
color: "$mauve11",
lineHeight: 1.5,
fontSize: "$sm",
});
// Exports
export const AlertDialog = Root;
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
export const AlertDialogContent = StyledContent;
export const AlertDialogTitle = StyledTitle;
export const AlertDialogDescription = StyledDescription;
export const AlertDialogAction = AlertDialogPrimitive.Action;
export const AlertDialogCancel = AlertDialogPrimitive.Cancel;

View File

@@ -1,11 +1,9 @@
import { CSS } from "@stitches/react/types/css-util";
import { StyledComponent } from "@stitches/react/types/styled-component";
import React from "react";
import { styled } from "../stitches.config";
import Flex from "./Flex";
import Spinner from "./Spinner";
const Button = styled("button", {
export const StyledButton = styled("button", {
// Reset
all: "unset",
appereance: "none",
@@ -32,8 +30,8 @@ const Button = styled("button", {
fontSize: "$2",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
backgroundColor: "red",
cursor: "pointer",
width: "max-content",
"&:disabled": {
opacity: 0.6,
pointerEvents: "none",
@@ -41,6 +39,12 @@ const Button = styled("button", {
},
variants: {
size: {
xs: {
borderRadius: "$sm",
height: "$5",
px: "$2",
fontSize: "$xs",
},
sm: {
borderRadius: "$sm",
height: "$7",
@@ -77,7 +81,7 @@ const Button = styled("button", {
},
"&:focus": {
boxShadow:
"inset 0 0 0 1px $colors$mauve12, 0 0 0 1px $colors$mauve12",
"inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
@@ -100,7 +104,7 @@ const Button = styled("button", {
boxShadow: "inset 0 0 0 1px $colors$pink8",
},
"&:focus": {
boxShadow: "inset 0 0 0 1px $colors$pink8",
boxShadow: "inset 0 0 0 2px $colors$pink12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
@@ -119,6 +123,11 @@ const Button = styled("button", {
textTransform: "uppercase",
},
},
fullWidth: {
true: {
width: "100%",
},
},
ghost: {
true: {
boxShadow: "none",
@@ -182,10 +191,10 @@ const Button = styled("button", {
});
const CustomButton: React.FC<
React.ComponentProps<typeof Button> & { as?: string }
React.ComponentProps<typeof StyledButton> & { as?: string }
> = React.forwardRef(({ children, as = "button", ...rest }, ref) => (
// @ts-expect-error
<Button {...rest} ref={ref} as={as}>
<StyledButton {...rest} ref={ref} as={as}>
<Flex
as="span"
css={{ gap: "$2", alignItems: "center" }}
@@ -194,7 +203,7 @@ const CustomButton: React.FC<
{children}
</Flex>
{rest.isLoading && <Spinner css={{ position: "absolute" }} />}
</Button>
</StyledButton>
));
CustomButton.displayName = "CustomButton";

View File

@@ -0,0 +1,29 @@
import { styled } from "../stitches.config";
import { StyledButton } from "./Button";
const ButtonGroup = styled("div", {
display: "flex",
marginLeft: "1px",
[`& ${StyledButton}`]: {
marginLeft: "-1px",
px: "$4",
zIndex: 2,
position: "relative",
"&:hover, &:focus": {
zIndex: 200,
},
},
[`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: {
borderRadius: 0,
},
[`& ${StyledButton}:first-child:not(:only-of-type)`]: {
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
},
[`& ${StyledButton}:last-child:not(:only-of-type)`]: {
borderBottomLeftRadius: 0,
borderTopLeftRadius: 0,
},
});
export default ButtonGroup;

View File

@@ -1,7 +1,7 @@
import React from "react";
import * as Stiches from "@stitches/react";
import { keyframes } from "@stitches/react";
import { violet, blackA, mauve, whiteA } from "@radix-ui/colors";
import { blackA } from "@radix-ui/colors";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { styled } from "../stitches.config";
@@ -23,7 +23,7 @@ const StyledOverlay = styled(DialogPrimitive.Overlay, {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
".dark &": {
backgroundColor: blackA.blackA9,
backgroundColor: blackA.blackA11,
},
});
@@ -49,7 +49,7 @@ const StyledContent = styled(DialogPrimitive.Content, {
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 10px 38px 0px rgba(22, 23, 24, 0.85), 0px 10px 20px 0px rgba(22, 23, 24, 0.6)",
"0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
},
});
@@ -77,7 +77,7 @@ const StyledDescription = styled(DialogPrimitive.Description, {
});
// Exports
export const Dialog = DialogPrimitive.Root;
export const Dialog = styled(DialogPrimitive.Root);
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogContent = Content;
export const DialogTitle = StyledTitle;

View File

@@ -61,6 +61,8 @@ const itemStyles = {
position: "relative",
paddingLeft: "5px",
userSelect: "none",
py: "$0.5",
pr: "$2",
gap: "$2",
"&[data-disabled]": {
@@ -113,6 +115,9 @@ const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
fill: "$mauve2",
".dark &": {
fill: "$mauve5",
},
});
// Exports

View File

@@ -1,8 +1,37 @@
import React, { useRef, useState } from "react";
import { Plus, Share, DownloadSimple, Gear, X } from "phosphor-react";
import { useTheme } from "next-themes";
import React, { useState, useEffect } from "react";
import {
Plus,
Share,
DownloadSimple,
Gear,
X,
GithubLogo,
SignOut,
ArrowSquareOut,
CloudArrowUp,
CaretDown,
User,
FilePlus,
} from "phosphor-react";
import Image from "next/image";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuArrow,
DropdownMenuSeparator,
} from "./DropdownMenu";
import NewWindow from "react-new-window";
import { signOut, useSession } from "next-auth/react";
import { useSnapshot } from "valtio";
import { createNewFile, state, updateEditorSettings } from "../state";
import {
createNewFile,
state,
syncToGist,
updateEditorSettings,
} from "../state";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
@@ -16,24 +45,52 @@ import {
} from "./Dialog";
import Flex from "./Flex";
import Stack from "./Stack";
import { useSnapshot } from "valtio";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import Input from "./Input";
import toast from "react-hot-toast";
import {
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
AlertDialogAction,
} from "./AlertDialog";
const EditorNavigation = () => {
const snap = useSnapshot(state);
const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [filename, setFilename] = useState("");
const { theme } = useTheme();
const { data: session, status } = useSession();
const router = useRouter();
const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
useEffect(() => {
if (session && session.user && popup) {
setPopUp(false);
}
}, [session, popup]);
return (
<Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex css={{ overflowX: "scroll", py: "$3", flex: 1 }}>
<Flex
css={{
overflowX: "scroll",
py: "$3",
flex: 1,
"&::-webkit-scrollbar": {
height: 0,
background: "transparent",
},
}}
>
<Container css={{ flex: 1 }}>
<Stack css={{ gap: "$3", flex: 1, flexWrap: "nowrap" }}>
{state.loading && "loading"}
<Stack
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{snap.files &&
snap.files.length > 0 &&
snap.files?.map((file, index) => (
@@ -138,111 +195,272 @@ const EditorNavigation = () => {
zIndex: 1,
}}
>
<Container css={{ width: "unset" }}>
<Container
css={{ width: "unset", display: "flex", alignItems: "center" }}
>
{status === "authenticated" ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Box
css={{
display: "flex",
borderRadius: "$full",
overflow: "hidden",
width: "$6",
height: "$6",
boxShadow: "0px 0px 0px 1px $colors$mauve11",
position: "relative",
mr: "$3",
"@hover": {
"&:hover": {
cursor: "pointer",
boxShadow: "0px 0px 0px 1px $colors$mauve12",
},
},
}}
>
<Image
src={session?.user?.image || ""}
width="30px"
height="30px"
objectFit="cover"
alt="User avatar"
/>
</Box>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} (
{session?.user.name})
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(
`http://gist.github.com/${session?.user.username}`
)
}
>
<ArrowSquareOut size="16px" />
Go to your Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut({ callbackUrl: "/" })}>
<SignOut size="16px" /> Log out
</DropdownMenuItem>
<DropdownMenuArrow offset={10} />
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button
outline
size="sm"
css={{ mr: "$3" }}
onClick={() => setPopUp(true)}
>
<GithubLogo size="16px" /> Login
</Button>
)}
<Stack
css={{
display: "inline-flex",
marginLeft: "auto",
flexShrink: 0,
gap: "$0",
border: "1px solid $mauve10",
borderRadius: "$sm",
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
zIndex: 9,
position: "relative",
overflow: "hidden",
button: {
borderRadius: "$0",
borderRadius: 0,
px: "$2",
alignSelf: "flex-start",
boxShadow: "none",
},
"button:not(:first-child):not(:last-child)": {
borderRight: 0,
borderLeft: 0,
},
"button:first-child": {
borderTopLeftRadius: "$sm",
borderBottomLeftRadius: "$sm",
},
"button:last-child": {
borderTopRightRadius: "$sm",
borderBottomRightRadius: "$sm",
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
"&:hover": {
boxShadow: "inset 0px 0px 0px 1px $colors$mauve12",
},
},
}}
>
<Button ghost size="sm" css={{ alignItems: "center" }}>
<Button outline size="sm" css={{ alignItems: "center" }}>
<DownloadSimple size="16px" />
</Button>
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: "center" }}>
<Button
outline
size="sm"
css={{ alignItems: "center" }}
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}`
);
toast.success("Copied share link to clipboard!");
}}
>
<Share size="16px" />
</Button>
<Button
outline
size="sm"
disabled={!session || !session.user}
isLoading={snap.gistLoading}
css={{ alignItems: "center" }}
onClick={() => {
if (snap.gistOwner === session?.user.username) {
syncToGist(session);
} else {
setCreateNewAlertOpen(true);
}
}}
>
<CloudArrowUp size="16px" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button outline size="sm">
<CaretDown size="16px" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DownloadSimple size="16px" /> Download as ZIP
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}`
);
toast.success("Copied share link to clipboard!");
}}
>
<Share size="16px" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Share hook</DialogTitle>
<DialogDescription>
<span>
We will store your hook code in public GitHub Gist and
generate link to that
</span>
</DialogDescription>
<Flex
css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
Copy share link to clipboard
</DropdownMenuItem>
<DropdownMenuItem
disabled={
session?.user.username !== snap.gistOwner || !snap.gistId
}
onClick={() => {
syncToGist(session);
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: "center" }}>
<Gear size="16px" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Editor settings</DialogTitle>
<DialogDescription>
<label>Tab size</label>
<Input
type="number"
min="1"
value={editorSettings.tabSize}
onChange={(e) =>
setEditorSettings((curr) => ({
...curr,
tabSize: Number(e.target.value),
}))
}
/>
</DialogDescription>
<Flex
css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
<CloudArrowUp size="16px" /> Push to Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={status !== "authenticated"}
onClick={() => {
setCreateNewAlertOpen(true);
}}
>
<DialogClose asChild>
<Button
outline
onClick={() => updateEditorSettings(editorSettings)}
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => updateEditorSettings(editorSettings)}
>
Save changes
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<FilePlus size="16px" /> Create as a new Gist
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEditorSettingsOpen(true)}>
<Gear size="16px" /> Editor Settings
</DropdownMenuItem>
<DropdownMenuArrow offset={10} />
</DropdownMenuContent>
</DropdownMenu>
</Stack>
{popup && !session ? (
<NewWindow center="parent" url="/sign-in" />
) : null}
</Container>
</Flex>
<AlertDialog
open={createNewAlertOpen}
onOpenChange={(value) => setCreateNewAlertOpen(value)}
>
<AlertDialogContent>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action will create new <strong>public</strong> Github Gist from
your current saved files. You can delete gist anytime from your
GitHub Gists page.
</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
<AlertDialogCancel asChild>
<Button outline>Cancel</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
variant="primary"
onClick={() => {
syncToGist(session, true);
}}
>
<FilePlus size="15px" /> Create new Gist
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
<Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}>
<DialogTrigger asChild>
{/* <Button outline size="sm" css={{ alignItems: "center" }}>
<Gear size="16px" />
</Button> */}
</DialogTrigger>
<DialogContent>
<DialogTitle>Editor settings</DialogTitle>
<DialogDescription>
<label>Tab size</label>
<Input
type="number"
min="1"
value={editorSettings.tabSize}
onChange={(e) =>
setEditorSettings((curr) => ({
...curr,
tabSize: Number(e.target.value),
}))
}
/>
</DialogDescription>
<Flex css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}>
<DialogClose asChild>
<Button
outline
onClick={() => updateEditorSettings(editorSettings)}
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => updateEditorSettings(editorSettings)}
>
Save changes
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
</Flex>
);
};

View File

@@ -1,9 +1,13 @@
import React from "react";
import { useSnapshot } from "valtio";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { state } from "../state";
import { compileCode, state } from "../state";
import { Play, Prohibit } from "phosphor-react";
import Button from "./Button";
import Heading from "./Heading";
const Footer = () => {
const snap = useSnapshot(state);
@@ -14,9 +18,31 @@ const Footer = () => {
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
}}
>
<Container css={{ py: "$3", flexShrink: 1 }}>
<Heading
as="h3"
css={{ fontWeight: 300, m: 0, fontSize: "11px", color: "$mauve9" }}
>
DEVELOPMENT LOG
</Heading>
<Button
ghost
size="xs"
css={{
position: "absolute",
right: "$3",
top: "$2",
color: "$mauve10",
}}
onClick={() => {
state.logs = [];
}}
>
<Prohibit size="14px" />
</Button>
<Box
as="pre"
css={{
@@ -27,14 +53,12 @@ const Footer = () => {
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "scroll",
overflowY: "auto",
wordWrap: "break-word",
py: 3,
px: 3,
m: 3,
}}
>
{snap.logs.map((log, index) => (
{snap.logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
<LogText capitalize variant={log.type}>
{log.type}:{" "}
@@ -43,6 +67,24 @@ const Footer = () => {
</Box>
))}
</Box>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Container>
</Box>
);

View File

@@ -4,7 +4,13 @@ const Heading = styled("span", {
fontFamily: "$heading",
lineHeight: "$heading",
fontWeight: "$heading",
textTransform: "uppercase",
variants: {
uppercase: {
true: {
textTransform: "uppercase",
},
},
},
});
export default Heading;

View File

@@ -1,16 +1,16 @@
import React, { useEffect, useRef } from "react";
import React, { useRef } from "react";
import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { ArrowBendLeftUp, Play } from "phosphor-react";
import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { compileCode, saveFile, state } from "../state";
import { saveFile, state } from "../state";
import EditorNavigation from "./EditorNavigation";
import Text from "./Text";
@@ -18,6 +18,7 @@ import Text from "./Text";
const HooksEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
return (
<Box
@@ -32,10 +33,11 @@ const HooksEditor = () => {
}}
>
<EditorNavigation />
{snap.files.length > 0 ? (
{snap.files.length > 0 && router.isReady ? (
<Editor
keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language}
language={snap.files?.[snap.active]?.language}
path={snap.files?.[snap.active]?.name}
defaultValue={snap.files?.[snap.active]?.content}
beforeMount={(monaco) => {
@@ -67,52 +69,35 @@ const HooksEditor = () => {
/>
) : (
<Container>
<Box
css={{
flexDirection: "row",
width: "$spaces$wide",
gap: "$3",
display: "inline-flex",
}}
>
<Box css={{ display: "inline-flex", pl: "35px" }}>
<ArrowBendLeftUp size={30} />
</Box>
{!snap.loading && router.isReady && (
<Box
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
css={{
flexDirection: "row",
width: "$spaces$wide",
gap: "$3",
display: "inline-flex",
}}
>
<Text
css={{
fontSize: "14px",
maxWidth: "220px",
fontFamily: "$monospace",
}}
<Box css={{ display: "inline-flex", pl: "35px" }}>
<ArrowBendLeftUp size={30} />
</Box>
<Box
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
>
Click the link above to create a your file (until we get the
fancy modal, where you can select example files 😊)
</Text>
<Text
css={{
fontSize: "14px",
maxWidth: "220px",
fontFamily: "$monospace",
}}
>
Click the link above to create a your file
</Text>
</Box>
</Box>
</Box>
)}
</Container>
)}
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Box>
);
};

View File

@@ -65,6 +65,14 @@ export const Input = styled("input", {
variants: {
size: {
sm: {
height: "$5",
fontSize: "$1",
lineHeight: "$sizes$4",
"&:-webkit-autofill::first-line": {
fontSize: "$1",
},
},
md: {
height: "$8",
fontSize: "$1",

View File

@@ -1,5 +1,3 @@
import { useTheme } from "next-themes";
import { styled } from "../stitches.config";
const SVG = styled("svg", {

View File

@@ -1,23 +1,9 @@
import React from "react";
import Link from "next/link";
import {
Gear,
GithubLogo,
SignOut,
User,
ArrowSquareOut,
} from "phosphor-react";
import { useSnapshot } from "valtio";
import Image from "next/image";
import { useSession, signIn, signOut } from "next-auth/react";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuArrow,
DropdownMenuSeparator,
} from "./DropdownMenu";
import { useRouter } from "next/router";
import { FolderOpen, X, ArrowUpRight, BookOpen } from "phosphor-react";
import Stack from "./Stack";
import Logo from "./Logo";
@@ -26,17 +12,33 @@ import Flex from "./Flex";
import Container from "./Container";
import Box from "./Box";
import ThemeChanger from "./ThemeChanger";
import { useRouter } from "next/router";
import { state } from "../state";
import Heading from "./Heading";
import Text from "./Text";
import Spinner from "./Spinner";
import truncate from "../utils/truncate";
import ButtonGroup from "./ButtonGroup";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "./Dialog";
import PanelBox from "./PanelBox";
const Navigation = () => {
const { data: session, status } = useSession();
const router = useRouter();
const snap = useSnapshot(state);
const slug = router.query?.slug;
const gistId = Array.isArray(slug) ? slug[0] : null;
return (
<Box
as="nav"
css={{
display: "flex",
height: "60px",
borderBottom: "1px solid $mauve6",
position: "relative",
zIndex: 2003,
@@ -46,27 +48,276 @@ const Navigation = () => {
css={{
display: "flex",
alignItems: "center",
py: "$2",
}}
>
<Link href="/" passHref>
<Box
as="a"
css={{
display: "flex",
alignItems: "center",
color: "$textColor",
}}
>
<Logo width="30px" height="30px" />
</Box>
</Link>
<Flex
css={{
flex: 1,
alignItems: "center",
borderRight: "1px solid $colors$mauve6",
py: "$3",
pr: "$4",
}}
>
<Link href="/" passHref>
<Box
as="a"
css={{
display: "flex",
alignItems: "center",
color: "$textColor",
}}
>
<Logo width="30px" height="30px" />
</Box>
</Link>
<Flex
css={{
ml: "$5",
flexDirection: "column",
gap: "1px",
}}
>
{snap.loading ? (
<Spinner />
) : (
<>
<Heading css={{ lineHeight: 1 }}>
{snap.files?.[0]?.name || "XRPL Hooks"}
</Heading>
<Text
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
>
{snap.files.length > 0 ? "Gist: " : "Playground"}
<Text css={{ color: "$mauve12" }}>
{snap.files.length > 0 &&
`${snap.gistOwner || "-"}/${truncate(snap.gistId || "")}`}
</Text>
</Text>
</>
)}
</Flex>
<ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<DialogTrigger asChild>
<Button outline>
<FolderOpen size="15px" />
</Button>
</DialogTrigger>
<DialogContent
css={{
maxWidth: "100%",
width: "80vw",
height: "80%",
backgroundColor: "$mauve1 !important",
overflowY: "auto",
p: 0,
}}
>
<Flex
css={{
flexDirection: "column",
flex: 1,
height: "auto",
"@md": {
flexDirection: "row",
height: "100%",
},
}}
>
<Flex
css={{
borderBottom: "1px solid $colors$mauve5",
width: "100%",
flexDirection: "column",
p: "$7",
height: "100%",
"@md": {
width: "30%",
borderBottom: "0px",
borderRight: "1px solid $colors$mauve5",
},
}}
>
<DialogTitle
css={{
textTransform: "uppercase",
display: "inline-flex",
alignItems: "center",
gap: "$3",
fontSize: "$xl",
}}
>
<Logo width="30px" height="30px" /> XRPL Hooks Editor
</DialogTitle>
<DialogDescription as="div">
<Text
css={{
display: "inline-flex",
color: "inherit",
my: "$5",
mb: "$7",
}}
>
Hooks add smart contract functionality to the XRP
Ledger.
</Text>
<Flex
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://github.com/XRPL-Labs/xrpld-hooks"
>
<ArrowUpRight size="15px" /> Developing Hooks
</Text>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl-hooks.readme.io/docs"
>
<ArrowUpRight size="15px" /> Hooks documentation
</Text>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$green9",
"&:hover": {
color: "$green11 !important",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl.org/docs.html"
>
<ArrowUpRight size="15px" /> XRPL documentation
</Text>
</Flex>
</DialogDescription>
</Flex>
<Flex
css={{
display: "grid",
gridTemplateColumns: "1fr",
gridTemplateRows: "max-content",
flex: 1,
p: "$7",
gap: "$3",
alignItems: "flex-start",
flexWrap: "wrap",
backgroundImage: `url('/pattern.svg'), url('/pattern-2.svg')`,
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left, top right",
"@md": {
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "max-content",
},
}}
>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Starter</Heading>
<Text>Just an empty starter with essential imports</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Accept</Heading>
<Text>
This hook just accepts any transaction coming through it
</Text>
</PanelBox>
<PanelBox
as="a"
href="/develop/be088224fb37c0075e84491da0e602c1"
>
<Heading>Accept</Heading>
<Text>
This hook just accepts any transaction coming through it
</Text>
</PanelBox>
</Flex>
</Flex>
<DialogClose asChild>
<Box
css={{
position: "absolute",
top: "$1",
right: "$1",
cursor: "pointer",
background: "$mauve1",
display: "flex",
borderRadius: "$full",
p: "$1",
}}
>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<ThemeChanger />
</ButtonGroup>
</Flex>
<Flex
css={{
flexWrap: "nowrap",
marginLeft: "$3",
marginLeft: "$4",
overflowX: "scroll",
"&::-webkit-scrollbar": {
height: 0,
background: "transparent",
},
}}
>
<Stack
@@ -75,117 +326,55 @@ const Navigation = () => {
gap: "$3",
flexWrap: "nowrap",
alignItems: "center",
pr: "$3",
marginLeft: "auto",
}}
>
<Link href="/develop" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
<ButtonGroup>
<Link
href={gistId ? `/develop/${gistId}` : "/develop"}
passHref
shallow
>
Develop
</Button>
</Link>
<Link href="/deploy" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
>
Develop
</Button>
</Link>
<Link
href={gistId ? `/deploy/${gistId}` : "/deploy"}
passHref
shallow
>
Deploy
</Button>
</Link>
<Link href="/test" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
Deploy
</Button>
</Link>
<Link
href={gistId ? `/test/${gistId}` : "/test"}
passHref
shallow
>
Test
</Button>
</Link>
<ThemeChanger />
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
Test
</Button>
</Link>
</ButtonGroup>
<Button outline disabled>
<BookOpen size="15px" />
</Button>
</Stack>
</Flex>
<Stack
css={{
color: "text",
ml: "auto",
flexWrap: "nowrap",
marginLeft: "$3",
"@sm": {
marginLeft: "auto",
},
}}
>
{status === "authenticated" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Box
css={{
borderRadius: "$full",
overflow: "hidden",
width: "30px",
height: "30px",
position: "relative",
}}
>
<Image
src={session?.user?.image || ""}
width="30px"
height="30px"
objectFit="cover"
alt="User avatar"
/>
</Box>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} (
{session?.user.name})
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(
`http://gist.github.com/${session?.user.username}`
)
}
>
<ArrowSquareOut size="16px" />
Go to your Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Gear size="16px" /> Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()}>
<SignOut size="16px" /> Log out
</DropdownMenuItem>
<DropdownMenuArrow offset={10} />
</DropdownMenuContent>
</DropdownMenu>
)}
{status !== "authenticated" && (
<Button
isLoading={status === "loading"}
outline
onClick={() => signIn("github")}
>
<GithubLogo size="16px" /> GitHub Login
</Button>
)}
{/* <Box
css={{
border: "1px solid",
borderRadius: "3px",
borderColor: "text",
p: 1,
}}
>
<BookOpen size="20px" />
</Box> */}
</Stack>
</Container>
</Box>
);

30
components/PanelBox.tsx Normal file
View File

@@ -0,0 +1,30 @@
import { styled } from "../stitches.config";
import Heading from "./Heading";
import Text from "./Text";
const PanelBox = styled("div", {
display: "flex",
flexDirection: "column",
border: "1px solid $colors$mauve5",
backgroundColor: "$mauve1",
padding: "$3",
borderRadius: "$sm",
fontWeight: "lighter",
height: "auto",
cursor: "pointer",
flex: "1 1 0px",
"&:hover": {
border: "1px solid $colors$mauve9",
},
[`& ${Heading}`]: {
fontWeight: "lighter",
mb: "$2",
},
[`& ${Text}`]: {
fontWeight: "lighter",
color: "$mauve10",
fontSize: "$sm",
},
});
export default PanelBox;

View File

@@ -1,8 +1,5 @@
import { Children } from "react";
import Box from "./Box";
import { styled } from "../stitches.config";
import type * as Stitches from "@stitches/react";
const StackComponent = styled(Box, {
display: "flex",

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import { Sun, Moon } from "phosphor-react";
import Box from "./Box";
import Button from "./Button";
const ThemeChanger = () => {
const { theme, setTheme } = useTheme();
@@ -12,7 +12,8 @@ const ThemeChanger = () => {
if (!mounted) return null;
return (
<Box
<Button
outline
onClick={() => {
setTheme(theme && theme === "light" ? "dark" : "light");
}}
@@ -25,12 +26,8 @@ const ThemeChanger = () => {
color: "$muted",
}}
>
{theme === "dark" ? (
<Sun weight="bold" size="16px" />
) : (
<Moon weight="bold" size="16px" />
)}
</Box>
{theme === "dark" ? <Sun size="15px" /> : <Moon size="15px" />}
</Button>
);
};

View File

@@ -12,18 +12,22 @@
"@monaco-editor/react": "^4.3.1",
"@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1",
"@radix-ui/react-dialog": "^0.1.1",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@stitches/react": "^1.2.5",
"@radix-ui/react-id": "^0.1.1",
"@stitches/react": "^1.2.6-0",
"monaco-editor": "^0.29.1",
"next": "^12.0.4",
"next-auth": "^4.0.0-beta.5",
"next-themes": "^0.0.15",
"octokit": "^1.7.0",
"phosphor-react": "^1.3.1",
"re-resizable": "^6.9.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hot-toast": "^2.1.1",
"react-new-window": "^0.2.1",
"valtio": "^1.2.5"
},
"devDependencies": {

View File

@@ -1,30 +1,67 @@
import { useEffect } from "react";
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Head from "next/head";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast";
import { useRouter } from "next/router";
import { IdProvider } from "@radix-ui/react-id";
import { darkTheme } from "../stitches.config";
import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation";
import { fetchFiles, state } from "../state";
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
const router = useRouter();
const slug = router.query?.slug;
const gistId = (Array.isArray(slug) && slug[0]) ?? null;
useEffect(() => {
if (router.pathname.includes("/develop")) {
if (gistId && router.isReady) {
fetchFiles(gistId);
} else {
if (!gistId && router.isReady) {
state.mainModalOpen = true;
}
}
}
}, [gistId, router.isReady, router.pathname]);
return (
<>
<SessionProvider session={session}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
value={{
light: "light",
dark: darkTheme.className,
}}
>
<Navigation />
<Component {...pageProps} />
<Toaster />
</ThemeProvider>
</SessionProvider>
<Head>
<title>XRPL Hooks Playground</title>
</Head>
<IdProvider>
<SessionProvider session={session}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
value={{
light: "light",
dark: darkTheme.className,
}}
>
<Navigation />
<Component {...pageProps} />
<Toaster
toastOptions={{
className: css({
backgroundColor: "$mauve1",
color: "$mauve10",
fontSize: "$sm",
".dark &": {
backgroundColor: "$mauve4",
color: "$mauve12",
},
})(),
}}
/>
</ThemeProvider>
</SessionProvider>
</IdProvider>
</>
);
}

View File

@@ -1,6 +1,5 @@
import type { NextRequest, NextFetchEvent } from 'next/server';
import { NextResponse as Response } from 'next/server';
import { getToken } from "next-auth/jwt"
export default function middleware(req: NextRequest, ev: NextFetchEvent) {

View File

@@ -1,5 +1,4 @@
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
export default NextAuth({
// Configure one or more authentication providers
@@ -42,7 +41,6 @@ export default NextAuth({
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
const user = { ...session.user, username: token.username };
session['user']['username'] = token.username as string;
return session
}

View File

@@ -1,7 +1,7 @@
import type { NextPage } from "next";
import Head from "next/head";
import dynamic from "next/dynamic";
import type { NextPage } from "next";
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
ssr: false,
});
@@ -13,9 +13,6 @@ const Footer = dynamic(() => import("../../components/Footer"), {
const Home: NextPage = () => {
return (
<>
<Head>
<title>XRPL Hooks Playground</title>
</Head>
<main style={{ display: "flex", flex: 1 }}>
<HooksEditor />
</main>

38
pages/sign-in.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { useEffect } from "react";
import { signIn, useSession } from "next-auth/react";
import Box from "../components/Box";
import Spinner from "../components/Spinner";
const SignInPage = () => {
const { data: session, status } = useSession();
useEffect(() => {
if (status !== "loading" && !session)
void signIn("github", { redirect: false });
if (status !== "loading" && session) window.close();
}, [session, status]);
return (
<Box
css={{
display: "flex",
backgroundColor: "$mauve1",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 9999,
textAlign: "center",
justifyContent: "center",
alignItems: "center",
gap: "$2",
}}
>
Logging in <Spinner />
</Box>
);
};
export default SignInPage;

9
public/pattern-2.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 138 KiB

9
public/pattern.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 134 KiB

136
state.ts
View File

@@ -1,8 +1,10 @@
import { proxy, subscribe } from 'valtio';
import { proxy } from 'valtio';
import { devtools } from 'valtio/utils';
import { Octokit } from '@octokit/core';
import type monaco from 'monaco-editor';
import toast from 'react-hot-toast';
import Router from 'next/router';
import type { Session } from 'next-auth';
const octokit = new Octokit();
@@ -14,8 +16,12 @@ interface File {
interface IState {
files: File[],
gistId?: string | null,
gistOwner?: string | null,
gistName?: string | null,
active: number;
loading: boolean;
gistLoading: boolean;
compiling: boolean;
logs: {
type: 'error' | 'warning' | 'log',
@@ -24,10 +30,11 @@ interface IState {
editorCtx?: typeof monaco.editor;
editorSettings: {
tabSize: number;
}
},
mainModalOpen: boolean;
}
let localStorageState: null | string = null;
// let localStorageState: null | string = null;
let initialState = {
files: [],
active: 0,
@@ -35,31 +42,38 @@ let initialState = {
compiling: false,
logs: [],
editorCtx: undefined,
gistId: undefined,
gistOwner: undefined,
gistName: undefined,
gistLoading: false,
editorSettings: {
tabSize: 2
}
},
mainModalOpen: false
}
// Check if there's a persited state in localStorage
if (typeof window !== 'undefined') {
try {
localStorageState = localStorage.getItem('hooksIdeState');
} catch (err) {
console.log(`localStorage state broken`);
localStorage.removeItem('hooksIdeState');
}
}
if (localStorageState) {
initialState = JSON.parse(localStorageState);
}
// if (typeof window !== 'undefined') {
// try {
// localStorageState = localStorage.getItem('hooksIdeState');
// } catch (err) {
// console.log(`localStorage state broken`);
// localStorage.removeItem('hooksIdeState');
// }
// }
// if (localStorageState) {
// initialState = JSON.parse(localStorageState);
// }
// Initialize state
export const state = proxy<IState>(initialState);
export const state = proxy<IState>({ ...initialState, logs: [] });
// Fetch content from Githug Gists
export const fetchFiles = (gistId: string) => {
state.loading = true;
if (gistId) {
state.logs.push({ type: 'log', message: `Fetching Gist with id: ${gistId}` });
octokit.request("GET /gists/{gist_id}", { gist_id: gistId }).then(res => {
if (res.data.files && Object.keys(res.data.files).length > 0) {
const files = Object.keys(res.data.files).map(filename => ({
@@ -71,11 +85,17 @@ export const fetchFiles = (gistId: string) => {
if (files.length > 0) {
state.logs.push({ type: 'log', message: 'Fetched successfully ✅' })
state.files = files;
state.gistId = gistId;
state.gistName = Object.keys(res.data.files)?.[0] || 'untitled';
state.gistOwner = res.data.owner?.login;
return
} else {
// Open main modal if now files
state.mainModalOpen = true;
}
return
return Router.push({ pathname: '/develop' })
}
state.loading = false;
}).catch(err => {
state.loading = false;
state.logs.push({ type: 'error', message: `Couldn't find Gist with id: ${gistId}` })
@@ -87,9 +107,72 @@ export const fetchFiles = (gistId: string) => {
// return state.files = initFiles
}
export const syncToGist = async (session?: Session | null, createNewGist?: boolean) => {
let files: Record<string, { filename: string, content: string }> = {};
state.gistLoading = true;
if (!session || !session.user) {
state.gistLoading = false;
return toast.error('You need to be logged in!')
}
const toastId = toast.loading('Pushing to Gist');
if (!state.files || !state.files.length) {
state.gistLoading = false;
return toast.error(`You need to create some files we can push to gist`, { id: toastId })
}
if (state.gistId && session?.user.username === state.gistOwner && !createNewGist) {
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", { gist_id: state.gistId });
if (currentFilesRes.data.files) {
Object.keys(currentFilesRes?.data?.files).forEach(filename => {
files[`${filename}`] = { filename, content: "" }
})
}
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
// Update existing Gist
octokit.request("PATCH /gists/{gist_id}", {
gist_id: state.gistId, files, headers: {
authorization: `token ${session?.accessToken || ''}`
}
}).then(res => {
state.gistLoading = false;
return toast.success('Updated to gist successfully!', { id: toastId })
}).catch(err => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not update Gist, try again later!`, { id: toastId })
})
} else {
// Not Gist of the current user or it isn't Gist yet
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
octokit.request("POST /gists", {
files,
public: true,
headers: {
authorization: `token ${session?.accessToken || ''}`
}
}).then(res => {
state.gistLoading = false;
state.gistOwner = res.data.owner?.login;
state.gistId = res.data.id;
state.gistName = Array.isArray(res.data.files) ? Object.keys(res.data?.files)?.[0] : 'Untitled';
Router.push({ pathname: `/develop/${res.data.id}` })
return toast.success('Created new gist successfully!', { id: toastId })
}).catch(err => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not create Gist, try again later!`, { id: toastId })
})
}
}
export const updateEditorSettings = (editorSettings: IState['editorSettings']) => {
state.editorCtx?.getModels().forEach(model => {
console.log(model.uri)
model.updateOptions({
...editorSettings
})
@@ -100,12 +183,9 @@ export const updateEditorSettings = (editorSettings: IState['editorSettings']) =
export const saveFile = (value: string) => {
const editorModels = state.editorCtx?.getModels();
const currentModel = editorModels?.find(editorModel => editorModel.uri.path === `/${state.files[state.active].name}`);
console.log(currentModel?.getValue())
if (state.files.length > 0) {
console.log('häää')
state.files[state.active].content = currentModel?.getValue() || '';
}
console.log(state.files[state.active])
toast.success('Saved successfully', { position: 'bottom-center' })
}
@@ -164,9 +244,11 @@ export const compileCode = async (activeId: number) => {
}
}
const unsub = devtools(state, 'Files State');
if (process.env.NODE_ENV !== 'production') {
devtools(state, 'Files State');
}
subscribe(state, () => {
const { editorCtx, ...storedState } = state;
localStorage.setItem('hooksIdeState', JSON.stringify(storedState))
});
// subscribe(state, () => {
// const { editorCtx, ...storedState } = state;
// localStorage.setItem('hooksIdeState', JSON.stringify(storedState))
// });

View File

@@ -191,7 +191,7 @@ export const {
},
fontWeights: {
body: 400,
heading: 400,
heading: 700,
bold: 700,
},
lineHeights: {

View File

@@ -17,7 +17,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"noUnusedLocals": true
},
"include": [
"next-env.d.ts",

8
utils/truncate.ts Normal file
View File

@@ -0,0 +1,8 @@
const truncate = (str: string, max: number = 8) => {
const array = str.trim().split('');
const ellipsis = array.length > max ? '...' : '';
return array.slice(0, max).join('') + ellipsis;
};
export default truncate

View File

@@ -487,6 +487,18 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-alert-dialog@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.1.1.tgz#979dc343bc2766b0049e4940a49867a3c4b002f8"
integrity sha512-2oiBy38KK/hZZFpAsjUibxjbKv+HXwsGQUcKKXZH5/D+9BPScKcF192r7aAbQer3NNMHuPEOGuGeYKWegUEzTA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.1.0"
"@radix-ui/react-compose-refs" "0.1.0"
"@radix-ui/react-context" "0.1.1"
"@radix-ui/react-dialog" "0.1.1"
"@radix-ui/react-slot" "0.1.1"
"@radix-ui/react-arrow@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-0.1.1.tgz#e8e05444b37b9f71bf712a8cd4dd07dbd419e749"
@@ -520,7 +532,7 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dialog@^0.1.1":
"@radix-ui/react-dialog@0.1.1", "@radix-ui/react-dialog@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.1.1.tgz#515bcda3ab6a3a9b1cc20237eedc38b6c5921a7d"
integrity sha512-+M/tY9n2/5yhpZrWankiVPJPFHkmgn4q+lXOeVRkMOFsyXQKhwhmPihYFUBk3BszsdKeV5BrZvdDpbWve3ZKKA==
@@ -584,7 +596,7 @@
"@radix-ui/react-primitive" "0.1.1"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-id@0.1.1":
"@radix-ui/react-id@0.1.1", "@radix-ui/react-id@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-0.1.1.tgz#42c8f3967875e6824b2ac9d49c66317047c8d6ff"
integrity sha512-Vlg5me65+NUgxPBuA0Lk6FerNe+Mq4EuJ8xzpskGxS2t8p1puI3IkyLZ2wWtDSb1KXazoaHn8adBypagt+1P0g==
@@ -752,10 +764,10 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.8.tgz#be3e914e84eacf16dbebd311c0d0b44aa1174c64"
integrity sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw==
"@stitches/react@^1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.5.tgz#2353343c220f0c59ba388a26fdd9ff7962cb6031"
integrity sha512-95Wwjp5cvoYQjg616OBiHZM1PnIF51pnFQIgSPxPzS/xXBrer9sNO1tfpVfLYfOifvuotse2IFNbypJ92BXzvg==
"@stitches/react@^1.2.6-0":
version "1.2.6-0"
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.6-0.tgz#ea7a1d35940f96337d18f5afd41aee60cbdcede7"
integrity sha512-hws7FTFTatR5RhLkg6lFWzvGXpZYNA9OqAA8HC/jCJDDvfEpN/8+9DjTX7k7/4oEWyaZBOfALAyHj/YrgScjxQ==
"@types/aws-lambda@^8.10.83":
version "8.10.84"
@@ -1883,6 +1895,11 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fast-memoize@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
@@ -3187,6 +3204,13 @@ raw-body@2.4.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
re-resizable@^6.9.1:
version "6.9.1"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.1.tgz#6be082b55d02364ca4bfee139e04feebdf52441c"
integrity sha512-KRYAgr9/j1PJ3K+t+MBhlQ+qkkoLDJ1rs0z1heIWvYbCW/9Vq4djDU+QumJ3hQbwwtzXF6OInla6rOx6hhgRhQ==
dependencies:
fast-memoize "^2.5.1"
react-dom@17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
@@ -3213,6 +3237,13 @@ react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-new-window@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/react-new-window/-/react-new-window-0.2.1.tgz#510aa91ba0334f0d42ba499c617a37f46c4d84ba"
integrity sha512-3s6W8eipRbjoLGvWpFRPSt2/z5h9FTS01C+ZjaEZX7/cF6ejGc6JVuL4oEo0s73IluaqoLIDB49AK4fSMW8pJw==
dependencies:
prop-types "^15.7.2"
react-refresh@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"