Skip to content
This repository has been archived by the owner on Nov 16, 2024. It is now read-only.

Commit

Permalink
feat: add copy json button
Browse files Browse the repository at this point in the history
  • Loading branch information
aiktb committed Oct 22, 2024
1 parent c77fb81 commit d525f6b
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 63 deletions.
66 changes: 42 additions & 24 deletions app/components/ast-json-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,52 @@ interface AstJsonPreviewProps {

const AstJsonPreview = ({ ast }: AstJsonPreviewProps) => {
const [highlightedAstJson, setHighlightedAstJson] = useState<string>("");

useEffect(() => {
const highlightJson = async () => {
const json = JSON.stringify(ast, null, 2);
const highlighted = await codeToHtml(json, {
lang: "json",
themes: {
light: "github-light-high-contrast",
dark: "github-dark-high-contrast",
},
colorReplacements: {
"github-dark-high-contrast": {
"#0a0c10": "#0d1117",
},
},
});
setHighlightedAstJson(highlighted);
};

highlightJson();
}, [ast]);
const jsonCode = JSON.stringify(ast, null, 2);
codeToHtml(jsonCode, {
lang: "json",
themes: {
light: "github-light-high-contrast",
dark: "github-dark-high-contrast",
},
colorReplacements: {
"github-dark-high-contrast": {
"#0a0c10": "#0d1117",
},
},
}).then(setHighlightedAstJson);

return (
<div className="text-sm">
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki has escaped html. */}
<div dangerouslySetInnerHTML={{ __html: highlightedAstJson }} />
<div className="relative">
<div className="h-screen overflow-scroll">
<CopyTextButton text={jsonCode} className="absolute top-0 right-0" />
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki has escaped html. */}
<div dangerouslySetInnerHTML={{ __html: highlightedAstJson }} />
</div>
</div>
);
};

export default AstJsonPreview;

const CopyTextButton = ({ text, className }: { text: string; className: string }) => {
const [copied, setCopied] = useState(false);
return (
<Button
size="icon"
variant="outline"
className={className}
onClick={() => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
>
<span className="sr-only">Copy JSON code</span>
{copied ? (
<span className="i-lucide-check-check size-4 animate-pulse" />
) : (
<span className="i-radix-icons-copy size-4" />
)}
</Button>
);
};
54 changes: 28 additions & 26 deletions app/components/code-mirror-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { EditorView, keymap } from "@codemirror/view";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror from "@uiw/react-codemirror";
import { useTheme } from "next-themes";
import parserBabel from "prettier/parser-babel";
import parserHtml from "prettier/parser-html";
import parserTypeScript from "prettier/parser-typescript";
import * as parserBabel from "prettier/parser-babel";
import * as parserHtml from "prettier/parser-html";
import * as parserPostCSS from "prettier/parser-postcss";
import * as parserTypeScript from "prettier/parser-typescript";
import prettier from "prettier/standalone";

interface CodeMirrorEditorProps {
Expand All @@ -30,6 +31,7 @@ const CodeMirrorEditorProps = ({ code, onChange }: CodeMirrorEditorProps) => {
borderRight: "none",
},
});

const customKeymap = keymap.of([
{
key: "Ctrl-s",
Expand All @@ -43,22 +45,22 @@ const CodeMirrorEditorProps = ({ code, onChange }: CodeMirrorEditorProps) => {
key: "Shift-Alt-f",
mac: "Shift-Cmd-f",
run: (editor) => {
new Promise<string>((resolve) => {
resolve(
prettier.format(editor.state.doc.toString(), {
parser: "vue",
plugins: [parserHtml, parserBabel, parserTypeScript],
}),
);
}).then((formatted) => {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: formatted,
},
prettier
.formatWithCursor(editor.state.doc.toString(), {
parser: "vue",
plugins: [parserHtml, parserBabel, parserTypeScript, parserPostCSS],
cursorOffset: editor.state.selection.main.head,
})
.then(({ formatted, cursorOffset }) => {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: formatted,
},
selection: { anchor: cursorOffset, head: cursorOffset },
});
});
});
return true;
},
},
Expand All @@ -71,6 +73,7 @@ const CodeMirrorEditorProps = ({ code, onChange }: CodeMirrorEditorProps) => {
value={code}
className="h-full"
height="100%"
autoFocus
theme={theme === "light" ? githubLight : githubDark}
extensions={[vue(), styleTheme, customKeymap]}
/>
Expand All @@ -82,7 +85,7 @@ export default CodeMirrorEditorProps;

const Kbd = ({ children }: { children: string }) => {
return (
<kbd className="inline-flex h-5 min-w-5 items-center justify-center rounded bg-neutral-50 px-1 font-medium font-mono text-[10px] text-neutral-900 ring-1 ring-neutral-300 ring-inset dark:bg-neutral-800 dark:text-white dark:ring-neutral-700">
<kbd className="rounded font-medium font-mono text-[12px] text-neutral-900 uppercase dark:text-white">
{children}
</kbd>
);
Expand All @@ -98,14 +101,13 @@ const ShortcutTips = ({ className }: { className: string }) => {
<span className="i-radix-icons-keyboard size-4" />
</Button>
</TooltipTrigger>
<TooltipContent className="text-sm">
<p className="flex items-center gap-0.5">
Save: <Kbd>{isMacOS ? "⌘" : "Ctrl"}</Kbd>
<Kbd>S</Kbd>
<TooltipContent>
<p className="text-sm">
Save: <Kbd>{isMacOS ? "⌘" : "Ctrl"}</Kbd> + <Kbd>S</Kbd>
</p>
<p className="mt-1 flex items-center gap-0.5">
Format: <Kbd>{isMacOS ? "⇧" : "Shift"}</Kbd> <Kbd>{isMacOS ? "⌥ " : "Alt"}</Kbd>
<Kbd>F</Kbd>
<p className="mt-1 text-sm">
Format: <Kbd>{isMacOS ? "⇧" : "Shift"}</Kbd> + <Kbd>{isMacOS ? "⌥ " : "Alt"}</Kbd>
{" +"} <Kbd> F </Kbd>
</p>
</TooltipContent>
</Tooltip>
Expand Down
4 changes: 2 additions & 2 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import "@fontsource/fira-code/700.css";
export function Layout({ children }: { children: React.ReactNode }) {
return (
// If you do not add suppressHydrationWarning to your <html> you will get warnings because next-themes updates that element.
<html lang="en" suppressHydrationWarning className="font-sans">
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand All @@ -20,7 +20,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<Meta />
<Links />
</head>
<body>
<body className="font-sans selection:bg-[#BBDFFF] dark:selection:bg-[#003D73]">
<ThemeProvider attribute="class" disableTransitionOnChange>
{children}
</ThemeProvider>
Expand Down
25 changes: 14 additions & 11 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MetaFunction } from "@netlify/remix-runtime";
import { type SFCParseResult, parse } from "@vue/compiler-sfc";
import { parse } from "@vue/compiler-sfc";

import { ClientOnly } from "remix-utils/client-only";
import AstJsonPreview from "~/components/ast-json-preview";
Expand All @@ -16,38 +16,41 @@ export const meta: MetaFunction = () => {
];
};

export default function Index() {
const exampleCode = `<script setup lang="ts">
const exampleCode = `<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<span>{{ count }}</span>
<span class="count">{{ count }}</span>
<button @click="count++">Add</button>
</template>
<style scoped>
.count {
color: green;
}
</style>
`;
const [code, setCode] = useState<string>(exampleCode);
const [ast, setAst] = useState<SFCParseResult>(parse(code));

useEffect(() => {
setAst(parse(code));
}, [code]);
export default function Index() {
const [code, setCode] = useState(exampleCode);
const ast = parse(code);

return (
<div className="relative h-screen">
<Header className="sticky top-0 z-20 h-12 w-full" />
<main className="h-[calc(100%-48px)] overflow-scroll">
<ResizablePanelGroup direction="horizontal" className="w-full overflow-scroll">
<ResizablePanel defaultSize={50}>
<div className="h-full items-center justify-center overflow-scroll p-6">
<div className="h-full items-center justify-center p-6">
<ClientOnly>{() => <CodeMirrorEditor code={code} onChange={setCode} />}</ClientOnly>
</div>
</ResizablePanel>
<ResizableHandle withHandle={true} />
<ResizablePanel defaultSize={50}>
<div className="max-h-full items-center justify-center overflow-scroll p-6">
<div className="h-full items-center justify-center p-6">
<AstJsonPreview ast={ast} />
</div>
</ResizablePanel>
Expand Down

0 comments on commit d525f6b

Please sign in to comment.