Initial open source commit
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
- name: Publish app
|
||||
uses: cloudflare/wrangler-action@1.3.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
environment: "production"
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
|
||||
/.cache
|
||||
/build
|
||||
/public/build
|
||||
.env
|
||||
/app/tailwind.css
|
||||
/jsonDocs
|
||||
.DS_Store
|
||||
/dist
|
||||
.mf
|
||||
/meta.json
|
||||
/stats.html
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost with document",
|
||||
"url": "http://localhost:8787",
|
||||
"webRoot": "${workspaceFolder}/app"
|
||||
},
|
||||
{
|
||||
"name": "Debug Jest All Tests",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/.bin/jest",
|
||||
"--runInBand"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"name": "Debug Jest Test File",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/.bin/jest",
|
||||
"--runInBand"
|
||||
],
|
||||
"args": ["${fileBasename}", "--no-cache"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# JSONHero
|
||||
|
||||
## Development
|
||||
|
||||
From your terminal:
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
|
||||
This will build JSONHero in development mode and start the local miniflare server. Should now be available at [http://localhost:8787](http://localhost:8787)
|
||||
|
||||
## Deploy
|
||||
|
||||
Use [wrangler](https://developers.cloudflare.com/workers/cli-wrangler) to build and deploy your application to Cloudflare Workers. If you don't have it yet, follow [the installation guide](https://developers.cloudflare.com/workers/cli-wrangler/install-update) to get it setup. Be sure to [authenticate the CLI](https://developers.cloudflare.com/workers/cli-wrangler/authentication) as well.
|
||||
|
||||
If you don't already have an account, then [create a cloudflare account here](https://dash.cloudflare.com/sign-up) and after verifying your email address with Cloudflare, go to your dashboard and set up your free custom Cloudflare Workers subdomain.
|
||||
|
||||
Once that's done, you should be able to deploy your app:
|
||||
|
||||
```sh
|
||||
npm run deploy
|
||||
```
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
const DOCUMENTS: KVNamespace;
|
||||
const SESSION_SECRET: string;
|
||||
const GRAPH_JSON_API_KEY: string;
|
||||
const GRAPH_JSON_COLLECTION: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useOnScreen } from "~/hooks/useOnScreen";
|
||||
|
||||
export function AutoplayVideo({ src }: { src: string }) {
|
||||
const elementRef = useRef<HTMLVideoElement>(null);
|
||||
const isOnScreen = useOnScreen(elementRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnScreen) {
|
||||
elementRef.current?.play();
|
||||
} else {
|
||||
elementRef.current?.pause();
|
||||
}
|
||||
}, [isOnScreen]);
|
||||
|
||||
return <video src={src} ref={elementRef} loop={true} muted={true} />;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import {
|
||||
EditorView,
|
||||
TransactionSpec,
|
||||
useCodeMirror,
|
||||
ViewUpdate,
|
||||
} from "@uiw/react-codemirror";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { getEditorSetup } from "~/utilities/codeMirrorSetup";
|
||||
import { darkTheme, lightTheme } from "~/utilities/codeMirrorTheme";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
|
||||
export type CodeEditorProps = {
|
||||
content: string;
|
||||
language?: "json";
|
||||
readOnly?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onUpdate?: (update: ViewUpdate) => void;
|
||||
selection?: { start: number; end: number };
|
||||
};
|
||||
|
||||
const languages = {
|
||||
json: jsonLang,
|
||||
};
|
||||
|
||||
type CodeEditorDefaultProps = Required<
|
||||
Omit<CodeEditorProps, "content" | "onChange" | "onUpdate">
|
||||
>;
|
||||
|
||||
const defaultProps: CodeEditorDefaultProps = {
|
||||
language: "json",
|
||||
readOnly: true,
|
||||
selection: { start: 0, end: 0 },
|
||||
};
|
||||
|
||||
export function CodeEditor(opts: CodeEditorProps) {
|
||||
const { content, language, readOnly, onChange, onUpdate, selection } = {
|
||||
...defaultProps,
|
||||
...opts,
|
||||
};
|
||||
|
||||
const [theme] = useTheme();
|
||||
|
||||
const extensions = getEditorSetup();
|
||||
|
||||
const languageExtension = languages[language];
|
||||
|
||||
extensions.push(languageExtension());
|
||||
|
||||
const editor = useRef(null);
|
||||
const { setContainer, view, state } = useCodeMirror({
|
||||
container: editor.current,
|
||||
extensions,
|
||||
editable: !readOnly,
|
||||
contentEditable: !readOnly,
|
||||
value: content,
|
||||
autoFocus: false,
|
||||
theme: theme === "light" ? lightTheme() : darkTheme(),
|
||||
indentWithTab: false,
|
||||
basicSetup: false,
|
||||
onChange,
|
||||
onUpdate,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
setContainer(editor.current);
|
||||
}
|
||||
}, [editor.current]);
|
||||
|
||||
const setSelectionRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (setSelectionRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (view) {
|
||||
setSelectionRef.current = true;
|
||||
|
||||
const lineNumber = state?.doc.lineAt(selection?.start ?? 0).number;
|
||||
|
||||
const transactionSpec: TransactionSpec = {
|
||||
selection: { anchor: selection.start, head: selection.end },
|
||||
effects: EditorView.scrollIntoView(selection.start, {
|
||||
y: "start",
|
||||
yMargin: 100,
|
||||
}),
|
||||
};
|
||||
|
||||
view.dispatch(transactionSpec);
|
||||
}
|
||||
}, [selection, view, setSelectionRef.current]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="h-viewerHeight overflow-y-auto no-scrollbar"
|
||||
ref={editor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import { useCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { getViewerSetup } from "~/utilities/codeMirrorSetup";
|
||||
import { darkTheme, lightTheme } from "~/utilities/codeMirrorTheme";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
|
||||
export function CodeViewer({ code, lang }: { code: string; lang?: "json" }) {
|
||||
const editor = useRef(null);
|
||||
|
||||
const extensions = getViewerSetup();
|
||||
|
||||
if (!lang || lang === "json") {
|
||||
extensions.push(jsonLang());
|
||||
}
|
||||
|
||||
const [theme] = useTheme();
|
||||
|
||||
const { setContainer } = useCodeMirror({
|
||||
container: editor.current,
|
||||
extensions,
|
||||
value: code,
|
||||
editable: false,
|
||||
contentEditable: false,
|
||||
autoFocus: false,
|
||||
basicSetup: false,
|
||||
theme: theme === "light" ? lightTheme() : darkTheme(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
setContainer(editor.current);
|
||||
}
|
||||
}, [editor.current]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={editor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Title } from "./Primitives/Title";
|
||||
import { colorForItemAtPath } from "~/utilities/colors";
|
||||
import { IconComponent } from "~/useColumnView";
|
||||
import { useJson } from "../hooks/useJson";
|
||||
|
||||
export type ColumnProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: IconComponent;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Column(column: ColumnProps) {
|
||||
const { id, title, children } = column;
|
||||
const [json] = useJson();
|
||||
const iconColor = colorForItemAtPath(id, json);
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"column flex-none border-r-[1px] border-slate-300 w-80 transition dark:border-slate-600"
|
||||
}
|
||||
>
|
||||
<div className="flex text-slate-800 bg-slate-50 mb-[3px] p-2 pb-0 transition dark:bg-slate-900 dark:text-slate-300">
|
||||
{column.icon && <column.icon className={`${iconColor} h-6 w-6 mr-1`} />}
|
||||
<Title className="">{title}</Title>
|
||||
</div>
|
||||
<div className="overflow-y-auto h-viewerHeight no-scrollbar">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { Mono } from "./Primitives/Mono";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { ColumnViewNode } from "~/useColumnView";
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "../hooks/useJsonColumnView";
|
||||
import { useJson } from "../hooks/useJson";
|
||||
import { colorForItemAtPath } from "~/utilities/colors";
|
||||
import { Body } from "./Primitives/Body";
|
||||
|
||||
export function ColumnItem({ item }: { item: ColumnViewNode }) {
|
||||
const { title, subtitle, children, id } = item;
|
||||
const [json] = useJson();
|
||||
const { selectedPath, highlightedPath } = useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
const htmlElement = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showArrow = children.length > 0;
|
||||
|
||||
const isHighlighted = (path: string[], id: string) => {
|
||||
return path[path.length - 1] === id;
|
||||
};
|
||||
|
||||
const isSelected = (path: string[], id: string) => {
|
||||
return path.includes(id);
|
||||
};
|
||||
|
||||
const stateStyle = useMemo<string>(() => {
|
||||
if (highlightedPath.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isHighlighted(highlightedPath, id)) {
|
||||
return "bg-slate-300 text-slate-700 hover:bg-slate-400 hover:bg-opacity-60 transition duration-75 ease-out dark:bg-white dark:bg-opacity-[15%] dark:text-slate-400";
|
||||
}
|
||||
|
||||
if (isSelected(selectedPath, id)) {
|
||||
return "bg-slate-200 hover:bg-slate-300 transition duration-75 ease-out dark:bg-white dark:bg-opacity-[5%] dark:hover:bg-white dark:hover:bg-opacity-[10%] dark:text-slate-400";
|
||||
}
|
||||
|
||||
return "hover:bg-slate-100 transition duration-75 ease-out dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:text-slate-400";
|
||||
}, [highlightedPath, selectedPath, id]);
|
||||
|
||||
const iconColor = colorForItemAtPath(id, json);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected(selectedPath, id) || isHighlighted(highlightedPath, id)) {
|
||||
htmlElement.current?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}, [highlightedPath, selectedPath, id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex h-9 items-center justify-items-stretch mx-1 px-1 py-1 my-1 rounded-sm ${stateStyle}`}
|
||||
onClick={() => goToNodeId(id)}
|
||||
ref={htmlElement}
|
||||
>
|
||||
<div className="w-4 flex-none flex-col justify-items-center">
|
||||
{item.icon && <item.icon className={`${iconColor} h-5 w-5`} />}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow flex-shrink items-baseline justify-between truncate">
|
||||
<Body className="flex-grow flex-shrink-0 pl-3 pr-2">{title}</Body>
|
||||
{subtitle && (
|
||||
<Mono className="truncate text-gray-400 pr-1 transition dark:text-gray-500">
|
||||
{subtitle}
|
||||
</Mono>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showArrow && (
|
||||
<ChevronRightIcon className="flex-none w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { inferType } from "@jsonhero/json-infer-types";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { useJson } from "~/hooks/useJson";
|
||||
import { useJsonColumnViewState } from "~/hooks/useJsonColumnView";
|
||||
import { pathToDescendant } from "~/utilities/jsonColumnView";
|
||||
import { JsonPreview } from "./JsonPreview";
|
||||
import { JsonSchemaViewer } from "./JsonSchemaViewer";
|
||||
import { TabContent, Tabs } from "./UI/Tabs";
|
||||
|
||||
const tabs = [
|
||||
{ value: "json", label: "JSON" },
|
||||
{ value: "schema", label: "Schema" },
|
||||
];
|
||||
|
||||
export function ContainerInfo() {
|
||||
const { selectedNodeId, highlightedNodeId } = useJsonColumnViewState();
|
||||
|
||||
if (!selectedNodeId || !highlightedNodeId) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const [json] = useJson();
|
||||
|
||||
const selectedHeroPath = new JSONHeroPath(selectedNodeId);
|
||||
const selectedJson = selectedHeroPath.first(json);
|
||||
const selectedInfo = inferType(selectedJson);
|
||||
|
||||
const isSelectedLeafNode =
|
||||
selectedInfo.name !== "object" && selectedInfo.name !== "array";
|
||||
|
||||
const highlightedHeroPath = new JSONHeroPath(highlightedNodeId);
|
||||
const highlightedJson = highlightedHeroPath.first(json);
|
||||
const highlightedInfo = inferType(highlightedJson);
|
||||
|
||||
const isHighlightedLeafNode =
|
||||
highlightedInfo.name !== "object" && highlightedInfo.name !== "array";
|
||||
|
||||
const shouldHighlightInPreview =
|
||||
selectedNodeId !== highlightedNodeId && !isHighlightedLeafNode;
|
||||
|
||||
const shouldDisplayCodePreview =
|
||||
shouldHighlightInPreview || !isSelectedLeafNode;
|
||||
|
||||
if (!shouldDisplayCodePreview) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs tabs={tabs}>
|
||||
<>
|
||||
<TabContent value="json">
|
||||
{shouldHighlightInPreview ? (
|
||||
<JsonPreview
|
||||
json={highlightedJson}
|
||||
highlightPath={pathToDescendant(
|
||||
highlightedNodeId,
|
||||
selectedNodeId
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<JsonPreview json={selectedJson} />
|
||||
)}
|
||||
</TabContent>
|
||||
<TabContent value="schema">
|
||||
{shouldHighlightInPreview ? (
|
||||
<JsonSchemaViewer path={highlightedNodeId} />
|
||||
) : (
|
||||
<JsonSchemaViewer path={selectedNodeId} />
|
||||
)}
|
||||
</TabContent>
|
||||
</>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export type CopyTextProps = {
|
||||
children: React.ReactNode;
|
||||
value: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function CopyText({ children, value, className }: CopyTextProps) {
|
||||
const onClick = useCallback(() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`${className} relative rounded-sm hover:cursor-pointer transition ease-out after:transition hover:bg-slate-100 hover:dark:bg-slate-700 active:bg-slate-200 after:active:bg-slate-200 dark:active:bg-opacity-70 dark:after:active:bg-opacity-70 after:absolute after:opacity-0 hover:after:opacity-100 after:content-[''] after:bg-[url('/svgs/CopyIcon.svg')] active:after:bg-[url('/svgs/TickIcon.svg')] after:bg-slate-100 after:dark:bg-slate-700 after:bg-no-repeat`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FunctionComponent } from "react";
|
||||
import { CopyText } from "./CopyText";
|
||||
import { Title } from "./Primitives/Title";
|
||||
|
||||
export type DataTableProps = {
|
||||
rows: DataTableRow[];
|
||||
};
|
||||
|
||||
export type DataTableRow = {
|
||||
key: string;
|
||||
value: string;
|
||||
icon?: JSX.Element;
|
||||
};
|
||||
|
||||
export const DataTable: FunctionComponent<DataTableProps> = ({ rows }) => {
|
||||
return (
|
||||
<div>
|
||||
<Title className="text-slate-700 dark:text-slate-400 mb-2">
|
||||
Properties
|
||||
</Title>
|
||||
<table className="w-full table-auto border-y-[0.5px] border-slate-300 transition dark:border-slate-700">
|
||||
<tbody className="divide-solid divide-y divide-slate-300 w-full transition dark:divide-slate-700">
|
||||
{rows.map((row) => {
|
||||
return (
|
||||
<tr
|
||||
key={row.key}
|
||||
className="divide-solid divide-x transition dark:divide-slate-700"
|
||||
>
|
||||
<td className="flex items-baseline py-2 pr-3 text-base dark:text-slate-400">
|
||||
<div className="flex-1 ml-1">{row.key}</div>
|
||||
</td>
|
||||
<td className="text-base text-slate-800 transition dark:text-slate-300 break-all">
|
||||
<CopyText
|
||||
className="p-1 pl-2 m-0.5 mr-0 after:w-4 after:h-4 after:top-1.5 after:right-1"
|
||||
value={row.value}
|
||||
>
|
||||
{row.value}
|
||||
</CopyText>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { PencilAltIcon } from "@heroicons/react/outline";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useFetcher } from "remix";
|
||||
import { match } from "ts-pattern";
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
|
||||
export function DocumentTitle() {
|
||||
const { doc } = useJsonDoc();
|
||||
const [editedTitle, setEditedTitle] = useState(doc.title);
|
||||
const updateDoc = useFetcher();
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (updateDoc.type === "done" && updateDoc.data.title) {
|
||||
ref.current?.blur();
|
||||
}
|
||||
}, [updateDoc]);
|
||||
|
||||
const startEditing = useCallback(() => {
|
||||
ref.current?.select();
|
||||
ref.current?.focus();
|
||||
}, [ref.current]);
|
||||
|
||||
return (
|
||||
<updateDoc.Form method="post" action={`/actions/${doc.id}/update`}>
|
||||
<div
|
||||
className="flex justify-center items-center w-full"
|
||||
title={doc.title}
|
||||
>
|
||||
<input
|
||||
ref={ref}
|
||||
className={
|
||||
"min-w-[15vw] border-none text-center text-ellipsis text-slate-300 px-2 pl-3 py-1 rounded-sm bg-transparent placeholder:text-slate-400 focus:bg-black focus:bg-opacity-30 focus:outline-none focus:border-none hover:bg-black hover:bg-opacity-30 hover:cursor-text transition dark:bg-transparent dark:text-slate-200 dark:placeholder:text-slate-400 dark:focus:bg-black dark:focus:bg-opacity-10 dark:hover:bg-black dark:hover:bg-opacity-10"
|
||||
}
|
||||
type="text"
|
||||
name="title"
|
||||
spellCheck="false"
|
||||
placeholder="Name your JSON file"
|
||||
value={editedTitle}
|
||||
onChange={(e) => setEditedTitle(e.target.value)}
|
||||
/>
|
||||
|
||||
{match(editedTitle)
|
||||
.with(doc.title, () => (
|
||||
<PencilAltIcon
|
||||
className="h-5 w-5 text-black text-opacity-50"
|
||||
onClick={startEditing}
|
||||
/>
|
||||
))
|
||||
.with("", () => (
|
||||
<button
|
||||
className="ml-2 text-lime-500 hover:text-lime-600 transition"
|
||||
onClick={() => setEditedTitle(doc.title)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-2 text-lime-500 hover:text-lime-600 transition"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</updateDoc.Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { ArrowCircleDownIcon } from "@heroicons/react/outline";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Form, useSubmit } from "remix";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function DragAndDropForm() {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const filenameInputRef = useRef<HTMLInputElement>(null);
|
||||
const rawJsonInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const submit = useSubmit();
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>) => {
|
||||
if (!formRef.current || !filenameInputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (acceptedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstFile = acceptedFiles[0];
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onabort = () => console.log("file reading was aborted");
|
||||
reader.onerror = () => console.log("file reading has failed");
|
||||
reader.onload = () => {
|
||||
if (reader.result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let jsonValue: string | undefined = undefined;
|
||||
|
||||
if (typeof reader.result === "string") {
|
||||
jsonValue = reader.result;
|
||||
} else {
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
jsonValue = decoder.decode(reader.result);
|
||||
}
|
||||
|
||||
invariant(rawJsonInputRef.current, "rawJsonInputRef is null");
|
||||
invariant(jsonValue, "jsonValue is undefined");
|
||||
|
||||
rawJsonInputRef.current.value = jsonValue;
|
||||
|
||||
submit(formRef.current);
|
||||
};
|
||||
reader.readAsArrayBuffer(firstFile);
|
||||
filenameInputRef.current.value = firstFile.name;
|
||||
},
|
||||
[formRef.current, filenameInputRef.current, rawJsonInputRef.current]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDropAccepted: onDrop,
|
||||
maxFiles: 1,
|
||||
maxSize: 1024 * 1024 * 1,
|
||||
multiple: false,
|
||||
accept: "application/json",
|
||||
});
|
||||
|
||||
return (
|
||||
<Form method="post" action="/actions/createFromFile" ref={formRef}>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="block min-w-[300px] p-4 text-base text-slate-300 bg-slate-800 border-2 border-slate-600 border-dashed rounded-md focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="flex items-center">
|
||||
<ArrowCircleDownIcon
|
||||
className={`w-6 h-6 inline mr-3 ${
|
||||
isDragActive ? "text-lime-500" : ""
|
||||
}`}
|
||||
/>
|
||||
<p className={`${isDragActive ? "text-lime-500" : ""}`}>
|
||||
{isDragActive
|
||||
? "Now drop to open it…"
|
||||
: "Drop a JSON file here, or click to select"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="filename" ref={filenameInputRef} />
|
||||
<input type="hidden" name="rawJson" ref={rawJsonInputRef} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Form } from "remix";
|
||||
|
||||
export function ExampleUrl({
|
||||
url,
|
||||
title,
|
||||
displayTitle,
|
||||
}: {
|
||||
url: string;
|
||||
title: string;
|
||||
displayTitle?: string;
|
||||
}) {
|
||||
return (
|
||||
<Form method="post" action="/actions/createFromUrl" reloadDocument>
|
||||
<input type="hidden" name="jsonUrl" value={url} />
|
||||
<input type="hidden" name="title" value={title} />
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-slate-900 px-4 py-2 rounded-sm whitespace-nowrap mr-2 mt-2 md:mr-0 md:p-0 md:bg-transparent text-lime-300 transition hover:text-lime-500"
|
||||
>
|
||||
{displayTitle ?? title}
|
||||
</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { FunctionComponent, useCallback } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { DocumentDownloadIcon } from "@heroicons/react/outline";
|
||||
|
||||
export const FileDropzone: FunctionComponent = ({ children }) => {
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
acceptedFiles.forEach((file: Blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onabort = () => console.log("file reading was aborted");
|
||||
reader.onerror = () => console.log("file reading has failed");
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
let json = JSON.parse(reader.result);
|
||||
// dataSourceDispatch(setJSONAction("Needs title", json));
|
||||
} else {
|
||||
// dataSourceDispatch(setErrorAction("Can't read file"));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { getRootProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
multiple: false,
|
||||
maxFiles: 1,
|
||||
accept: "application/json, text/*",
|
||||
noDragEventsBubbling: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={"absolute w-screen h-screen m-0 p-0 left-0 top-0"}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
isDragActive ? "" : "hidden"
|
||||
} absolute w-screen h-screen bg-black bg-opacity-50 flex justify-center items-center`}
|
||||
>
|
||||
<div className={"text-center"}>
|
||||
{/*<input {...getInputProps()} />*/}
|
||||
<DocumentDownloadIcon className={"w-72 h-72 text-white"} />
|
||||
<p className={"text-white text-2xl"}>
|
||||
Drag 'n' drop some files here, or click to select files
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ArrowKeysIcon } from "./Icons/ArrowKeysIcon";
|
||||
import { EscapeKeyIcon } from "./Icons/EscapeKeyIcon";
|
||||
import { SquareBracketsIcon } from "./Icons/SquareBracketsIcon";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { ThemeModeToggler } from "./ThemeModeToggle";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="flex items-center justify-between w-screen h-[30px] bg-slate-200 dark:bg-slate-800 border-t-[1px] border-slate-400 transition dark:border-slate-600">
|
||||
<ol className="flex pl-3">
|
||||
<li className="flex items-center">
|
||||
<ArrowKeysIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="pl-2 pr-4 text-slate-800 transition dark:text-white">
|
||||
Navigate
|
||||
</Body>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<SquareBracketsIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="pl-2 pr-4 text-slate-800 transition dark:text-white">
|
||||
History
|
||||
</Body>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<EscapeKeyIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="pl-2 pr-4 text-slate-800 transition dark:text-white">
|
||||
Reset path
|
||||
</Body>
|
||||
</li>
|
||||
</ol>
|
||||
<ThemeModeToggler />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ShareIcon, PlusIcon } from "@heroicons/react/outline";
|
||||
import { DocumentTitle } from "./DocumentTitle";
|
||||
import { DiscordIcon } from "./Icons/DiscordIcon";
|
||||
import { GithubIcon } from "./Icons/GithubIcon";
|
||||
import { Logo } from "./Icons/Logo";
|
||||
import { Share } from "./Share";
|
||||
import { NewDocument } from "./NewDocument";
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "./UI/Popover";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="flex items-center justify-between w-screen h-[40px] bg-indigo-700">
|
||||
<Logo className="pl-1 pr-2" width={"130"} />
|
||||
<DocumentTitle />
|
||||
<div className="flex px-4">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button className="flex items-center justify-center mr-2 bg-slate-200 text-indigo-700 bg-opacity-80 text-base font-bold pl-1 pr-2 rounded-sm uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
|
||||
<ShareIcon className="w-4 h-4 mr-1"></ShareIcon>
|
||||
Share
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" sideOffset={8}>
|
||||
<Share />
|
||||
<PopoverArrow
|
||||
className="fill-current text-indigo-700"
|
||||
offset={20}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button className="flex items-center justify-center bg-lime-500 text-slate-700 bg-opacity-80 text-base font-bold pl-1 pr-2 rounded-sm uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
|
||||
<PlusIcon className="w-4 h-4 mr-1"></PlusIcon>
|
||||
New
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" sideOffset={8}>
|
||||
<NewDocument />
|
||||
<PopoverArrow
|
||||
className="fill-current text-indigo-700"
|
||||
offset={20}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<ol className="flex ml-4">
|
||||
<li className="ml-0 opacity-80 transition hover:cursor-pointer hover:opacity-100">
|
||||
<a href="https://discord.gg/ZQq6Had5nP" target="_blank">
|
||||
<DiscordIcon />
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-2 opacity-80 transition hover:cursor-pointer hover:opacity-100">
|
||||
<GithubIcon />
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { AutoplayVideo } from "../AutoplayVideo";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
export function HomeCollaborateSection() {
|
||||
return (
|
||||
<HomeSection containerClassName="py-10 px-6 bg-black md:py-36 lg:py-20">
|
||||
<div className="w-full md:pr-10 md:w-1/2">
|
||||
<ExtraLargeTitle className="text-white mb-4">
|
||||
Collaborate with the whole world (and yourself)
|
||||
</ExtraLargeTitle>
|
||||
<SmallSubtitle className="mb:6 md:mb-10">
|
||||
Easily share your JSON documents with any distant relative with a CS
|
||||
degree. Link right to the part of the document you're on. Or save the
|
||||
link for some casual browsing later in the evening while enjoying a
|
||||
glass of red.
|
||||
</SmallSubtitle>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2">
|
||||
<AutoplayVideo src="/home/JsonHeroShare.mp4" />
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AutoplayVideo } from "../AutoplayVideo";
|
||||
import { Body } from "../Primitives/Body";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
export function HomeEdgeCasesSection() {
|
||||
return (
|
||||
<HomeSection
|
||||
containerClassName="py-10 px-6 bg-black md:py-36 lg:py-20"
|
||||
reversed
|
||||
>
|
||||
<div className="w-full md:pl-10 md:w-1/2">
|
||||
<ExtraLargeTitle className="text-white mb-4">
|
||||
Uncover edge cases
|
||||
</ExtraLargeTitle>
|
||||
<SmallSubtitle className="mb:6 md:mb-10">
|
||||
Sometimes a field can be null, have an unexpected value or be missing
|
||||
entirely. View any field's related values and see what to expect when
|
||||
you least expect it. Or check out the inferred JSON schema to see what
|
||||
your JSON is really made of.
|
||||
</SmallSubtitle>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2">
|
||||
<AutoplayVideo src="/home/UncoverEdgeCases.mp4" />
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
FastForwardIcon,
|
||||
MoonIcon,
|
||||
ClockIcon,
|
||||
CodeIcon,
|
||||
LockOpenIcon,
|
||||
CubeTransparentIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { Body } from "../Primitives/Body";
|
||||
import { LargeTitle } from "../Primitives/LargeTitle";
|
||||
import { HomeGridFeatureItem } from "./HomeGridFeatureItem";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
export function HomeFeatureGridSection() {
|
||||
return (
|
||||
<HomeSection containerClassName="bg-black">
|
||||
<div className="flex flex-col px-4 pb-2 pt-6 md:py-12">
|
||||
<LargeTitle className="mb-4 text-slate-500">
|
||||
And lots more features…
|
||||
</LargeTitle>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:flex-wrap">
|
||||
<HomeGridFeatureItem
|
||||
icon={FastForwardIcon}
|
||||
title="Keyboard shortcuts"
|
||||
titleClassName="text-white"
|
||||
>
|
||||
<Body className="text-slate-400">
|
||||
Move as fast as you can think… after 3 coffees
|
||||
</Body>
|
||||
</HomeGridFeatureItem>
|
||||
|
||||
<HomeGridFeatureItem
|
||||
icon={MoonIcon}
|
||||
title="Dark mode"
|
||||
titleClassName="text-white"
|
||||
>
|
||||
<Body className="text-slate-400">
|
||||
Of course, we’re not animals.
|
||||
</Body>
|
||||
</HomeGridFeatureItem>
|
||||
|
||||
<HomeGridFeatureItem
|
||||
icon={ClockIcon}
|
||||
title="Code view"
|
||||
titleClassName="text-white"
|
||||
>
|
||||
<Body className="text-slate-400">
|
||||
Easily switch to the code view, so you can appear hardcore.
|
||||
</Body>
|
||||
</HomeGridFeatureItem>
|
||||
<HomeGridFeatureItem
|
||||
icon={CubeTransparentIcon}
|
||||
title="Auto JSON Schema"
|
||||
titleClassName="text-white"
|
||||
>
|
||||
<Body className="text-slate-400">
|
||||
Automatically generates JSON Schema (draft 2020-12) from your
|
||||
JSON.
|
||||
</Body>
|
||||
</HomeGridFeatureItem>
|
||||
<HomeGridFeatureItem
|
||||
icon={CodeIcon}
|
||||
title="VS Code plugin"
|
||||
titleClassName="text-white"
|
||||
>
|
||||
<Body className="text-slate-400">
|
||||
Quickly view JSON files or selections in JSON Hero, right from the
|
||||
VS Code.{" "}
|
||||
<a
|
||||
className="whitespace-nowrap text-lime-300 hover:text-lime-500"
|
||||
href="https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Get it here
|
||||
</a>
|
||||
.
|
||||
</Body>
|
||||
</HomeGridFeatureItem>
|
||||
<HomeGridFeatureItem
|
||||
icon={LockOpenIcon}
|
||||
title="100% open source"
|
||||
titleClassName="text-white"
|
||||
>
|
||||
<Body className="text-slate-400">
|
||||
Use jsonhero.io or fork it on GitHub and run it yourself.
|
||||
</Body>
|
||||
</HomeGridFeatureItem>
|
||||
</div>
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { DiscordIcon } from "../Icons/DiscordIcon";
|
||||
import { GithubIcon } from "../Icons/GithubIcon";
|
||||
import { Logo } from "../Icons/Logo";
|
||||
|
||||
export type HomeFooterProps = {
|
||||
maxWidth?: string;
|
||||
};
|
||||
|
||||
export function HomeFooter({ maxWidth = "1150px" }: HomeFooterProps) {
|
||||
return (
|
||||
<footer className="flex flex-col items-center w-full px-4 py-6 bg-black md:py-10">
|
||||
<div
|
||||
className="flex items-center justify-between w-full pt-9 border-t-[1px] border-slate-800"
|
||||
style={{ maxWidth: maxWidth }}
|
||||
>
|
||||
<div className="flex flex-grow items-start ">
|
||||
<Logo />
|
||||
</div>
|
||||
<ol className="flex ml-2">
|
||||
<li className="ml-0 hover:cursor-pointer">
|
||||
<DiscordIcon />
|
||||
</li>
|
||||
<li className="ml-2 hover:cursor-pointer">
|
||||
<GithubIcon />
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IconComponent } from "~/useColumnView";
|
||||
import { Body } from "../Primitives/Body";
|
||||
import { Title } from "../Primitives/Title";
|
||||
|
||||
export type HomeGridFeatureItemProps = {
|
||||
icon: IconComponent;
|
||||
title: string;
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function HomeGridFeatureItem(props: HomeGridFeatureItemProps) {
|
||||
return (
|
||||
<div className="flex lg:basis-1/4 basis-1 md:basis-1/4 flex-grow flex-col p-6 rounded-sm bg-white bg-opacity-[7%]">
|
||||
<props.icon className="w-10 h-10 text-indigo-700 mb-3" />
|
||||
<Title className={props.titleClassName}>{props.title}</Title>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { DiscordIcon } from "../Icons/DiscordIcon";
|
||||
import { GithubIcon } from "../Icons/GithubIcon";
|
||||
import { Logo } from "../Icons/Logo";
|
||||
import { NewDocument } from "../NewDocument";
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "../UI/Popover";
|
||||
|
||||
export function HomeHeader() {
|
||||
return (
|
||||
<header className="fixed z-20 flex items-center justify-between w-screen h-[82px] px-4 bg-indigo-700">
|
||||
<div className="flex flex-grow items-start ">
|
||||
<Logo />
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button className="mr-2 bg-lime-500 text-white text-lg font-bold px-2 rounded-sm uppercase cursor-pointer">
|
||||
Try now
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" sideOffset={30}>
|
||||
<NewDocument />
|
||||
<PopoverArrow className="fill-current text-indigo-700" offset={20} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<ol className="flex ml-2">
|
||||
<li className="ml-0 hover:cursor-pointer">
|
||||
<a href="https://discord.gg/ZQq6Had5nP" target="_blank">
|
||||
<DiscordIcon />
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-2 hover:cursor-pointer">
|
||||
<a href="https://github.com/jsonhero-io" target="_blank">
|
||||
<GithubIcon />
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AutoplayVideo } from "../AutoplayVideo";
|
||||
import { NewFile } from "../NewFile";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
const jsonHeroTitle = "JSON used to suck.";
|
||||
const jsonHeroSlogan = "It still does, but we're trying to make it better.";
|
||||
|
||||
export function HomeHeroSection() {
|
||||
return (
|
||||
<HomeSection
|
||||
containerClassName="md:h-[80vh] bg-black p-6 pb-16 pt-32 md:pt-48"
|
||||
flipped
|
||||
>
|
||||
<div className="mt-6 lg:w-1/2 md:pr-10">
|
||||
<AutoplayVideo src="/home/JsonHero2.mp4" />
|
||||
</div>
|
||||
<div className="lg:w-1/2">
|
||||
<ExtraLargeTitle className="text-lime-300">
|
||||
{jsonHeroTitle}
|
||||
</ExtraLargeTitle>
|
||||
<ExtraLargeTitle className="text-white mb-4">
|
||||
{jsonHeroSlogan}
|
||||
</ExtraLargeTitle>
|
||||
<SmallSubtitle className="text-slate-400 mb-8">
|
||||
Stop staring at thousand line JSON files in your editor and start
|
||||
staring at thousand line JSON files in your browser. With a few nice
|
||||
features to help make it not <em>the worst</em>.
|
||||
</SmallSubtitle>
|
||||
<NewFile />
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { JsonProvider } from "~/hooks/useJson";
|
||||
import {
|
||||
JsonColumnViewProvider,
|
||||
useJsonColumnViewAPI,
|
||||
} from "~/hooks/useJsonColumnView";
|
||||
import { JsonDocProvider } from "~/hooks/useJsonDoc";
|
||||
import { JsonPreview } from "../JsonPreview";
|
||||
import { PreviewValue } from "../Preview/PreviewValue";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
import { PropertiesValue } from "../Properties/PropertiesValue";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
const json = {
|
||||
id: "a1c33bd1-0528-4de3-a745-44d95e7ac3d8",
|
||||
title: "JSON Hero is a tool for JSON",
|
||||
thumbnail: "https://media.giphy.com/media/13CoXDiaCcCoyk/giphy-downsized.gif",
|
||||
createdAt: "2022-02-01T02:25:41-05:00",
|
||||
tint: "#EAB308",
|
||||
webpages: "https://www.theonion.com/",
|
||||
youtube: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
json: "bourne",
|
||||
};
|
||||
|
||||
const infoBoxData = [
|
||||
{
|
||||
title: "Images",
|
||||
highlight: "$.thumbnail",
|
||||
},
|
||||
{
|
||||
title: "Dates",
|
||||
highlight: "$.createdAt",
|
||||
},
|
||||
{
|
||||
title: "Colors",
|
||||
highlight: "$.tint",
|
||||
},
|
||||
{
|
||||
title: "URLs",
|
||||
highlight: "$.webpages",
|
||||
},
|
||||
{
|
||||
title: "Videos",
|
||||
highlight: "$.youtube",
|
||||
},
|
||||
];
|
||||
|
||||
const autoplayDuration = 3000;
|
||||
|
||||
export function HomeInfoBoxSection() {
|
||||
return (
|
||||
<SampleJSONPreview initialSelection={infoBoxData[0].highlight}>
|
||||
<HomeInfoBoxSectionContent />
|
||||
</SampleJSONPreview>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeInfoBoxSectionContent() {
|
||||
const [index, setIndex] = useState(0);
|
||||
const api = useJsonColumnViewAPI();
|
||||
const interval = useRef<NodeJS.Timer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedPath = infoBoxData[index].highlight;
|
||||
api.goToNodeId(selectedPath);
|
||||
}, [index]);
|
||||
|
||||
const resetInterval = () => {
|
||||
if (interval.current != null) {
|
||||
clearInterval(interval.current);
|
||||
}
|
||||
interval.current = setInterval(() => {
|
||||
setIndex((i) => (i = (i + 1) % infoBoxData.length));
|
||||
}, autoplayDuration);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
resetInterval();
|
||||
return () => {
|
||||
if (interval.current == null) return;
|
||||
clearInterval(interval.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HomeSection containerClassName="bg-black p-6">
|
||||
<div className="md:pr-4 lg:pr-10 flex flex-col w-full md:w-1/2">
|
||||
<ExtraLargeTitle className="text-white mb-4">
|
||||
<span className=" text-lime-300">{infoBoxData[index].title}</span> are
|
||||
more than just strings
|
||||
</ExtraLargeTitle>
|
||||
<SmallSubtitle className="text-slate-400 mb-10">
|
||||
We figure out what your strings are made of, so you don't have to.
|
||||
</SmallSubtitle>
|
||||
<ul className="flex w-full text-slate-300 mb-3">
|
||||
{infoBoxData.map((value, i) => {
|
||||
return (
|
||||
<li
|
||||
key={value.highlight}
|
||||
onClick={() => {
|
||||
resetInterval();
|
||||
setIndex(i);
|
||||
}}
|
||||
className={`flex flex-grow justify-center px-4 py-2 cursor-pointer border-b-2 ${
|
||||
index === i
|
||||
? "text-white border-lime-500"
|
||||
: "border-slate-600"
|
||||
}`}
|
||||
>
|
||||
{value.title}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="w-full">
|
||||
<JsonPreview
|
||||
json={json}
|
||||
highlightPath={infoBoxData[index].highlight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative pointer-events-none w-full md:w-1/2 flex flex-col justify-center items-center py-5">
|
||||
<div className="absolute z-10 bottom-0 w-full h-[200px] bg-gradient-to-t from-slate-900 to-transparent"></div>
|
||||
<div className="min-w-full max-w-full p-4 rounded-sm bg-slate-900 h-[65vh] overflow-y-auto">
|
||||
<div className="mb-4">
|
||||
<PreviewValue />
|
||||
</div>
|
||||
<PropertiesValue />
|
||||
</div>
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
|
||||
function SampleJSONPreview({
|
||||
children,
|
||||
initialSelection,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
initialSelection: string;
|
||||
}) {
|
||||
return (
|
||||
<JsonDocProvider
|
||||
doc={{ id: "sample", title: "Sample", type: "raw", contents: "" }}
|
||||
path={initialSelection}
|
||||
>
|
||||
<JsonProvider initialJson={json}>
|
||||
<JsonColumnViewProvider>{children}</JsonColumnViewProvider>
|
||||
</JsonProvider>
|
||||
</JsonDocProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export type HomeSectionProps = {
|
||||
containerClassName?: string;
|
||||
maxWidth?: string;
|
||||
reversed?: boolean;
|
||||
flipped?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function HomeSection({
|
||||
containerClassName,
|
||||
maxWidth = "1150px",
|
||||
reversed = false,
|
||||
flipped = false,
|
||||
children,
|
||||
}: HomeSectionProps) {
|
||||
return (
|
||||
<div className={`flex justify-center items-center ${containerClassName}`}>
|
||||
<div
|
||||
className={`flex flex-col md:flex-row w-full ${
|
||||
reversed ? "md:flex-row-reverse" : ""
|
||||
}${flipped ? "flex-col-reverse" : ""}`}
|
||||
style={{ maxWidth: maxWidth }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
|
||||
export type HomeSplitSectionProps = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function HomeSplitSection({
|
||||
className,
|
||||
children,
|
||||
}: HomeSplitSectionProps) {
|
||||
return (
|
||||
<div
|
||||
className={`grid lg:grid-cols-2 items-center justify-items-center py-12 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeSplitTextContent({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="justify-self-center lg:justify-self-end max-w-2xl px-20 flex flex-col justify-center">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeSplitMediaContent({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-center items-center px-10 py-5">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export function ArrowKeysIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="62"
|
||||
height="14"
|
||||
viewBox="0 0 62 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path
|
||||
d="M6.60956 4.48804C6.80972 4.23784 7.19026 4.23784 7.39043 4.48804L10.3501 8.18765C10.612 8.51503 10.3789 9 9.95969 9H4.04031C3.62106 9 3.38797 8.51503 3.64988 8.18765L6.60956 4.48804Z"
|
||||
fill="black"
|
||||
/>
|
||||
<rect x="16" width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path
|
||||
d="M23.3904 9.51196C23.1903 9.76216 22.8097 9.76216 22.6096 9.51196L19.6499 5.81235C19.388 5.48496 19.6211 5 20.0403 5L25.9597 5C26.3789 5 26.612 5.48497 26.3501 5.81235L23.3904 9.51196Z"
|
||||
fill="black"
|
||||
/>
|
||||
<rect x="32" width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path
|
||||
d="M36.488 7.39044C36.2378 7.19028 36.2378 6.80974 36.488 6.60957L40.1877 3.64988C40.515 3.38797 41 3.62106 41 4.04031L41 9.95969C41 10.3789 40.515 10.612 40.1877 10.3501L36.488 7.39044Z"
|
||||
fill="black"
|
||||
/>
|
||||
<rect x="48" width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path
|
||||
d="M57.512 6.60956C57.7622 6.80972 57.7622 7.19026 57.512 7.39043L53.8123 10.3501C53.485 10.612 53 10.3789 53 9.95969L53 4.04031C53 3.62106 53.485 3.38797 53.8123 3.64988L57.512 6.60956Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function DiscordIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="12" cy="12" r="12" fill="#F8FAFC" />
|
||||
<path
|
||||
d="M18.0881 7.3374C18.0116 7.27279 17.9402 7.2032 17.8637 7.14356C17.554 6.88097 17.2269 6.63856 16.8846 6.41792C16.4342 6.13677 15.9516 5.90824 15.4464 5.73702C15.0844 5.61277 14.7172 5.51835 14.35 5.40901C14.2837 5.40901 14.2786 5.38414 14.3092 5.3245C14.3398 5.26485 14.4061 5.14558 14.4469 5.05115C14.4538 5.03366 14.4667 5.0191 14.4835 5.01001C14.5003 5.00092 14.5198 4.99789 14.5387 5.00146C14.809 5.04619 15.0844 5.07601 15.3547 5.13069C15.8281 5.229 16.2896 5.3756 16.7316 5.56805C17.1998 5.76225 17.6502 5.99501 18.0779 6.26385C18.2267 6.353 18.3697 6.45094 18.5063 6.5571C18.5891 6.62989 18.6566 6.71764 18.7051 6.81552C19.1108 7.51363 19.4521 8.24546 19.7251 9.00236C20.1066 10.0234 20.3983 11.0742 20.5971 12.1435C20.7042 12.715 20.7909 13.2866 20.8674 13.8631C20.9184 14.216 20.9388 14.5788 20.9745 14.9366C20.9745 15.0559 20.9745 15.1702 21 15.2895C21 15.3164 20.9911 15.3425 20.9745 15.3641C20.462 15.9257 19.8549 16.398 19.1794 16.7606C18.5379 17.1017 17.8516 17.3558 17.1395 17.5161C16.7511 17.6096 16.3554 17.6711 15.9564 17.7H15.7116C15.701 17.7002 15.6904 17.6981 15.6807 17.6938C15.671 17.6895 15.6624 17.6831 15.6555 17.6752C15.4413 17.4068 15.2323 17.1334 15.0232 16.8551V16.8253C16.3606 16.3823 17.5548 15.6041 18.4859 14.5689C18.3788 14.6434 18.2819 14.718 18.1748 14.7826C17.8739 14.9665 17.5781 15.1504 17.267 15.3193C16.7354 15.61 16.1728 15.8433 15.5892 16.0151C14.6422 16.3069 13.6595 16.474 12.6671 16.5121H12.3713H11.8155C11.4011 16.5146 10.9871 16.4897 10.5762 16.4376C10.1887 16.3879 9.80109 16.3332 9.41351 16.2636C8.86661 16.1567 8.33068 16.002 7.81221 15.8014C7.15233 15.5479 6.523 15.2246 5.93553 14.8372L5.55306 14.5788C6.01711 15.0934 6.54864 15.5462 7.13396 15.9257C7.72153 16.3044 8.35541 16.6099 9.02084 16.8352L8.98514 16.8899L8.39358 17.6553C8.38145 17.6729 8.36453 17.6868 8.34472 17.6956C8.3249 17.7044 8.30298 17.7076 8.28138 17.705C7.93875 17.691 7.59775 17.6511 7.26145 17.5857C6.76756 17.4952 6.28289 17.3621 5.81314 17.1881C5.27458 16.9934 4.76114 16.7382 4.28323 16.4277C3.86783 16.1551 3.48621 15.8365 3.14601 15.4784C3.14601 15.4784 3.12051 15.4386 3.10011 15.4287C3.06012 15.3983 3.03012 15.3571 3.01381 15.3103C2.9975 15.2635 2.99559 15.2131 3.00831 15.1653L3.05421 14.6335C3.0899 14.2856 3.1205 13.9426 3.1664 13.5947C3.2123 13.2468 3.28879 12.7647 3.36529 12.3472C3.51174 11.5311 3.7093 10.7244 3.95685 9.93177C4.16738 9.2543 4.42116 8.59033 4.71671 7.94373C4.91624 7.50667 5.14275 7.08178 5.39497 6.6714C5.46939 6.5728 5.56514 6.49137 5.67544 6.43284C6.1388 6.11857 6.63239 5.84893 7.14925 5.62769C7.71444 5.38251 8.30641 5.20075 8.91375 5.08594L9.47981 5.00643C9.49599 5.00328 9.51279 5.00611 9.52694 5.01438C9.54108 5.02265 9.55155 5.03575 9.55631 5.05115L9.7042 5.33942C9.7297 5.38415 9.7042 5.39907 9.6685 5.40901C9.41351 5.47859 9.15854 5.54319 8.90865 5.61774C8.45618 5.75584 8.01886 5.93729 7.60313 6.15946C7.24627 6.34465 6.9052 6.5574 6.58319 6.79565C6.3588 6.9696 6.14462 7.14853 5.92533 7.32745C5.9235 7.33135 5.92255 7.33557 5.92255 7.33986C5.92255 7.34415 5.9235 7.3484 5.92533 7.35229L5.99163 7.32248C6.471 7.09882 6.95037 6.86522 7.43994 6.65647C8.00719 6.4106 8.59831 6.22081 9.20443 6.08991C9.61682 5.99062 10.0361 5.92083 10.459 5.88114C10.8414 5.84635 11.2239 5.82649 11.6013 5.80661C11.79 5.80661 11.9787 5.80661 12.1673 5.80661C12.5141 5.80661 12.866 5.8414 13.2128 5.86625C13.8437 5.91322 14.4686 6.01806 15.0793 6.17936C15.6332 6.32264 16.1739 6.51049 16.6959 6.74099L17.9606 7.33243L18.0218 7.36224L18.0881 7.3374ZM9.35232 10.5679C9.08643 10.5761 8.82881 10.66 8.6113 10.8093C8.39378 10.9586 8.2259 11.1667 8.12839 11.4079C7.98657 11.7022 7.93351 12.0296 7.97541 12.3522C8.01397 12.7406 8.19505 13.1024 8.48538 13.371C8.61754 13.5006 8.77761 13.6 8.95401 13.6619C9.13041 13.7238 9.31872 13.7467 9.50531 13.7289C9.68475 13.7178 9.85988 13.6705 10.0196 13.5901C10.1794 13.5097 10.3203 13.3979 10.4335 13.2617C10.7252 12.9245 10.8682 12.4886 10.8312 12.049C10.8196 11.7253 10.7096 11.4122 10.515 11.1494C10.3862 10.9659 10.2123 10.8166 10.0093 10.7151C9.80628 10.6135 9.58046 10.563 9.35232 10.5679ZM16.1094 12.1733C16.1148 11.8593 16.0319 11.55 15.8697 11.2787C15.7548 11.0583 15.5775 10.8747 15.3587 10.7496C15.14 10.6245 14.889 10.5632 14.6356 10.5729C14.451 10.578 14.2698 10.6219 14.1043 10.7017C13.9388 10.7815 13.793 10.8953 13.6769 11.0351C13.5285 11.203 13.4159 11.398 13.3459 11.6088C13.2758 11.8196 13.2496 12.0419 13.2689 12.2627C13.2861 12.6947 13.4787 13.1023 13.8043 13.3959C13.9417 13.5243 14.1072 13.6205 14.2883 13.6773C14.4694 13.7342 14.6614 13.7501 14.8498 13.7239C15.1962 13.6764 15.5095 13.4978 15.7218 13.2269C15.9694 12.9284 16.106 12.5571 16.1094 12.1733Z"
|
||||
fill="#4338CA"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export function EscapeKeyIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path
|
||||
d="M3.21695 10C2.79876 10 2.42068 9.88168 2.08269 9.64504C1.75044 9.4084 1.48693 9.0687 1.29216 8.62595C1.09739 8.17557 1 7.63359 1 7C1 6.38168 1.09739 5.85114 1.29216 5.4084C1.49265 4.95802 1.75044 4.61069 2.06551 4.36641C2.38058 4.12214 2.71283 4 3.06228 4C3.48619 4 3.83563 4.12595 4.1106 4.37786C4.3913 4.62214 4.59753 4.9542 4.72928 5.37405C4.86677 5.79389 4.93551 6.25954 4.93551 6.77099C4.93551 6.93893 4.92692 7.10305 4.90973 7.26336C4.89827 7.41603 4.88682 7.52672 4.87536 7.59542H2.42641C2.49515 7.93893 2.61831 8.17939 2.7959 8.31679C2.97348 8.44656 3.18257 8.51145 3.42317 8.51145C3.69814 8.51145 3.9903 8.39695 4.29964 8.16794L4.78084 9.33588C4.5517 9.54962 4.29391 9.71374 4.00749 9.82824C3.72106 9.94275 3.45754 10 3.21695 10ZM2.40922 6.31298H3.68096C3.68096 6.0916 3.638 5.90076 3.55207 5.74046C3.47187 5.57252 3.32006 5.48855 3.09665 5.48855C2.93625 5.48855 2.79303 5.55344 2.66701 5.68321C2.54098 5.81298 2.45505 6.0229 2.40922 6.31298Z"
|
||||
fill="#0F172A"
|
||||
/>
|
||||
<path
|
||||
d="M7.06968 10C6.79471 10 6.50256 9.92748 6.19322 9.78244C5.8896 9.62977 5.62609 9.43511 5.40268 9.19847L6.05573 7.98473C6.44527 8.36641 6.79471 8.55725 7.10405 8.55725C7.25872 8.55725 7.36757 8.53053 7.43058 8.4771C7.49932 8.41603 7.53369 8.32824 7.53369 8.21374C7.53369 8.06107 7.45063 7.94275 7.2845 7.85878C7.11838 7.76718 6.92647 7.66412 6.70879 7.54962C6.54266 7.45801 6.37653 7.34351 6.2104 7.20611C6.05 7.06107 5.91538 6.87786 5.80654 6.65649C5.6977 6.43511 5.64327 6.16794 5.64327 5.85496C5.64327 5.29008 5.80081 4.83969 6.11588 4.50382C6.43095 4.16794 6.84054 4 7.34465 4C7.69982 4 8.00344 4.08015 8.25549 4.24046C8.51328 4.39313 8.73669 4.56489 8.92573 4.75573L8.27268 5.92366C8.11801 5.77099 7.9662 5.65267 7.81726 5.5687C7.66832 5.48473 7.52797 5.44275 7.39621 5.44275C7.14415 5.44275 7.01813 5.54962 7.01813 5.76336C7.01813 5.9084 7.09546 6.0229 7.25013 6.10687C7.41053 6.18321 7.59671 6.27481 7.80866 6.38168C7.98052 6.46565 8.14951 6.57634 8.31564 6.71374C8.4875 6.85115 8.62785 7.03054 8.73669 7.25191C8.85126 7.47328 8.90855 7.75573 8.90855 8.09924C8.90855 8.63359 8.75101 9.08397 8.43594 9.45038C8.1266 9.81679 7.67118 10 7.06968 10Z"
|
||||
fill="#0F172A"
|
||||
/>
|
||||
<path
|
||||
d="M11.5736 10C11.1669 10 10.8002 9.88168 10.4737 9.64504C10.1472 9.4084 9.88654 9.0687 9.69177 8.62595C9.50273 8.17557 9.4082 7.63359 9.4082 7C9.4082 6.36641 9.51418 5.82825 9.72614 5.3855C9.94382 4.93512 10.2274 4.5916 10.5768 4.35496C10.9263 4.11832 11.3044 4 11.7111 4C11.9689 4 12.2009 4.05343 12.4071 4.16031C12.6191 4.26718 12.8052 4.41221 12.9656 4.59542L12.2782 5.85496C12.1865 5.74809 12.1035 5.67557 12.029 5.6374C11.9545 5.59924 11.8772 5.58015 11.797 5.58015C11.522 5.58015 11.3072 5.70992 11.1525 5.96947C10.9979 6.22137 10.9205 6.56489 10.9205 7C10.9205 7.43511 11.0007 7.78244 11.1611 8.04198C11.3215 8.29389 11.5163 8.41985 11.7455 8.41985C11.8657 8.41985 11.9832 8.3855 12.0978 8.31679C12.2181 8.24046 12.3298 8.15267 12.4329 8.05344L13 9.33588C12.788 9.58779 12.5532 9.76336 12.2954 9.8626C12.0376 9.9542 11.797 10 11.5736 10Z"
|
||||
fill="#0F172A"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="12" cy="12" r="12" fill="#F8FAFC" />
|
||||
<path
|
||||
d="M21 12.0523C20.9915 12.1541 20.982 12.2565 20.9741 12.3588C20.8837 13.6147 20.5087 14.8358 19.8759 15.935C19.1892 17.1392 18.2177 18.1681 17.0412 18.9371C16.376 19.3775 15.6509 19.7259 14.8867 19.9725C14.6521 20.0485 14.4508 19.9604 14.2906 19.7886C14.1508 19.642 14.0747 19.4487 14.0779 19.249C14.0779 18.3672 14.0779 17.4853 14.0779 16.6031C14.0863 16.3681 14.0628 16.1329 14.008 15.9038C13.9792 15.7993 13.9324 15.6997 13.8918 15.5924C14.0311 15.5634 14.1766 15.5377 14.3216 15.5032C15.1958 15.3068 16.0136 14.9915 16.7231 14.4432C17.5308 13.8177 18.0294 13.0111 18.176 12.0157C18.3187 11.0531 18.2037 10.1195 17.7728 9.233C17.6063 8.89501 17.3874 8.58384 17.1236 8.31036L17.0903 8.2737C17.3809 7.63017 17.4174 6.90541 17.193 6.23744C17.1338 6.05812 17.0463 5.88882 16.9335 5.73562C16.9232 5.71873 16.9081 5.70507 16.89 5.69624C16.8719 5.68742 16.8516 5.6838 16.8314 5.68583C16.1357 5.70242 15.4598 5.91436 14.8856 6.29599C14.6578 6.44107 14.4484 6.61171 14.2618 6.80437C14.253 6.8169 14.2399 6.82597 14.2248 6.82998C14.2097 6.83399 14.1936 6.83267 14.1795 6.82626C13.6814 6.65717 13.1643 6.58603 12.6409 6.54936C12.1713 6.51543 11.6997 6.51927 11.2308 6.56085C10.7502 6.59484 10.2762 6.68958 9.82082 6.84268C9.80583 6.84985 9.78867 6.85153 9.77251 6.84741C9.75634 6.84328 9.74225 6.83364 9.73283 6.82024C9.20278 6.26705 8.50548 5.89131 7.74132 5.74712C7.55238 5.711 7.35723 5.70771 7.16491 5.68801C7.14583 5.6851 7.1263 5.68795 7.10894 5.69617C7.09159 5.7044 7.07726 5.71759 7.0679 5.73398C6.87448 6.00312 6.75032 6.3133 6.70581 6.63856C6.63122 7.07823 6.66761 7.52889 6.81184 7.95192C6.85301 8.06958 6.91505 8.18012 6.97145 8.3027C6.90772 8.37329 6.83158 8.45374 6.75939 8.53746C6.25302 9.13009 5.92997 9.84975 5.82765 10.6131C5.72834 11.2252 5.75886 11.8505 5.91733 12.4507C6.1621 13.3378 6.6872 14.0377 7.44973 14.5713C8.04589 14.9899 8.71085 15.2624 9.41924 15.4408C9.65331 15.4999 9.88963 15.5503 10.1231 15.6012C10.0859 15.7046 10.0408 15.8086 10.0103 15.9175C9.94909 16.1717 9.92142 16.4324 9.92798 16.6934C9.93155 16.7171 9.92539 16.7411 9.91082 16.7604C9.89625 16.7796 9.87445 16.7925 9.85015 16.7963C9.51067 16.9011 9.15221 16.935 8.79827 16.8959C8.46398 16.8686 8.14004 16.7699 7.84961 16.607C7.58186 16.4549 7.35663 16.2415 7.19367 15.9853C6.92069 15.5618 6.54507 15.269 6.03915 15.1481C5.87828 15.104 5.70835 15.1016 5.54621 15.1409C5.51726 15.1481 5.48911 15.158 5.46217 15.1705C5.38152 15.2104 5.35557 15.2756 5.40577 15.3472C5.45449 15.4143 5.5113 15.4755 5.57497 15.5295C5.70582 15.6389 5.85359 15.7374 5.97429 15.8578C6.24219 16.126 6.4255 16.4505 6.5727 16.793C6.79548 17.3091 7.18352 17.6686 7.68661 17.9209C8.20945 18.1819 8.77007 18.2443 9.34762 18.1945C9.53769 18.1775 9.72663 18.1453 9.91896 18.1201C9.91896 18.1305 9.92234 18.1458 9.92234 18.1606C9.92234 18.5321 9.92572 18.9037 9.92234 19.2753C9.92243 19.3969 9.89197 19.5168 9.8336 19.6244C9.77522 19.7321 9.69069 19.8243 9.58732 19.8931C9.51652 19.946 9.4329 19.9803 9.34451 19.9927C9.25611 20.0052 9.1659 19.9954 9.08253 19.9643C6.95171 19.2321 5.31609 17.9187 4.17567 16.0242C3.63235 15.1126 3.27001 14.1103 3.10744 13.0691C2.81135 11.2274 3.13103 9.3421 4.01965 7.68951C4.90826 6.03693 6.31908 4.70396 8.04532 3.88597C8.87822 3.48473 9.7727 3.21731 10.6939 3.09412C10.9951 3.05418 11.2991 3.0394 11.602 3.00985C11.6251 3.00985 11.6471 3.00328 11.6702 3H12.3346C12.3572 3.00328 12.3792 3.00766 12.4023 3.00985C12.6685 3.03283 12.9353 3.04761 13.2003 3.0799C14.0888 3.18707 14.9544 3.42898 15.7655 3.79677C17.2635 4.46177 18.5425 5.5162 19.4608 6.84323C20.3464 8.10221 20.8692 9.56789 20.9752 11.0887C20.9831 11.1905 20.9914 11.2926 21 11.3951V12.0523Z"
|
||||
fill="#4338CA"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Link } from "remix";
|
||||
|
||||
export function Logo({
|
||||
className,
|
||||
width = "196",
|
||||
}: {
|
||||
className?: string;
|
||||
width?: string;
|
||||
}) {
|
||||
return (
|
||||
<Link to="/" aria-label="JSON Hero homepage">
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height="50"
|
||||
viewBox="0 0 196 50"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M24.6178 47.452C35.7512 47.452 44.7766 38.4266 44.7766 27.2933C44.7766 16.1599 35.7512 7.13446 24.6178 7.13446C13.4844 7.13446 4.45898 16.1599 4.45898 27.2933C4.45898 38.4266 13.4844 47.452 24.6178 47.452Z" fill="#312E81"/>
|
||||
<path d="M39.7024 29.6068C40.025 29.3266 40.2864 28.983 40.4705 28.5974C40.6545 28.2119 40.7573 27.7925 40.7723 27.3655C40.7873 26.9385 40.7143 26.513 40.5578 26.1154C40.4013 25.7179 40.1647 25.3568 39.8626 25.0546C39.5605 24.7524 39.1995 24.5157 38.802 24.359C38.4045 24.2024 37.979 24.1292 37.552 24.1441C37.125 24.159 36.7056 24.2616 36.32 24.4455C35.9343 24.6294 35.5907 24.8907 35.3104 25.2132C36.5232 24.0004 37.4387 21.1185 36.2258 19.9057C35.0114 18.6929 32.1296 19.6083 30.9167 20.8212C32.1296 19.6083 33.045 16.7265 31.8322 15.5121C30.6194 14.2977 27.7375 15.2147 26.5247 16.4276C26.8177 16.1328 27.0488 15.7824 27.2042 15.397C27.3597 15.0115 27.4364 14.5989 27.4299 14.1833C27.4234 13.7677 27.3337 13.3577 27.1662 12.9773C26.9987 12.597 26.7568 12.254 26.4547 11.9686C25.2169 10.8025 23.2412 10.9239 22.0393 12.1243L18.2451 15.9216C18.1716 15.9937 18.1024 16.0701 18.038 16.1504L18.0271 16.1395L11.9349 22.2302C11.3085 22.8565 10.8116 23.6 10.4725 24.4184C10.1335 25.2368 9.95901 26.1139 9.95901 26.9997C9.95901 27.8856 10.1335 28.7627 10.4725 29.5811C10.8116 30.3995 11.3085 31.143 11.9349 31.7693L11.0708 30.9052L5.5 33.7046C6.84673 38.0483 6.06079 37.812 9.5 41.259C12.9392 44.706 13.6802 45.1517 18.0271 46.5L20.8327 40.6608L19.9686 39.7968C20.5947 40.4231 21.3382 40.92 22.1564 41.259C22.9747 41.598 23.8517 41.7725 24.7374 41.7725C25.6231 41.7725 26.5001 41.598 27.3183 41.259C28.1366 40.92 28.88 40.4231 29.5062 39.7968L35.5984 33.7046L35.5875 33.6937C35.6678 33.6293 35.7442 33.5601 35.8164 33.4866L39.7024 29.6068Z" fill="#FBBF24"/>
|
||||
<path opacity="0.6" d="M29.8145 39.4884L32.248 36.879L17.9587 22.2737L17.476 27.1904L23.1899 32.9042L20.3874 32.775L14.876 27.4971L15.3181 30.0146L18.7169 33.4134C19.3974 34.0937 20.3092 34.4929 21.2707 34.5315C22.2322 34.5702 23.1731 34.2455 23.906 33.622L29.8145 39.4884Z" fill="#D0830B"/>
|
||||
<path opacity="0.6" d="M19.7681 39.8154L19.0566 39.339L15.7847 45.9271L17.5004 46.5L20.7489 40.7963L19.7681 39.8154Z" fill="#D0830B"/>
|
||||
<path d="M18.0274 16.1333L11.9353 22.2255C11.3089 22.8517 10.812 23.5951 10.473 24.4133C10.134 25.2316 9.95947 26.1086 9.95947 26.9943C9.95947 27.88 10.134 28.757 10.473 29.5752C10.812 30.3935 11.3089 31.1369 11.9353 31.7631" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M35.5986 33.7046L29.5064 39.7968C28.8802 40.4232 28.1368 40.92 27.3185 41.2591C26.5003 41.5981 25.6233 41.7726 24.7376 41.7726C23.8519 41.7726 22.9749 41.5981 22.1566 41.2591C21.3384 40.92 20.5949 40.4232 19.9688 39.7968" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M20.833 40.6593L18 46.5" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M11.0709 30.899L5.5 34" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M9.68359 34.1577L6.5 36" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M12.0977 22.0806L13.0629 21.1154" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M13.1722 33.6095L11.6309 32.0682L12.2412 31.4579L14.7416 33.9583C14.7602 33.9762 14.7721 34.0001 14.7752 34.0258C14.7782 34.0516 14.7723 34.0776 14.7583 34.0994C14.7444 34.1212 14.7233 34.1376 14.6987 34.1456C14.674 34.1537 14.6474 34.1529 14.6232 34.1436L13.1722 33.6095Z" fill="#0F172A"/>
|
||||
<path d="M19.19 32.0293L14.8789 27.7166Z" fill="#FBBF24"/>
|
||||
<path d="M19.6635 40.1018L18.6312 39.0696C18.6128 39.0522 18.6009 39.029 18.5973 39.004C18.5937 38.9789 18.5988 38.9533 18.6116 38.9315C18.6244 38.9096 18.6443 38.8927 18.6679 38.8836C18.6915 38.8745 18.7176 38.8736 18.7418 38.8812L20.034 39.2518L20.2738 39.4915L19.6635 40.1018Z" fill="#0F172A"/>
|
||||
<path d="M24.6178 47.452C35.7512 47.452 44.7766 38.4266 44.7766 27.2933C44.7766 16.1599 35.7512 7.13446 24.6178 7.13446C13.4844 7.13446 4.45898 16.1599 4.45898 27.2933C4.45898 38.4266 13.4844 47.452 24.6178 47.452Z" stroke="#1B1966" stroke-width="1.51676" stroke-miterlimit="10"/>
|
||||
<path d="M14.9494 12.9072L3 1L10.4104 22.3794L10.7439 16.1015L36.8456 42.2032L37.2121 37.1606L49.2366 49.1429L41.6074 27.2876L41.0537 33.5774L15.2131 7.96608L14.9494 12.9072Z" fill="#FDE047" stroke="#0F172A" stroke-width="0.833187" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M26.5171 16.4258C27.7301 15.2128 27.7301 13.2462 26.5171 12.0332C25.3041 10.8202 23.3375 10.8202 22.1245 12.0332L18.2438 15.9139C17.0309 17.1268 17.0309 19.0935 18.2438 20.3064C19.4568 21.5194 21.4234 21.5194 22.6364 20.3064L26.5171 16.4258Z" fill="#FBBF24" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M31.9639 19.7669C33.1769 18.5539 33.1769 16.5873 31.9639 15.3743C30.7509 14.1613 28.7843 14.1613 27.5713 15.3743L22.6381 20.3074C21.4252 21.5204 21.4252 23.487 22.6381 24.7C23.8511 25.913 25.8178 25.913 27.0307 24.7L31.9639 19.7669Z" fill="#FBBF24" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M36.3579 24.1607C37.5709 22.9477 37.5709 20.9811 36.3579 19.7681C35.1449 18.5552 33.1783 18.5552 31.9653 19.7681L27.0322 24.7013C25.8192 25.9142 25.8192 27.8809 27.0322 29.0939C28.2452 30.3068 30.2118 30.3068 31.4248 29.0939L36.3579 24.1607Z" fill="#FBBF24" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M39.6973 29.6056C40.9103 28.3926 40.9103 26.426 39.6973 25.213C38.4843 24 36.5177 24 35.3047 25.213L31.424 29.0937C30.211 30.3067 30.211 32.2733 31.424 33.4863C32.637 34.6993 34.6036 34.6993 35.8166 33.4863L39.6973 29.6056Z" fill="#FBBF24" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M12.8564 21.3022C13.44 20.7213 14.2299 20.3951 15.0532 20.3951C15.8766 20.3951 16.6665 20.7213 17.25 21.3022L23.5835 27.6357C24.1644 28.2192 24.4906 29.0091 24.4906 29.8325C24.4906 30.6559 24.1644 31.4457 23.5835 32.0293C23 32.6102 22.2101 32.9363 21.3867 32.9363C20.5634 32.9363 19.7735 32.6102 19.1899 32.0293" fill="#FBBF24"/>
|
||||
<path d="M12.8564 21.3022C13.44 20.7213 14.2299 20.3951 15.0532 20.3951C15.8766 20.3951 16.6665 20.7213 17.25 21.3022L23.5835 27.6357C24.1644 28.2192 24.4906 29.0091 24.4906 29.8325C24.4906 30.6559 24.1644 31.4457 23.5835 32.0293C23 32.6102 22.2101 32.9363 21.3867 32.9363C20.5634 32.9363 19.7735 32.6102 19.1899 32.0293" stroke="#0F172A" stroke-width="0.862527" stroke-miterlimit="10"/>
|
||||
<path d="M22.313 16.5691L21.9816 16.2377C21.4867 15.7428 20.6842 15.7428 20.1893 16.2377L19.1314 17.2957C18.6364 17.7906 18.6364 18.593 19.1313 19.0879L19.4627 19.4193C19.9576 19.9142 20.7601 19.9142 21.255 19.4193L22.313 18.3614C22.8079 17.8664 22.8079 17.064 22.313 16.5691Z" fill="#F59E0B"/>
|
||||
<path d="M21.5713 27.892C20.9845 27.3053 20.0335 27.305 19.4471 27.8915C18.8606 28.4779 18.8609 29.4289 19.4476 30.0156L20.1731 30.7411C20.7598 31.3279 21.7109 31.3281 22.2973 30.7417C22.8837 30.1553 22.8835 29.2042 22.2968 28.6175L21.5713 27.892Z" fill="#F59E0B"/>
|
||||
<path d="M26.7094 20.9614L26.3781 20.63C25.8831 20.1351 25.0807 20.1351 24.5858 20.6301L23.5278 21.688C23.0329 22.1829 23.0329 22.9854 23.5278 23.4803L23.8592 23.8117C24.3541 24.3066 25.1565 24.3066 25.6515 23.8116L26.7094 22.7537C27.2044 22.2588 27.2044 21.4563 26.7094 20.9614Z" fill="#F59E0B"/>
|
||||
<path d="M31.1015 25.3549L30.7702 25.0235C30.2752 24.5286 29.4728 24.5286 28.9779 25.0235L27.9199 26.0815C27.425 26.5764 27.425 27.3789 27.9199 27.8738L28.2513 28.2051C28.7462 28.7001 29.5486 28.7001 30.0436 28.2051L31.1015 27.1472C31.5964 26.6523 31.5964 25.8498 31.1015 25.3549Z" fill="#F59E0B"/>
|
||||
<path d="M35.4946 29.7462L35.1632 29.4149C34.6683 28.9199 33.8659 28.9199 33.371 29.4149L32.313 30.4728C31.8181 30.9678 31.8181 31.7702 32.313 32.2651L32.6444 32.5965C33.1393 33.0914 33.9417 33.0914 34.4366 32.5965L35.4946 31.5385C35.9895 31.0436 35.9895 30.2412 35.4946 29.7462Z" fill="#F59E0B"/>
|
||||
<path d="M18.8863 32.3346L14.5752 28.0219L14.3682 26.8854C14.3643 26.8618 14.368 26.8376 14.3788 26.8162C14.3895 26.7948 14.4067 26.7774 14.4279 26.7663C14.4491 26.7553 14.4733 26.7512 14.4969 26.7547C14.5206 26.7582 14.5425 26.769 14.5597 26.7857L19.4966 31.7242L18.8863 32.3346Z" fill="#0F172A"/>
|
||||
<g clip-path="url(#clip0_444_4251)">
|
||||
<path d="M51.942 32.4324C52.0745 33.1461 52.4232 33.8034 52.9422 34.3176C53.5012 34.7979 54.2301 35.0408 54.9699 34.9932C55.5684 35.0187 56.162 34.877 56.6825 34.5845C57.1117 34.3223 57.4655 33.9559 57.71 33.5203C57.9639 33.0576 58.1332 32.5544 58.2101 32.0338C58.301 31.4672 58.3468 30.8946 58.3471 30.321V17.4831H60.2857V31.1352C60.2829 31.793 60.1966 32.4478 60.0288 33.0845C59.8613 33.7598 59.5469 34.3913 59.1075 34.9349C58.6681 35.4786 58.1141 35.9216 57.4839 36.2331C56.6874 36.6124 55.8092 36.7944 54.9254 36.7635C53.7486 36.8048 52.5918 36.4551 51.6407 35.7703C50.7242 35.0416 50.134 33.9876 49.9966 32.8345L51.942 32.4324Z" fill="white"/>
|
||||
<path d="M74.2261 20.5845C73.3835 19.3637 72.1539 18.7534 70.5372 18.7534C70.0371 18.7535 69.5394 18.8206 69.0575 18.9527C68.588 19.078 68.1448 19.2851 67.7491 19.5642C67.3609 19.8424 67.0432 20.2055 66.8209 20.625C66.5814 21.0986 66.4637 21.623 66.4784 22.152C66.4562 22.5134 66.5092 22.8755 66.6341 23.2159C66.7589 23.5564 66.9531 23.868 67.2045 24.1317C67.7272 24.6255 68.3396 25.0175 69.0096 25.2872C69.7775 25.6048 70.5645 25.8756 71.3661 26.098C72.1869 26.3245 72.9778 26.6454 73.7226 27.0541C74.4325 27.447 75.0482 27.9863 75.5276 28.6352C76.052 29.4307 76.3063 30.3701 76.2538 31.3176C76.2653 32.1165 76.0758 32.9058 75.7023 33.6149C75.3467 34.2819 74.8504 34.8659 74.2466 35.3277C73.6389 35.7918 72.9555 36.1505 72.2258 36.3885C71.5028 36.6339 70.7435 36.7595 69.9789 36.7601C68.8375 36.7647 67.7068 36.5442 66.6531 36.1115C65.5775 35.6558 64.6505 34.9157 63.9746 33.973L65.7523 32.723C66.164 33.4167 66.755 33.9903 67.4648 34.3852C68.2616 34.8157 69.1598 35.0302 70.0679 35.0068C70.5607 35.0054 71.0504 34.9302 71.5202 34.7838C71.9904 34.6344 72.4308 34.4058 72.8218 34.1081C73.2071 33.8146 73.5306 33.4497 73.774 33.0338C74.03 32.5882 74.1601 32.083 74.1507 31.5709C74.1721 30.9988 74.0272 30.4327 73.7329 29.9392C73.449 29.5004 73.0714 29.128 72.6265 28.848C72.1384 28.5422 71.6138 28.297 71.0647 28.1183C70.4824 27.9246 69.8819 27.7297 69.263 27.5338C68.6502 27.3386 68.0478 27.1131 67.458 26.8581C66.8943 26.6193 66.3683 26.3019 65.8961 25.9155C65.4281 25.5207 65.0519 25.031 64.7932 24.4797C64.4918 23.7977 64.349 23.0578 64.3754 22.3142C64.3556 21.4959 64.5353 20.6849 64.8994 19.9494C65.2324 19.29 65.7111 18.7127 66.3003 18.2602C66.892 17.8132 67.5636 17.4797 68.28 17.277C69.0146 17.0667 69.7757 16.9598 70.5406 16.9595C71.5426 16.9461 72.5382 17.118 73.476 17.4662C74.427 17.8528 75.2677 18.4629 75.9249 19.2432L74.2261 20.5845Z" fill="white"/>
|
||||
<path d="M97.7398 26.8817C97.7556 28.2365 97.4995 29.5811 96.9863 30.8378C96.5127 32.0049 95.8046 33.0654 94.9046 33.9555C94.0046 34.8455 92.9312 35.5467 91.7493 36.0169C89.2189 37.005 86.4018 37.005 83.8714 36.0169C82.6894 35.5461 81.616 34.8447 80.7155 33.9547C79.815 33.0648 79.106 32.0046 78.631 30.8378C77.6263 28.2924 77.6263 25.4677 78.631 22.9223C79.106 21.7555 79.815 20.6953 80.7155 19.8054C81.616 18.9154 82.6894 18.214 83.8714 17.7433C86.4018 16.7551 89.2189 16.7551 91.7493 17.7433C92.9312 18.2134 94.0046 18.9146 94.9046 19.8047C95.8046 20.6947 96.5127 21.7552 96.9863 22.9223C97.4995 24.1803 97.7556 25.5258 97.7398 26.8817V26.8817ZM95.6402 26.8817C95.6453 25.8272 95.4634 24.7799 95.1025 23.7872C94.7601 22.8333 94.2344 21.9535 93.5543 21.1959C92.8656 20.4419 92.0259 19.8368 91.0882 19.4189C90.0504 18.9799 88.9328 18.7535 87.8035 18.7535C86.6742 18.7535 85.5567 18.9799 84.5188 19.4189C83.5821 19.8369 82.7436 20.442 82.0561 21.1959C81.3748 21.9526 80.849 22.8327 80.508 23.7872C79.7911 25.7896 79.7911 27.9739 80.508 29.9763C80.8498 30.9294 81.3756 31.8082 82.0561 32.5642C82.7435 33.3192 83.582 33.9254 84.5188 34.3446C85.5572 34.7816 86.6746 35.0068 87.8035 35.0068C88.9325 35.0068 90.0498 34.7816 91.0882 34.3446C92.0261 33.9254 92.8657 33.3192 93.5543 32.5642C94.2337 31.8074 94.7593 30.9288 95.1025 29.9763C95.4631 28.9836 95.645 27.9363 95.6402 26.8817V26.8817Z" fill="white"/>
|
||||
<path d="M114.407 33.4156H114.458V17.4797H116.411V36.2838H113.986L102.611 20.1824H102.559V36.277H100.617V17.4797H103.052L114.407 33.4156Z" fill="white"/>
|
||||
<path d="M120.603 17.7162H124.802V24.7297H132.96V17.7162H137.163V36.5236H132.96V28.3953H124.802V36.5236H120.603V17.7162Z" fill="#FDE047"/>
|
||||
<path d="M141.201 17.7162H154.155V21.5338H145.4V25.0405H153.669V28.8648H145.4V32.6824H154.648V36.5101H141.211L141.201 17.7162Z" fill="#FDE047"/>
|
||||
<path d="M158.005 17.7162H165.383C166.31 17.7123 167.236 17.8064 168.143 17.9966C168.959 18.1605 169.735 18.4758 170.431 18.9257C171.094 19.3706 171.631 19.9738 171.993 20.679C172.407 21.5366 172.605 22.4798 172.572 23.429C172.608 24.6386 172.238 25.8259 171.521 26.8074C170.766 27.7701 169.66 28.4041 168.438 28.5743L173.284 36.5169H168.249L164.266 29H162.19V36.5169H157.991L158.005 17.7162ZM162.204 25.4864H164.677C165.054 25.4864 165.451 25.4864 165.872 25.4459C166.264 25.4265 166.651 25.3456 167.016 25.2061C167.353 25.0788 167.646 24.8625 167.866 24.5811C168.114 24.2333 168.235 23.8128 168.208 23.3885C168.238 23.02 168.154 22.6513 167.967 22.331C167.78 22.0107 167.498 21.754 167.16 21.5946C166.829 21.4369 166.474 21.3341 166.109 21.2905C165.725 21.2372 165.338 21.2101 164.951 21.2094H162.211L162.204 25.4864Z" fill="#FDE047"/>
|
||||
<path d="M174.429 27.1182C174.409 25.7187 174.671 24.3292 175.2 23.0304C175.679 21.8512 176.409 20.7868 177.34 19.9087C178.278 19.037 179.386 18.3633 180.598 17.929C183.265 17.0101 186.17 17.0101 188.838 17.929C190.049 18.3641 191.157 19.0376 192.096 19.9087C193.026 20.7877 193.756 21.8518 194.236 23.0304C195.259 25.6634 195.259 28.5764 194.236 31.2094C193.756 32.388 193.026 33.4521 192.096 34.3311C191.157 35.2021 190.049 35.8757 188.838 36.3108C186.17 37.2297 183.265 37.2297 180.598 36.3108C179.386 35.8765 178.278 35.2028 177.34 34.3311C176.409 33.453 175.679 32.3886 175.2 31.2094C174.67 29.9095 174.408 28.5189 174.429 27.1182ZM178.792 27.1182C178.783 27.9472 178.924 28.7711 179.21 29.5506C179.474 30.2617 179.882 30.9124 180.409 31.4628C180.936 32.0073 181.573 32.4365 182.279 32.7229C183.846 33.3265 185.586 33.3265 187.153 32.7229C187.86 32.4363 188.498 32.0071 189.027 31.4628C189.553 30.9114 189.96 30.2611 190.226 29.5506C190.778 27.9806 190.778 26.2727 190.226 24.7027C189.963 23.987 189.555 23.3317 189.027 22.777C188.498 22.2327 187.86 21.8035 187.153 21.5168C185.586 20.9133 183.846 20.9133 182.279 21.5168C181.573 21.8033 180.936 22.2326 180.409 22.777C179.88 23.3309 179.472 23.9864 179.21 24.7027C178.927 25.4771 178.786 26.295 178.792 27.1182V27.1182Z" fill="#FDE047"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_444_4251">
|
||||
<rect width="146" height="20" fill="white" transform="translate(50 17)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { transition } from "../../utilities/animationConstants";
|
||||
|
||||
export const MoonIcon = () => {
|
||||
const variants = {
|
||||
initial: { scale: 0.6, rotate: 90 },
|
||||
animate: { scale: 1, rotate: 0, transition },
|
||||
whileTap: { scale: 0.95, rotate: 15 },
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 50 50"
|
||||
key="moon"
|
||||
>
|
||||
<motion.path
|
||||
d="M 43.81 29.354 C 43.688 28.958 43.413 28.626 43.046 28.432 C 42.679 28.238 42.251 28.198 41.854 28.321 C 36.161 29.886 30.067 28.272 25.894 24.096 C 21.722 19.92 20.113 13.824 21.683 8.133 C 21.848 7.582 21.697 6.985 21.29 6.578 C 20.884 6.172 20.287 6.022 19.736 6.187 C 10.659 8.728 4.691 17.389 5.55 26.776 C 6.408 36.163 13.847 43.598 23.235 44.451 C 32.622 45.304 41.28 39.332 43.816 30.253 C 43.902 29.96 43.9 29.647 43.81 29.354 Z"
|
||||
fill="currentColor"
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
whileTap="whileTap"
|
||||
variants={variants}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const ShortcutIcon: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
className={`flex items-center justify-center h-[26px] w-[26px] ml-1 text-slate-700 bg-slate-200 dark:text-slate-300 dark:bg-slate-800 rounded ${className}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export function SquareBracketsIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="30"
|
||||
height="14"
|
||||
viewBox="0 0 30 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path d="M6 11V3H9V4.5H7.5V9.5H9V11H6Z" fill="#0F172A" />
|
||||
<rect x="16" width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path
|
||||
d="M25 3V11L21.9997 11V9.5H23.5V4.5H21.9997V3.00002L25 3Z"
|
||||
fill="#0F172A"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function StringIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
viewBox="-2 -5 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.536 6.845a3.908 3.908 0 0 1 2.598.566l-.025-.032a4.114 4.114 0 0 1 1.766 2.478 4.228 4.228 0 0 1-.391 3.047 4.026 4.026 0 0 1-2.33 1.92 3.898 3.898 0 0 1-2.973-.273h-.03a1.296 1.296 0 0 0-.082-.045c-.033-.018-.066-.035-.096-.056a4.236 4.236 0 0 1-.99-.88A7.746 7.746 0 0 1 .095 9.743a8.717 8.717 0 0 1 .191-3.498c.3-1.14.827-2.203 1.55-3.12A8.282 8.282 0 0 1 4.477.918 8.047 8.047 0 0 1 7.763 0c.287 0 .562.117.765.326.203.209.317.492.317.787 0 .296-.114.579-.317.788a1.066 1.066 0 0 1-.765.326c-.96.04-1.895.332-2.716.847A5.758 5.758 0 0 0 3.07 5.17a6.53 6.53 0 0 0-.905 2.906 3.962 3.962 0 0 1 2.37-1.232ZM15.53 6.83c.901-.12 1.815.079 2.591.565h-.006a4.105 4.105 0 0 1 1.761 2.473 4.22 4.22 0 0 1-.39 3.04 4.016 4.016 0 0 1-2.324 1.917 3.886 3.886 0 0 1-2.966-.273h-.036c-.03-.021-.063-.038-.097-.056a1.317 1.317 0 0 1-.08-.045 4.226 4.226 0 0 1-.987-.879 7.782 7.782 0 0 1-1.902-3.848 8.715 8.715 0 0 1 .194-3.49 8.564 8.564 0 0 1 1.546-3.111A8.276 8.276 0 0 1 15.469.92 8.037 8.037 0 0 1 18.742 0c.286 0 .56.117.763.325.202.209.316.491.316.786 0 .295-.114.577-.316.786a1.063 1.063 0 0 1-.763.325 5.526 5.526 0 0 0-2.707.846 5.738 5.738 0 0 0-1.968 2.092 6.519 6.519 0 0 0-.902 2.9 3.952 3.952 0 0 1 2.364-1.23Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { transition } from "../../utilities/animationConstants";
|
||||
|
||||
export const SunIcon = () => {
|
||||
const whileTap = { scale: 0.95, rotate: 15 };
|
||||
|
||||
const raysVariants = {
|
||||
initial: { rotate: 45 },
|
||||
animate: { rotate: 0, transition },
|
||||
};
|
||||
|
||||
const coreVariants = {
|
||||
initial: { scale: 1.5 },
|
||||
animate: { scale: 1, transition },
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.svg
|
||||
key="sun"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
whileTap={whileTap}
|
||||
// Centers the rotation anchor point vertically & horizontally
|
||||
style={{ originX: "50%", originY: "50%" }}
|
||||
>
|
||||
<motion.circle
|
||||
cx="11.9998"
|
||||
cy="11.9998"
|
||||
r="5.75375"
|
||||
fill="currentColor"
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
variants={coreVariants}
|
||||
/>
|
||||
<motion.g initial="initial" animate="animate" variants={raysVariants}>
|
||||
<circle
|
||||
cx="3.08982"
|
||||
cy="6.85502"
|
||||
r="1.71143"
|
||||
transform="rotate(-60 3.08982 6.85502)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<circle
|
||||
cx="3.0903"
|
||||
cy="17.1436"
|
||||
r="1.71143"
|
||||
transform="rotate(-120 3.0903 17.1436)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<circle cx="12" cy="22.2881" r="1.71143" fill="currentColor" />
|
||||
<circle
|
||||
cx="20.9101"
|
||||
cy="17.1436"
|
||||
r="1.71143"
|
||||
transform="rotate(-60 20.9101 17.1436)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<circle
|
||||
cx="20.9101"
|
||||
cy="6.8555"
|
||||
r="1.71143"
|
||||
transform="rotate(-120 20.9101 6.8555)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<circle cx="12" cy="1.71143" r="1.71143" fill="currentColor" />
|
||||
</motion.g>
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export function TreeIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="28"
|
||||
height="30"
|
||||
viewBox="0 0 28 30"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.1805 30C14.5093 30 14.8245 29.8694 15.0571 29.6368C15.2895 29.4044 15.4201 29.089 15.4201 28.7602V22.5619H21.6184C23.2624 22.5619 24.839 21.9091 26.0014 20.7467C27.1638 19.5843 27.8167 18.0077 27.8167 16.3636C27.8234 14.3646 26.854 12.4882 25.2197 11.3368C25.2971 10.9512 25.3364 10.5588 25.3375 10.1653C25.3379 8.64671 24.7808 7.18076 23.7722 6.04565C22.7633 4.91048 21.3726 4.18522 19.8643 4.00752C19.2285 2.5124 18.0292 1.3282 16.5263 0.711248C15.0232 0.0942975 13.3379 0.0942975 11.8348 0.711248C10.332 1.3282 9.13259 2.51246 8.49682 4.00752C6.98853 4.18522 5.59785 4.91048 4.58897 6.04565C3.58025 7.18082 3.02318 8.64671 3.02362 10.1653C3.0247 10.5588 3.06405 10.9511 3.14144 11.3368C1.50714 12.4882 0.537772 14.3646 0.544468 16.3636C0.544468 18.0077 1.19734 19.5843 2.35974 20.7467C3.52214 21.9091 5.09872 22.5619 6.74276 22.5619H12.941V28.7602C12.941 29.089 13.0716 29.4044 13.304 29.6368C13.5366 29.8694 13.8518 30 14.1806 30H14.1805ZM6.74257 20.0827C5.75616 20.0827 4.81014 19.6908 4.11272 18.9934C3.41531 18.296 3.02337 17.35 3.02337 16.3636C3.02273 15.6644 3.22118 14.9796 3.59561 14.389C3.96981 13.7986 4.50443 13.3267 5.13716 13.029C5.41473 12.8941 5.63221 12.6606 5.7468 12.3742C5.86138 12.0875 5.86505 11.7685 5.75696 11.4794C5.31725 10.3533 5.4569 9.0835 6.13052 8.07999C6.80414 7.07625 7.92631 6.466 9.13497 6.4463C9.18145 6.4463 9.30857 6.46489 9.35202 6.46489C9.63457 6.47851 9.91302 6.39311 10.1391 6.22341C10.3655 6.05371 10.5255 5.8103 10.5916 5.53507C10.8614 4.46133 11.5979 3.56463 12.599 3.0914C13.6002 2.6184 14.7606 2.6184 15.7617 3.0914C16.7628 3.56461 17.4993 4.46133 17.7691 5.53507C17.8363 5.80962 17.9965 6.05239 18.2227 6.22186C18.4486 6.39135 18.7266 6.47739 19.0087 6.46485C19.083 6.46485 19.1544 6.46485 19.1388 6.44928L19.139 6.4495C20.3615 6.43934 21.5099 7.03579 22.2045 8.04207C22.8991 9.04841 23.0497 10.3333 22.607 11.473C22.4987 11.7621 22.5024 12.0811 22.6169 12.3678C22.7317 12.6545 22.949 12.8879 23.2268 13.0226C24.2385 13.5174 24.9716 14.444 25.2202 15.5424C25.4691 16.6408 25.2066 17.7928 24.5066 18.675C23.8066 19.5572 22.7445 20.0748 21.6182 20.0826H15.42V16.9151L19.8735 13.6424C20.1495 13.4519 20.3365 13.1579 20.3919 12.8272C20.4472 12.4965 20.3664 12.1575 20.1677 11.8875C19.969 11.6175 19.6692 11.4394 19.3371 11.3942C19.0049 11.3488 18.6685 11.4398 18.4045 11.6467L15.4199 13.8379V8.92563C15.4199 8.48267 15.1836 8.07347 14.8002 7.8521C14.4167 7.63074 13.9441 7.63074 13.5606 7.8521C13.1771 8.07347 12.9408 8.48272 12.9408 8.92563V15.1239L9.96256 12.9018C9.60716 12.6372 9.13741 12.5823 8.73054 12.7578C8.32368 12.9334 8.04136 13.3125 7.9899 13.7527C7.93844 14.1927 8.12565 14.627 8.48105 14.8916L12.9408 18.2045V20.0827L6.74257 20.0827Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function TwitterIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="12" cy="12" r="12" fill="#F8FAFC" />
|
||||
<path
|
||||
d="M5.43319 6H5.44396C5.50679 6.07822 5.56783 6.15853 5.63271 6.23467C6.27615 6.99369 6.98755 7.67709 7.80461 8.24238C8.73734 8.88746 9.75135 9.33515 10.8628 9.5487C11.3459 9.64122 11.8363 9.68896 12.3279 9.69132C12.3435 9.69076 12.359 9.68937 12.3744 9.68715C12.3664 9.65326 12.3582 9.62536 12.3538 9.59694C12.2968 9.24285 12.298 8.8816 12.3572 8.52789C12.5788 7.19811 13.6625 6.20938 14.9433 6.03181C15.0276 6.02008 15.1125 6.01069 15.1971 6H15.5621C15.5811 6.00425 15.6004 6.00747 15.6198 6.00965C15.9463 6.0339 16.2671 6.10973 16.5707 6.23441C16.9759 6.40128 17.347 6.64347 17.665 6.94858C17.6748 6.9577 17.6863 6.96472 17.6988 6.9692C17.7113 6.97368 17.7246 6.97554 17.7378 6.97465C18.2937 6.87178 18.8309 6.68327 19.3309 6.41562C19.4296 6.36347 19.5276 6.30924 19.6387 6.24901C19.3596 6.95666 18.9111 7.50057 18.2866 7.89872C18.8855 7.84345 19.4486 7.66119 20 7.42652C19.9379 7.51934 19.8738 7.60773 19.8071 7.6943C19.4338 8.17928 19.0121 8.61524 18.5141 8.96803C18.4917 8.98129 18.4736 9.00084 18.4618 9.02433C18.4501 9.04783 18.4453 9.07427 18.4479 9.10048C18.4686 9.8569 18.3891 10.6127 18.2115 11.3476C17.928 12.5242 17.41 13.6293 16.6897 14.5943C15.6526 15.9935 14.2124 17.0293 12.5695 17.5579C12.1008 17.7133 11.6191 17.8245 11.1303 17.8901C10.8077 17.9305 10.4838 17.9657 10.1596 17.9845C9.618 18.016 9.07662 17.996 8.53576 17.9558C8.04402 17.9215 7.55544 17.8504 7.07398 17.743C6.48868 17.6143 5.9222 17.4092 5.38857 17.1329C5.26034 17.0656 5.13699 16.9916 5.01132 16.9207L5.01517 16.9071H5.07672C5.59886 16.9102 6.12023 16.9071 6.63826 16.8229C7.15926 16.7393 7.66519 16.5776 8.13954 16.3431C8.63963 16.0944 9.09073 15.7695 9.53619 15.4334C9.54091 15.4282 9.54481 15.4223 9.54773 15.4159C9.1274 15.4081 8.75888 15.2882 8.4837 15.1716C7.99209 14.9607 7.53859 14.6678 7.14194 14.3049C6.77034 13.9696 6.44618 13.5941 6.21717 13.1417C6.15818 13.0254 6.11177 12.9026 6.05535 12.7736C6.52466 12.8779 6.96935 12.8317 7.40942 12.7071C5.75248 12.3707 4.93798 10.8409 5.00748 9.69654C5.43653 9.89705 5.89019 9.99691 6.35411 10.0566C6.22307 9.94633 6.08663 9.84568 5.96661 9.72757C5.13109 8.90571 4.83078 7.91854 5.09083 6.76267C5.15514 6.48825 5.27143 6.2292 5.43319 6Z"
|
||||
fill="#4338CA"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { inferType } from "@jsonhero/json-infer-types";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { useMemo } from "react";
|
||||
import { useJson } from "~/hooks/useJson";
|
||||
import { useJsonColumnViewState } from "~/hooks/useJsonColumnView";
|
||||
import { concatenated, getHierarchicalTypes } from "~/utilities/dataType";
|
||||
import { formatRawValue } from "~/utilities/formatter";
|
||||
import { isNullable } from "~/utilities/nullable";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { LargeMono } from "./Primitives/LargeMono";
|
||||
import { Title } from "./Primitives/Title";
|
||||
import { ValueIcon, ValueIconSize } from "./ValueIcon";
|
||||
import { CopyText } from "./CopyText";
|
||||
|
||||
export function InfoHeader() {
|
||||
const { selectedNodeId, highlightedNodeId, selectedNodes } =
|
||||
useJsonColumnViewState();
|
||||
|
||||
if (!selectedNodeId || !highlightedNodeId) {
|
||||
return <EmptyState />;
|
||||
}
|
||||
|
||||
const selectedNode = selectedNodes[selectedNodes.length - 1];
|
||||
|
||||
const [json] = useJson();
|
||||
|
||||
const selectedHeroPath = new JSONHeroPath(selectedNodeId);
|
||||
const selectedJson = selectedHeroPath.first(json);
|
||||
const selectedInfo = inferType(selectedJson);
|
||||
const selectedName = selectedNode.longTitle ?? selectedNode.title;
|
||||
|
||||
const isSelectedLeafNode =
|
||||
selectedInfo.name !== "object" && selectedInfo.name !== "array";
|
||||
|
||||
const canBeNull = useMemo(() => {
|
||||
return isNullable(selectedNodeId, json);
|
||||
}, [selectedNodeId, json]);
|
||||
|
||||
return (
|
||||
<div className="mb-4 pb-4">
|
||||
<div className="flex items-center">
|
||||
<Title className="flex-1 mr-2 text-slate-700 transition dark:text-slate-400">
|
||||
{selectedName ?? "nothing"}
|
||||
</Title>
|
||||
<div>
|
||||
<ValueIcon type={selectedInfo} size={ValueIconSize.Medium} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isSelectedLeafNode && (
|
||||
<CopyText
|
||||
className="after:w-5 after:h-5 after:top-0.5 after:right-0.5"
|
||||
value={formatRawValue(selectedInfo)}
|
||||
>
|
||||
<LargeMono className="text-slate-800 mb-1 overflow-ellipsis break-words dark:text-slate-300">
|
||||
{formatRawValue(selectedInfo)}
|
||||
</LargeMono>
|
||||
</CopyText>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-gray-400">
|
||||
<Body className="flex-1">
|
||||
{concatenated(getHierarchicalTypes(selectedInfo))}
|
||||
</Body>
|
||||
{canBeNull && <Body>Can be null</Body>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="mb-4 pb-4 border-b border-slate-300">
|
||||
<div className="flex items-center">
|
||||
<Title className="flex-1 mr-2 text-slate-800 transition dark:text-slate-300">
|
||||
Nothing selected
|
||||
</Title>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<Title className="text-indigo-600 overflow-ellipsis break-words">
|
||||
null
|
||||
</Title>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PreviewValue } from "./Preview/PreviewValue";
|
||||
import { RelatedValues } from "./RelatedValues";
|
||||
import { PropertiesValue } from "./Properties/PropertiesValue";
|
||||
import { InfoHeader } from "./InfoHeader";
|
||||
import { ContainerInfo } from "./ContainerInfo";
|
||||
import { useSelectedInfo } from "~/hooks/useSelectedInfo";
|
||||
|
||||
export function InfoPanel() {
|
||||
const selectedInfo = useSelectedInfo();
|
||||
|
||||
if (!selectedInfo) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const isSelectedLeafNode =
|
||||
selectedInfo.name !== "object" && selectedInfo.name !== "array";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-inspectorHeight p-4 bg-white border-l-[1px] border-slate-300 overflow-y-auto no-scrollbar transition dark:bg-slate-800 dark:border-slate-600">
|
||||
<InfoHeader />
|
||||
|
||||
<div className="mb-4">
|
||||
<PreviewValue />
|
||||
</div>
|
||||
<PropertiesValue />
|
||||
|
||||
<ContainerInfo />
|
||||
|
||||
{isSelectedLeafNode && <RelatedValues />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useMemo } from "react";
|
||||
import { Column } from "./Column";
|
||||
import { ColumnItem } from "./ColumnItem";
|
||||
import { ScrollingColumnView } from "./ScrollingColumnView";
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "../hooks/useJsonColumnView";
|
||||
import { useJson } from "~/hooks/useJson";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { inferType } from "@jsonhero/json-infer-types";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export function JsonColumnView() {
|
||||
const { getColumnViewProps, columns, highlightedPath, selectedPath } =
|
||||
useJsonColumnViewState();
|
||||
const [json] = useJson();
|
||||
|
||||
const addBlankColumn = useMemo<boolean>(() => {
|
||||
if (columns.length === 0) return true;
|
||||
|
||||
const deepestElementId = selectedPath[selectedPath.length - 1];
|
||||
const heroPath = new JSONHeroPath(deepestElementId);
|
||||
const value = heroPath.first(json);
|
||||
const item = inferType(value);
|
||||
|
||||
let isObject = item.name === "array" || item.name === "object";
|
||||
return !isObject;
|
||||
}, [columns, selectedPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<KeyboardShortcuts />
|
||||
<div {...getColumnViewProps()}>
|
||||
<ScrollingColumnView selectedPath={highlightedPath}>
|
||||
{columns.map((column) => {
|
||||
return (
|
||||
<Column
|
||||
id={column.id}
|
||||
title={column.title}
|
||||
key={column.id}
|
||||
icon={column.icon}
|
||||
>
|
||||
{column.items.map((item) => (
|
||||
<ColumnItem key={item.id} item={item} />
|
||||
))}
|
||||
</Column>
|
||||
);
|
||||
})}
|
||||
{addBlankColumn && (
|
||||
<div className="w-80 h-viewerHeight no-scrollbar flex-none"></div>
|
||||
)}
|
||||
</ScrollingColumnView>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardShortcuts() {
|
||||
const api = useJsonColumnViewAPI();
|
||||
|
||||
useHotkeys(
|
||||
"down",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
api.goToNextSibling();
|
||||
},
|
||||
{ enabled: true },
|
||||
[api]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"up",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
api.goToPreviousSibling();
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"right",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
api.goToChildren();
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"left,alt+left",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
api.goToParent({ source: e });
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"esc",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
api.resetSelection();
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
import { useJson } from "~/hooks/useJson";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "~/hooks/useJsonColumnView";
|
||||
import { ViewUpdate } from "@uiw/react-codemirror";
|
||||
import jsonMap from "json-source-map";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
|
||||
export function JsonEditor() {
|
||||
const [json] = useJson();
|
||||
const { selectedNodeId } = useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
const jsonMapped = useMemo(() => {
|
||||
return jsonMap.stringify(json, null, 2);
|
||||
}, [json]);
|
||||
|
||||
const selection = useMemo<{ start: number; end: number } | undefined>(() => {
|
||||
if (!selectedNodeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = new JSONHeroPath(selectedNodeId);
|
||||
const pointer = path.jsonPointer();
|
||||
|
||||
const location = jsonMapped.pointers[pointer];
|
||||
|
||||
if (location) {
|
||||
if (location.key) {
|
||||
return { start: location.key.pos, end: location.valueEnd.pos };
|
||||
}
|
||||
|
||||
return { start: location.value.pos, end: location.valueEnd.pos };
|
||||
}
|
||||
}, [selectedNodeId, jsonMapped]);
|
||||
|
||||
const currentSelectedLine = useRef<number | undefined>(undefined);
|
||||
|
||||
const onUpdate = useCallback(
|
||||
(update: ViewUpdate) => {
|
||||
if (!update.selectionSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = update.state.selection.ranges[0];
|
||||
const line = update.state.doc.lineAt(range.anchor);
|
||||
|
||||
if (
|
||||
currentSelectedLine.current &&
|
||||
currentSelectedLine.current === line.number
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentSelectedLine.current = line.number;
|
||||
|
||||
// Find the key if the selected line using jsonMapped.pointers
|
||||
const pointerEntry = Object.entries(jsonMapped.pointers).find(
|
||||
([pointer, info]) => {
|
||||
return info.value.line === line.number - 1;
|
||||
}
|
||||
);
|
||||
|
||||
if (!pointerEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [pointer] = pointerEntry;
|
||||
|
||||
const path = JSONHeroPath.fromPointer(pointer);
|
||||
|
||||
goToNodeId(path.toString());
|
||||
},
|
||||
[goToNodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
language="json"
|
||||
content={jsonMapped.json}
|
||||
readOnly={false}
|
||||
onUpdate={onUpdate}
|
||||
selection={selection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { RangeSetBuilder } from "@codemirror/rangeset";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import {
|
||||
useCodeMirror,
|
||||
EditorView,
|
||||
Decoration,
|
||||
Facet,
|
||||
ViewPlugin,
|
||||
Compartment,
|
||||
TransactionSpec,
|
||||
} from "@uiw/react-codemirror";
|
||||
import jsonMap from "json-source-map";
|
||||
import { useRef, useEffect, useMemo } from "react";
|
||||
import { getPreviewSetup } from "~/utilities/codeMirrorSetup";
|
||||
import { lightTheme, darkTheme } from "~/utilities/codeMirrorTheme";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
|
||||
export type JsonPreviewProps = {
|
||||
json: unknown;
|
||||
highlightPath?: string;
|
||||
};
|
||||
|
||||
export function JsonPreview({ json, highlightPath }: JsonPreviewProps) {
|
||||
const editor = useRef(null);
|
||||
|
||||
const jsonMapped = useMemo(() => {
|
||||
return jsonMap.stringify(json, null, 2);
|
||||
}, [json]);
|
||||
|
||||
const lines: LineRange | undefined = useMemo(() => {
|
||||
if (!highlightPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = new JSONHeroPath(highlightPath);
|
||||
let pointer = path.jsonPointer();
|
||||
|
||||
let selectionInfo = jsonMapped.pointers[pointer];
|
||||
|
||||
return {
|
||||
from: selectionInfo.value.line + 1,
|
||||
to: selectionInfo.valueEnd.line + 1,
|
||||
};
|
||||
}, [jsonMapped, highlightPath]);
|
||||
|
||||
const extensions = getPreviewSetup();
|
||||
|
||||
const highlighting = new Compartment();
|
||||
|
||||
if (lines) {
|
||||
extensions.push(highlighting.of(highlightLineRange(lines)));
|
||||
}
|
||||
|
||||
const [theme] = useTheme();
|
||||
|
||||
const { setContainer, view } = useCodeMirror({
|
||||
container: editor.current,
|
||||
extensions,
|
||||
value: jsonMapped.json,
|
||||
editable: false,
|
||||
contentEditable: false,
|
||||
autoFocus: false,
|
||||
basicSetup: false,
|
||||
theme: theme === "light" ? lightTheme() : darkTheme(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
setContainer(editor.current);
|
||||
}
|
||||
}, [editor.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
let transactionSpec: TransactionSpec = {
|
||||
changes: { from: 0, to: view.state.doc.length, insert: jsonMapped.json },
|
||||
};
|
||||
|
||||
let range = lines;
|
||||
if (range != null) {
|
||||
transactionSpec.effects = highlighting.reconfigure(
|
||||
highlightLineRange(range)
|
||||
);
|
||||
}
|
||||
|
||||
view.dispatch(transactionSpec);
|
||||
}, [view, highlighting, jsonMapped, highlightPath]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={editor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LineRange {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
const baseTheme = EditorView.baseTheme({
|
||||
"&light .cm-highlighted": { backgroundColor: "#ffee0055" },
|
||||
"&dark .cm-highlighted": { backgroundColor: "#ffee0055" },
|
||||
});
|
||||
|
||||
const highlightedRange = Facet.define<LineRange, LineRange>({
|
||||
combine: (values) => (values.length ? values[0] : { from: -1, to: -1 }),
|
||||
});
|
||||
|
||||
function highlightLineRange(range: LineRange | null) {
|
||||
return [
|
||||
baseTheme,
|
||||
range == null ? [] : highlightedRange.of(range),
|
||||
highlightLineRangePlugin,
|
||||
];
|
||||
}
|
||||
const lineHighlightDecoration = Decoration.line({
|
||||
attributes: { class: "cm-highlighted" },
|
||||
});
|
||||
|
||||
function highlightLines(view: EditorView) {
|
||||
let highlightRange = view.state.facet(highlightedRange);
|
||||
let builder = new RangeSetBuilder();
|
||||
for (let { from, to } of view.visibleRanges) {
|
||||
for (let pos = from; pos <= to; ) {
|
||||
let line = view.state.doc.lineAt(pos);
|
||||
if (
|
||||
line.number >= highlightRange.from &&
|
||||
line.number <= highlightRange.to
|
||||
) {
|
||||
builder.add(line.from, line.from, lineHighlightDecoration);
|
||||
}
|
||||
pos = line.to + 1;
|
||||
}
|
||||
}
|
||||
return builder.finish();
|
||||
}
|
||||
|
||||
const highlightLineRangePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: any;
|
||||
constructor(view: any) {
|
||||
this.decorations = highlightLines(view);
|
||||
}
|
||||
|
||||
update(update: { docChanged: any; viewportChanged: any; view: any }) {
|
||||
if (update.docChanged || update.viewportChanged)
|
||||
this.decorations = highlightLines(update.view);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { useJsonSchema } from "~/hooks/useJsonSchema";
|
||||
import { CodeViewer } from "./CodeViewer";
|
||||
|
||||
export function JsonSchemaViewer({ path }: { path: string }) {
|
||||
const schema = useJsonSchema();
|
||||
const schemaPath = schemaPathFromPath(path);
|
||||
const schemaJson = schemaPath.first(schema);
|
||||
|
||||
return <CodeViewer code={JSON.stringify(schemaJson, null, 2)} lang="json" />;
|
||||
}
|
||||
|
||||
function schemaPathFromPath(path: JSONHeroPath | string): JSONHeroPath {
|
||||
const heroPath = typeof path === "string" ? new JSONHeroPath(path) : path;
|
||||
|
||||
if (heroPath.isRoot) {
|
||||
return heroPath;
|
||||
}
|
||||
|
||||
return heroPath.components.slice(1).reduce((acc, component) => {
|
||||
if (component.isArray) {
|
||||
return acc.child("items");
|
||||
} else {
|
||||
return acc.child("properties").child(component.toString());
|
||||
}
|
||||
}, new JSONHeroPath("$"));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { PathBar, PathHistoryControls } from "./PathBar";
|
||||
|
||||
export function JsonView({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="path-bar-and-column-wrapper flex flex-col flex-grow overflow-x-hidden border-l-[1px] border-slate-300 transition dark:border-slate-600">
|
||||
<div className="path-bar p-1 flex bg-slate-200 border-slate-300 border-b-[1px] transition dark:bg-slate-900 dark:border-slate-600">
|
||||
<div className="flex-shrink-0 flex-grow-0">
|
||||
<PathHistoryControls />
|
||||
</div>
|
||||
<div className="flex-1 pr-2 min-w-0">
|
||||
<PathBar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { DragAndDropForm } from "./DragAndDropForm";
|
||||
import { UrlForm } from "./UrlForm";
|
||||
|
||||
export function NewDocument() {
|
||||
return (
|
||||
<div className="bg-indigo-700 text-white rounded-sm shadow-md w-80 max-w-max p-3 transition">
|
||||
<div className="flex flex-col">
|
||||
<UrlForm className="mb-2" />
|
||||
<DragAndDropForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { DragAndDropForm } from "./DragAndDropForm";
|
||||
import { Title } from "./Primitives/Title";
|
||||
import { SampleUrls } from "./SampleUrls";
|
||||
import { UrlForm } from "./UrlForm";
|
||||
|
||||
export function NewFile() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<UrlForm />
|
||||
</div>
|
||||
<DragAndDropForm />
|
||||
|
||||
<div className="mt-4 pt-5">
|
||||
<Title className="text-slate-400">No JSON? Try it out:</Title>
|
||||
<SampleUrls />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { ColumnViewNode } from "~/useColumnView";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "../hooks/useJsonColumnView";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export function PathBar() {
|
||||
const { selectedNodes, highlightedNodeId } = useJsonColumnViewState();
|
||||
|
||||
return (
|
||||
<PathBarLink
|
||||
selectedNodes={selectedNodes}
|
||||
highlightedNodeId={highlightedNodeId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type PathBarLinkProps = {
|
||||
selectedNodes: ColumnViewNode[];
|
||||
highlightedNodeId?: string;
|
||||
};
|
||||
|
||||
export function PathBarLink({
|
||||
selectedNodes,
|
||||
highlightedNodeId,
|
||||
}: PathBarLinkProps) {
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
return (
|
||||
<div className="flex flex-shrink-0 flex-grow-0 overflow-x-hidden">
|
||||
{selectedNodes.map((node, index) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center min-w-0"
|
||||
style={{ flexShrink: 1 }}
|
||||
key={node.id}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center hover:cursor-pointer min-w-0 transition ${
|
||||
highlightedNodeId === node.id
|
||||
? "text-slate-700 bg-slate-300 px-2 py-[3px] rounded-sm dark:text-white dark:bg-slate-700"
|
||||
: "hover:bg-slate-300 px-2 py-[3px] rounded-sm transition dark:hover:bg-white dark:hover:bg-opacity-[5%]"
|
||||
}`}
|
||||
style={{ flexShrink: 1 }}
|
||||
onClick={() => goToNodeId(node.id)}
|
||||
>
|
||||
<div className="w-4 flex-shrink-[0.5] flex-grow-0 flex-col justify-items-center whitespace-nowrap overflow-x-hidden transition dark:text-slate-400">
|
||||
{node.icon && <node.icon className="h-3 w-3" />}
|
||||
</div>
|
||||
<Body className="flex-shrink flex-grow-0 whitespace-nowrap overflow-x-hidden text-ellipsis transition dark:text-slate-400">
|
||||
{node.title}
|
||||
</Body>
|
||||
</div>
|
||||
|
||||
{index == selectedNodes.length - 1 ? (
|
||||
<></>
|
||||
) : (
|
||||
<ChevronRightIcon className="flex-grow-0 flex-shrink-[0.5] w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PathHistoryControls() {
|
||||
const { canGoBack, canGoForward } = useJsonColumnViewState();
|
||||
const { goBack, goForward } = useJsonColumnViewAPI();
|
||||
|
||||
useHotkeys(
|
||||
"[",
|
||||
() => {
|
||||
goBack();
|
||||
},
|
||||
[goBack]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
"]",
|
||||
() => {
|
||||
goForward();
|
||||
},
|
||||
[goForward]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<button
|
||||
className="flex justify-center items-center w-[26px] h-[26px] disabled:text-slate-400 disabled:text-opacity-50 text-slate-700 hover:bg-slate-300 hover:disabled:bg-transparent rounded-sm transition dark:disabled:text-slate-700 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:hover:disabled:bg-transparent"
|
||||
disabled={!canGoBack}
|
||||
onClick={goBack}
|
||||
>
|
||||
<ArrowLeftIcon className="w-5 h-6" />
|
||||
</button>
|
||||
<button
|
||||
className="flex justify-center items-center w-[26px] h-[26px] disabled:text-slate-400 disabled:text-opacity-50 text-slate-700 hover:bg-slate-300 hover:disabled:bg-transparent rounded-sm transition dark:disabled:text-slate-700 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:hover:disabled:bg-transparent"
|
||||
disabled={!canGoForward}
|
||||
onClick={goForward}
|
||||
>
|
||||
<ArrowRightIcon className="w-5 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { ChevronRightIcon, EyeIcon } from "@heroicons/react/outline";
|
||||
import { useMemo } from "react";
|
||||
import { useJsonColumnViewAPI } from "~/hooks/useJsonColumnView";
|
||||
import { ColumnViewNode, IconComponent } from "~/useColumnView";
|
||||
import { Body } from "./Primitives/Body";
|
||||
|
||||
export type PathPreviewProps = {
|
||||
nodes: ColumnViewNode[];
|
||||
maxComponents?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
type ValueComponent = {
|
||||
type: "value";
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: IconComponent;
|
||||
};
|
||||
|
||||
type EllipsisComponent = {
|
||||
type: "ellipsis";
|
||||
id: "ellipsis";
|
||||
};
|
||||
|
||||
type Component = ValueComponent | EllipsisComponent;
|
||||
|
||||
export function PathPreview({
|
||||
nodes,
|
||||
maxComponents,
|
||||
enabled,
|
||||
}: PathPreviewProps) {
|
||||
const isEnabled = useMemo(() => {
|
||||
if (enabled === undefined) {
|
||||
return true;
|
||||
}
|
||||
return enabled;
|
||||
}, [enabled]);
|
||||
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
const components = useMemo<Array<Component>>(() => {
|
||||
if (maxComponents == null || nodes.length <= maxComponents) {
|
||||
return nodes.map((n) => {
|
||||
return { type: "value", id: n.id, title: n.title, icon: n.icon };
|
||||
});
|
||||
}
|
||||
|
||||
let components = Array<Component>();
|
||||
|
||||
//add the elements up to the ellipsis
|
||||
for (let index = 0; index < maxComponents - 1; index++) {
|
||||
const node = nodes[index];
|
||||
components.push({
|
||||
type: "value",
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
icon: node.icon,
|
||||
});
|
||||
}
|
||||
|
||||
//add ellipsis
|
||||
components.push({ type: "ellipsis", id: "ellipsis" });
|
||||
|
||||
//add final element
|
||||
const lastNode = nodes[nodes.length - 1];
|
||||
components.push({
|
||||
type: "value",
|
||||
id: lastNode.id,
|
||||
title: lastNode.title,
|
||||
icon: lastNode.icon,
|
||||
});
|
||||
|
||||
return components;
|
||||
}, [nodes, maxComponents]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex select-none pl-7 ${
|
||||
isEnabled
|
||||
? "relative transition hover:bg-slate-200 hover:cursor-pointer dark:hover:bg-slate-600 after:transition after:absolute after:h-3 after:w-3 after:opacity-0 hover:after:opacity-100 after:top-1 after:left-1 after:content-[''] after:bg-[url('/svgs/EyeIcon.svg')] after:bg-no-repeat"
|
||||
: "disabled"
|
||||
}`}
|
||||
onClick={() =>
|
||||
isEnabled && goToNodeId(components[components.length - 1].id)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`flex rounded-sm px-2 ${
|
||||
isEnabled
|
||||
? ""
|
||||
: "hover:bg-slate-100 hover:cursor-pointer dark:hover:bg-slate-600"
|
||||
}`}
|
||||
>
|
||||
{components.map((node, index) => {
|
||||
if (node.type === "ellipsis") {
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
className="flex flex-none items-center min-w-0"
|
||||
>
|
||||
<div className="flex-none text-md">…</div>
|
||||
<ChevronRightIcon className="flex-none w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="flex items-center min-w-0" key={node.id}>
|
||||
<div className="flex items-center min-w-0">
|
||||
<div className="w-4 flex-shrink-[0.5] flex-grow-0 flex-col justify-items-center whitespace-nowrap overflow-x-hidden transition dark:text-slate-300">
|
||||
{node.icon && <node.icon className="h-3 w-3" />}
|
||||
</div>
|
||||
<Body className="flex-shrink flex-grow-0 whitespace-nowrap overflow-x-hidden text-ellipsis transition dark:text-slate-300">
|
||||
{node.title}
|
||||
</Body>
|
||||
</div>
|
||||
|
||||
{index == components.length - 1 ? (
|
||||
<></>
|
||||
) : (
|
||||
<ChevronRightIcon className="flex-grow-0 flex-shrink-[0.5] w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useMemo } from "react";
|
||||
import { Title } from "../Primitives/Title";
|
||||
|
||||
export type CalendarMonthProps = {
|
||||
date: Date;
|
||||
};
|
||||
|
||||
type Day = {
|
||||
date: string;
|
||||
isCurrentMonth: boolean;
|
||||
isHighlighted: boolean;
|
||||
};
|
||||
|
||||
function dateString(date: Date): string {
|
||||
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
}
|
||||
|
||||
function isSameDay(date: Date, otherDate: Date): boolean {
|
||||
return (
|
||||
date.getFullYear() === otherDate.getFullYear() &&
|
||||
date.getMonth() === otherDate.getMonth() &&
|
||||
date.getDate() === otherDate.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarMonth({ date }: CalendarMonthProps) {
|
||||
const days = useMemo<Array<Day>>(() => {
|
||||
let days: Array<Day> = [];
|
||||
|
||||
//create first day of the month
|
||||
const firstDayOfMonth = new Date(date);
|
||||
firstDayOfMonth.setDate(1);
|
||||
|
||||
//if the first day isn't a monday, we need to add days from the previous month in
|
||||
const firstDayOfWeek = firstDayOfMonth.getDay() - 1;
|
||||
if (firstDayOfWeek != 0) {
|
||||
const previousMonthDate = new Date(firstDayOfMonth);
|
||||
for (let index = 0; index < firstDayOfWeek; index++) {
|
||||
previousMonthDate.setDate(previousMonthDate.getDate() - 1);
|
||||
days.push({
|
||||
date: dateString(previousMonthDate),
|
||||
isCurrentMonth: false,
|
||||
isHighlighted: isSameDay(date, previousMonthDate),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//current month
|
||||
let currentMonthDate = new Date(firstDayOfMonth);
|
||||
const monthNumber = firstDayOfMonth.getMonth();
|
||||
while (true) {
|
||||
days.push({
|
||||
date: dateString(currentMonthDate),
|
||||
isCurrentMonth: true,
|
||||
isHighlighted: isSameDay(date, currentMonthDate),
|
||||
});
|
||||
|
||||
currentMonthDate.setDate(currentMonthDate.getDate() + 1);
|
||||
|
||||
if (currentMonthDate.getMonth() !== monthNumber) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//next month
|
||||
const lastDayOfMonthDayOfWeek = currentMonthDate.getDay() - 1;
|
||||
const nextMonthDayCount = 7 - lastDayOfMonthDayOfWeek;
|
||||
for (let index = 0; index < nextMonthDayCount; index++) {
|
||||
days.push({
|
||||
date: dateString(currentMonthDate),
|
||||
isCurrentMonth: false,
|
||||
isHighlighted: isSameDay(date, currentMonthDate),
|
||||
});
|
||||
currentMonthDate.setDate(currentMonthDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return days;
|
||||
}, [date]);
|
||||
|
||||
return (
|
||||
<section className="">
|
||||
<Title className="text-left text-slate-800 dark:text-slate-400">
|
||||
{new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
hour12: true,
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZoneName: "short",
|
||||
}).format(date)}
|
||||
</Title>
|
||||
<div className="uppercase mt-2 grid text-center tracking-wider grid-cols-7 text-sm leading-6 text-gray-500 dark:text-slate-500">
|
||||
<div>Mon</div>
|
||||
<div>Tue</div>
|
||||
<div>Wed</div>
|
||||
<div>Thu</div>
|
||||
<div>Fri</div>
|
||||
<div>Sat</div>
|
||||
<div>Sun</div>
|
||||
</div>
|
||||
<div className="isolate mt-2 grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm ring-1 cursor-default ring-slate-200 dark:ring-slate-600 dark:bg-slate-600">
|
||||
{days.map((day, dayIdx) => (
|
||||
<button
|
||||
key={day.date}
|
||||
type="button"
|
||||
className={`"cursor-default" ${classNames(
|
||||
day.isCurrentMonth
|
||||
? "bg-white text-slate-900 dark:text-slate-300 dark:bg-slate-800"
|
||||
: "bg-slate-100 text-slate-400 dark:text-slate-500 dark:bg-slate-800 dark:bg-opacity-90",
|
||||
dayIdx === 0 && "rounded-tl-lg",
|
||||
dayIdx === 6 && "rounded-tr-lg",
|
||||
dayIdx === days.length - 7 && "rounded-bl-lg",
|
||||
dayIdx === days.length - 1 && "rounded-br-lg",
|
||||
"relative py-1.5 focus:z-10"
|
||||
)}`}
|
||||
>
|
||||
<time
|
||||
dateTime={day.date}
|
||||
className={classNames(
|
||||
day.isHighlighted && "bg-indigo-600 font-semibold text-white",
|
||||
"mx-auto flex h-7 w-7 items-center cursor-default justify-center rounded-full"
|
||||
)}
|
||||
>
|
||||
{day.date.split("-").pop()?.replace(/^0/, "")}
|
||||
</time>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function classNames(...classes: (string | boolean)[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Title } from "../Primitives/Title";
|
||||
|
||||
export type PreviewBoxProps = {
|
||||
link?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function PreviewBox({ link, children }: PreviewBoxProps) {
|
||||
return (
|
||||
<>
|
||||
<Title className="text-slate-700 transition dark:text-slate-400 mb-2">
|
||||
Preview
|
||||
</Title>
|
||||
<a
|
||||
className="block rounded-sm p-2 text-slate-700 bg-slate-100 transition dark:text-slate-300 dark:bg-white dark:bg-opacity-5 hover:bg-slate-200 hover:cursor-pointer"
|
||||
href={link}
|
||||
target="_blank"
|
||||
>
|
||||
<div>{children}</div>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Body } from "../Primitives/Body";
|
||||
|
||||
export type PreviewPropertyProps = {
|
||||
properties: Array<PreviewProperty>;
|
||||
};
|
||||
|
||||
export type PreviewProperty = {
|
||||
key: string;
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
};
|
||||
|
||||
export function PreviewProperties({ properties }: PreviewPropertyProps) {
|
||||
return (
|
||||
<div className="-mb-1">
|
||||
{properties.map((property) => {
|
||||
return (
|
||||
<Body
|
||||
className="text-slate-500 mr-2 inline-flex items-center"
|
||||
key={property.key}
|
||||
>
|
||||
{property.icon && (
|
||||
<span className="w-4 h-4 inline-block text-slate-500 mr-1 flex-none">
|
||||
{property.icon}
|
||||
</span>
|
||||
)}
|
||||
<span>{property.title}</span>
|
||||
</Body>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useSelectedInfo } from "~/hooks/useSelectedInfo";
|
||||
import { PreviewString } from "./Types/PreviewString";
|
||||
|
||||
export function PreviewValue() {
|
||||
const info = useSelectedInfo();
|
||||
|
||||
if (!info) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
switch (info.name) {
|
||||
case "string":
|
||||
return <PreviewString info={info} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Temporal } from "@js-temporal/polyfill";
|
||||
import { JSONDateTimeFormat, JSONStringType } from "@jsonhero/json-infer-types";
|
||||
import { useMemo } from "react";
|
||||
import { inferTemporal } from "~/utilities/inferredTemporal";
|
||||
import { CalendarMonth } from "../CalendarMonth";
|
||||
|
||||
export type PreviewDateProps = {
|
||||
value: string;
|
||||
format: JSONDateTimeFormat;
|
||||
};
|
||||
|
||||
export function PreviewDate({ value, format }: PreviewDateProps) {
|
||||
const temporal = inferTemporal(value, format);
|
||||
|
||||
if (!temporal) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// Can only convert to the legacy Date class if temporal is either a ZonedDateTime or an Instant
|
||||
if ("epochMilliseconds" in temporal) {
|
||||
const date = new Date(temporal.epochMilliseconds);
|
||||
|
||||
return <CalendarMonth date={date} />;
|
||||
} else if (temporal instanceof Temporal.PlainDate) {
|
||||
const date = new Date(temporal.year, temporal.month - 1, temporal.day);
|
||||
|
||||
return <CalendarMonth date={date} />;
|
||||
} else if (temporal instanceof Temporal.PlainDateTime) {
|
||||
const date = new Date(
|
||||
temporal.year,
|
||||
temporal.month - 1,
|
||||
temporal.day,
|
||||
temporal.hour,
|
||||
temporal.minute,
|
||||
temporal.second,
|
||||
temporal.millisecond
|
||||
);
|
||||
|
||||
return <CalendarMonth date={date} />;
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
CalendarIcon,
|
||||
EyeIcon,
|
||||
ThumbUpIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { inferType } from "@jsonhero/json-infer-types";
|
||||
import { Body } from "~/components/Primitives/Body";
|
||||
import { Title } from "~/components/Primitives/Title";
|
||||
import { formatNumber, formatValue } from "~/utilities/formatter";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
import { PreviewProperties, PreviewProperty } from "../PreviewProperties";
|
||||
import { PreviewHtml } from "./preview.types";
|
||||
import { RetweetIcon } from "./RetweetIcon";
|
||||
|
||||
export type PreviewHtmlProps = {
|
||||
info: PreviewHtml;
|
||||
};
|
||||
|
||||
export function PreviewHtml({ info }: PreviewHtmlProps) {
|
||||
const formatDate = (dateString: string): string => {
|
||||
return formatValue(inferType(dateString)) ?? dateString;
|
||||
};
|
||||
|
||||
const details = () => {
|
||||
if (!info.details) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
switch (info.details.type) {
|
||||
case "youtube": {
|
||||
const properties: Array<PreviewProperty> = [
|
||||
{
|
||||
key: "likeCount",
|
||||
title: formatNumber(info.details.likeCount),
|
||||
icon: <ThumbUpIcon />,
|
||||
},
|
||||
{
|
||||
key: "viewCount",
|
||||
title: formatNumber(info.details.viewCount),
|
||||
icon: <EyeIcon />,
|
||||
},
|
||||
{
|
||||
key: "date",
|
||||
title: formatDate(info.details.publishedAt),
|
||||
icon: <CalendarIcon />,
|
||||
},
|
||||
];
|
||||
return <PreviewProperties properties={properties} />;
|
||||
}
|
||||
case "twitter": {
|
||||
const properties: Array<PreviewProperty> = [
|
||||
{
|
||||
key: "likeCount",
|
||||
title: formatNumber(info.details.likesCount),
|
||||
icon: <ThumbUpIcon />,
|
||||
},
|
||||
{
|
||||
key: "retweetCount",
|
||||
title: formatNumber(info.details.retweetCount),
|
||||
icon: <RetweetIcon />,
|
||||
},
|
||||
{
|
||||
key: "date",
|
||||
title: formatDate(info.details.publishedAt),
|
||||
icon: <CalendarIcon />,
|
||||
},
|
||||
];
|
||||
return <PreviewProperties properties={properties} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
return (
|
||||
<PreviewBox link={info.url}>
|
||||
<div>
|
||||
{info.title && (
|
||||
<Title>
|
||||
{info.icon && (
|
||||
<img src={info.icon.url} className="w-4 h-4 inline mr-1" />
|
||||
)}
|
||||
<span className="inline">{info.title}</span>
|
||||
</Title>
|
||||
)}
|
||||
{info.description && <Body>{info.description}</Body>}
|
||||
</div>
|
||||
{info.image && (
|
||||
<div>
|
||||
<img className="block" src={info.image?.url} />
|
||||
</div>
|
||||
)}
|
||||
{details()}
|
||||
</PreviewBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { formatBytes } from "~/utilities/formatter";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
import { PreviewProperties, PreviewProperty } from "../PreviewProperties";
|
||||
import { PreviewImage } from "./preview.types";
|
||||
|
||||
export type PreviewImageProps = {
|
||||
info: PreviewImage;
|
||||
};
|
||||
|
||||
export function PreviewImage({ info }: PreviewImageProps) {
|
||||
let properties: Array<PreviewProperty> = [];
|
||||
|
||||
if (info.mimeType) {
|
||||
properties.push({ key: "mimeType", title: info.mimeType });
|
||||
}
|
||||
|
||||
if (info.size) {
|
||||
properties.push({ key: "fileSize", title: `${formatBytes(info.size)}` });
|
||||
}
|
||||
|
||||
const src = info.image ? info.image.url : info.url;
|
||||
|
||||
return (
|
||||
<PreviewBox link={info.url}>
|
||||
<img className="block max-h-96 w-full object-contain" src={src} />
|
||||
<PreviewProperties properties={properties} />
|
||||
</PreviewBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Body } from "~/components/Primitives/Body";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
|
||||
export function PreviewImageUri({
|
||||
src,
|
||||
contentType,
|
||||
}: {
|
||||
src: string;
|
||||
contentType: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<PreviewBox>
|
||||
<Body>
|
||||
<img src={src} />
|
||||
</Body>
|
||||
</PreviewBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from "react";
|
||||
import { useFetcher } from "remix";
|
||||
import { CodeViewer } from "~/components/CodeViewer";
|
||||
import { Body } from "~/components/Primitives/Body";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
|
||||
export function PreviewJsonUri({ url }: { url: string }) {
|
||||
const previewFetcher = useFetcher<unknown>();
|
||||
|
||||
useEffect(() => {
|
||||
const encodedUri = encodeURIComponent(url);
|
||||
previewFetcher.load(
|
||||
`/actions/getPreview/${encodedUri}?contentType=application%2Fjson`
|
||||
);
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
{previewFetcher.type === "done" ? (
|
||||
<>
|
||||
<PreviewBox>
|
||||
<CodeViewer code={JSON.stringify(previewFetcher.data, null, 2)} />
|
||||
</PreviewBox>
|
||||
</>
|
||||
) : (
|
||||
<PreviewBox>
|
||||
<Body>Loading…</Body>
|
||||
</PreviewBox>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { JSONStringType } from "@jsonhero/json-infer-types/lib/@types";
|
||||
import {
|
||||
JSONColorFormat,
|
||||
JSONJSONFormat,
|
||||
} from "@jsonhero/json-infer-types/lib/formats";
|
||||
import Color from "color";
|
||||
import { CodeViewer } from "~/components/CodeViewer";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
import { PreviewDate } from "./PreviewDate";
|
||||
import { PreviewImageUri } from "./PreviewImageUri";
|
||||
import { PreviewJsonUri } from "./PreviewJsonUri";
|
||||
import { PreviewUri } from "./PreviewUri";
|
||||
|
||||
export function PreviewString({ info }: { info: JSONStringType }) {
|
||||
if (info.format == null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
switch (info.format.name) {
|
||||
case "uri":
|
||||
if (
|
||||
info.format.contentType === "image/png" ||
|
||||
info.format.contentType === "image/jpeg" ||
|
||||
info.format.contentType === "image/gif" ||
|
||||
info.format.contentType === "image/svg+xml" ||
|
||||
info.format.contentType === "image/webp"
|
||||
) {
|
||||
return (
|
||||
<PreviewImageUri
|
||||
src={info.value}
|
||||
contentType={info.format.contentType}
|
||||
/>
|
||||
);
|
||||
} else if (info.format.contentType === "application/json") {
|
||||
return <PreviewJsonUri url={info.value} />;
|
||||
} else {
|
||||
return <PreviewUri value={info.value} type={info} />;
|
||||
}
|
||||
case "datetime":
|
||||
if (info.format.parts === "date" || info.format.parts === "datetime") {
|
||||
return <PreviewDate value={info.value} format={info.format} />;
|
||||
}
|
||||
return <></>;
|
||||
case "color":
|
||||
return <PreviewColor value={info.value} format={info.format} />;
|
||||
case "json":
|
||||
return <PreviewJson value={info.value} format={info.format} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
function PreviewJson({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value: string;
|
||||
format: JSONJSONFormat;
|
||||
}) {
|
||||
if (format.variant === "json5") {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <CodeViewer code={JSON.stringify(JSON.parse(value), null, 2)} />;
|
||||
}
|
||||
|
||||
function PreviewColor({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value: string;
|
||||
format: JSONColorFormat;
|
||||
}) {
|
||||
const color = new Color(value);
|
||||
|
||||
const textColor = color.isLight() ? "text-slate-800" : "text-slate-100";
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewBox>
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center justify-center w-full h-52"
|
||||
style={{ backgroundColor: color.hex().toString() }}
|
||||
>
|
||||
<p className={`text-center text-xl ${textColor}`}>{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { JSONStringType } from "@jsonhero/json-infer-types/lib/@types";
|
||||
import { useEffect } from "react";
|
||||
import { useFetcher } from "remix";
|
||||
import { Body } from "~/components/Primitives/Body";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
import { PreviewResult } from "./preview.types";
|
||||
import { PreviewUriElement } from "./PreviewUriElement";
|
||||
|
||||
export type PreviewUriProps = {
|
||||
value: string;
|
||||
type: JSONStringType;
|
||||
};
|
||||
|
||||
export function PreviewUri(props: PreviewUriProps) {
|
||||
const previewFetcher = useFetcher<PreviewResult>();
|
||||
|
||||
useEffect(() => {
|
||||
const encodedUri = encodeURIComponent(props.value);
|
||||
previewFetcher.load(`/actions/getPreview/${encodedUri}`);
|
||||
}, [props.value]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{previewFetcher.type === "done" ? (
|
||||
<>
|
||||
{"error" in previewFetcher.data ? (
|
||||
<PreviewBox>
|
||||
<Body>{previewFetcher.data.error}</Body>
|
||||
</PreviewBox>
|
||||
) : (
|
||||
<PreviewUriElement info={previewFetcher.data} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<PreviewBox>
|
||||
<Body>Loading…</Body>
|
||||
</PreviewBox>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PreviewInfo } from "./preview.types";
|
||||
import { PreviewHtml } from "./PreviewHtml";
|
||||
import { PreviewImage } from "./PreviewImage";
|
||||
|
||||
export type PreviewUriElementProps = {
|
||||
info: PreviewInfo;
|
||||
};
|
||||
|
||||
export function PreviewUriElement({ info }: PreviewUriElementProps) {
|
||||
switch (info.contentType) {
|
||||
case "html":
|
||||
return <PreviewHtml info={info} />;
|
||||
case "image":
|
||||
case "gif":
|
||||
return <PreviewImage info={info} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export function RetweetIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<g>
|
||||
<path d="M23.77 15.67c-.292-.293-.767-.293-1.06 0l-2.22 2.22V7.65c0-2.068-1.683-3.75-3.75-3.75h-5.85c-.414 0-.75.336-.75.75s.336.75.75.75h5.85c1.24 0 2.25 1.01 2.25 2.25v10.24l-2.22-2.22c-.293-.293-.768-.293-1.06 0s-.294.768 0 1.06l3.5 3.5c.145.147.337.22.53.22s.383-.072.53-.22l3.5-3.5c.294-.292.294-.767 0-1.06zm-10.66 3.28H7.26c-1.24 0-2.25-1.01-2.25-2.25V6.46l2.22 2.22c.148.147.34.22.532.22s.384-.073.53-.22c.293-.293.293-.768 0-1.06l-3.5-3.5c-.293-.294-.768-.294-1.06 0l-3.5 3.5c-.294.292-.294.767 0 1.06s.767.293 1.06 0l2.22-2.22V16.7c0 2.068 1.683 3.75 3.75 3.75h5.85c.414 0 .75-.336.75-.75s-.337-.75-.75-.75z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
export declare type PreviewImage = {
|
||||
url: string;
|
||||
contentType: "image" | "gif";
|
||||
mimeType: string;
|
||||
size?: number;
|
||||
image?: ImageAssetDetails;
|
||||
};
|
||||
|
||||
export declare type PreviewVideo = {
|
||||
url: string;
|
||||
contentType: "video";
|
||||
mimeType: string;
|
||||
size?: number;
|
||||
image?: ImageAssetDetails;
|
||||
};
|
||||
|
||||
export declare type PreviewHtml = {
|
||||
url: string;
|
||||
contentType: "html";
|
||||
mimeType: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
name?: string;
|
||||
icon?: ImageAssetDetails;
|
||||
image?: ImageAssetDetails;
|
||||
details?: YouTubeLinkDetails | TwitterLinkDetails;
|
||||
};
|
||||
|
||||
export declare type PreviewInfo = PreviewImage | PreviewHtml;
|
||||
export type PreviewResult = PreviewInfo | { error: string };
|
||||
|
||||
declare type YouTubeLinkDetails = {
|
||||
type: "youtube";
|
||||
videoId: string;
|
||||
duration: string;
|
||||
viewCount: number;
|
||||
likeCount: number;
|
||||
dislikeCount: number;
|
||||
commentCount: number;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
declare type TwitterLinkDetails = {
|
||||
type: "twitter";
|
||||
statusId: string;
|
||||
retweetCount: number;
|
||||
likesCount: number;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
declare type ImageAssetDetails = {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const Body: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return <p className={`font-sans text-base ${className}`}>{children}</p>;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const BodyBold: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<p className={`font-sans text-base font-bold ${className}`}>{children}</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const ExtraLargeTitle: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<h1 className={`font-sans font-bold text-6xl ${className}`}>{children}</h1>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const LargeMono: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return <p className={`font-mono text-md ${className}`}>{children}</p>;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const LargeTitle: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<h1 className={`font-sans font-bold text-2xl ${className}`}>{children}</h1>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const Mono: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return <p className={`font-mono text-sm ${className}`}>{children}</p>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const SmallBody: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return <p className={`font-sans text-sm ${className}`}>{children}</p>;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const SmallSubtitle: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<h3 className={`font-sans text-xl text-slate-300 ${className}`}>{children}</h3>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const SmallTitle: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<h3 className={`font-sans font-bold text-lg ${className}`}>{children}</h3>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export const Title: FunctionComponent<{ className?: string }> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<h2 className={`font-sans font-bold text-xl ${className}`}>{children}</h2>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { JSONFloatType, JSONIntType } from "@jsonhero/json-infer-types";
|
||||
import { formatValue } from "~/utilities/formatter";
|
||||
import { DataTable } from "../DataTable";
|
||||
import { ValueIcon } from "../ValueIcon";
|
||||
|
||||
export type PropertiesNumberProps = {
|
||||
type: JSONIntType | JSONFloatType;
|
||||
};
|
||||
|
||||
export function PropertiesNumber(info: PropertiesNumberProps) {
|
||||
return (
|
||||
<DataTable
|
||||
rows={[
|
||||
{
|
||||
key: "Formatted value",
|
||||
value: formatValue(info.type) ?? "",
|
||||
icon: <ValueIcon type={info.type} />,
|
||||
},
|
||||
{
|
||||
key: "Type",
|
||||
value: info.type.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { JSONStringType, JSONURIFormat } from "@jsonhero/json-infer-types";
|
||||
import {
|
||||
JSONColorFormat,
|
||||
JSONDateTimeFormat,
|
||||
JSONJWTStringFormat,
|
||||
JSONTimestampFormat,
|
||||
} from "@jsonhero/json-infer-types/lib/formats";
|
||||
import Color from "color";
|
||||
import { DataTableRow, DataTable } from "../DataTable";
|
||||
|
||||
export type PropertiesStringProps = {
|
||||
type: JSONStringType;
|
||||
};
|
||||
|
||||
export function PropertiesString({ type }: { type: JSONStringType }) {
|
||||
if (type.format == null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
switch (type.format.name) {
|
||||
case "uri":
|
||||
return <PropertiesUri value={type.value} format={type.format} />;
|
||||
case "color":
|
||||
return <PropertiesColor value={type.value} format={type.format} />;
|
||||
case "datetime":
|
||||
return <PropertiesDateTime value={type.value} format={type.format} />;
|
||||
case "timestamp":
|
||||
return <PropertiesTimestamp value={type.value} format={type.format} />;
|
||||
case "jwt":
|
||||
return <PropertiesJwt value={type.value} format={type.format} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { inferTemporal } from "~/utilities/inferredTemporal";
|
||||
|
||||
function PropertiesJwt({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value: string;
|
||||
format: JSONJWTStringFormat;
|
||||
}) {
|
||||
const properties: DataTableRow[] = [];
|
||||
|
||||
const decodedPayload = jwtDecode(value) as Record<string, any>;
|
||||
|
||||
for (const [key, value] of Object.entries(decodedPayload)) {
|
||||
properties.push({
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
const decodedHeader = jwtDecode(value, { header: true }) as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
for (const [key, value] of Object.entries(decodedHeader)) {
|
||||
properties.push({
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
return <DataTable rows={properties} />;
|
||||
}
|
||||
|
||||
function PropertiesTimestamp({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value: string;
|
||||
format: JSONTimestampFormat;
|
||||
}) {
|
||||
const date =
|
||||
format.variant === "millisecondsSinceEpoch"
|
||||
? new Date(parseInt(value))
|
||||
: format.variant === "secondsSinceEpoch"
|
||||
? new Date(parseInt(value) * 1000)
|
||||
: new Date(parseInt(value) / 1000000);
|
||||
|
||||
const properties = [
|
||||
{
|
||||
key: "rfc3339",
|
||||
value: date.toISOString(),
|
||||
},
|
||||
{
|
||||
key: "rfc2822",
|
||||
value: date.toUTCString(),
|
||||
},
|
||||
{
|
||||
key: "unix",
|
||||
value: (date.getTime() / 1000).toFixed(0),
|
||||
},
|
||||
{
|
||||
key: "unix ms",
|
||||
value: date.getTime().toString(),
|
||||
},
|
||||
{
|
||||
key: "date",
|
||||
value: date.toDateString(),
|
||||
},
|
||||
{
|
||||
key: "time",
|
||||
value: date.toTimeString(),
|
||||
},
|
||||
];
|
||||
|
||||
return <DataTable rows={properties} />;
|
||||
}
|
||||
|
||||
function PropertiesDateTime({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value: string;
|
||||
format: JSONDateTimeFormat;
|
||||
}) {
|
||||
if (format.parts === "time") {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const temporal = inferTemporal(value, format);
|
||||
|
||||
if (!temporal) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const properties = [
|
||||
{
|
||||
key: "rfc3339",
|
||||
value: temporal.toString(),
|
||||
},
|
||||
// {
|
||||
// key: "unix",
|
||||
// value: (date.getTime() / 1000).toFixed(0),
|
||||
// },
|
||||
// {
|
||||
// key: "unix ms",
|
||||
// value: date.getTime().toString(),
|
||||
// },
|
||||
// {
|
||||
// key: "date",
|
||||
// value: date.toDateString(),
|
||||
// },
|
||||
// {
|
||||
// key: "time",
|
||||
// value: date.toTimeString(),
|
||||
// },
|
||||
];
|
||||
|
||||
if ("epochSeconds" in temporal) {
|
||||
properties.push({
|
||||
key: "unix",
|
||||
value: temporal.epochSeconds.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if ("epochMilliseconds" in temporal) {
|
||||
properties.push({
|
||||
key: "unix ms",
|
||||
value: temporal.epochMilliseconds.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
properties.push({
|
||||
key: "date",
|
||||
value: temporal.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
});
|
||||
|
||||
return <DataTable rows={properties} />;
|
||||
}
|
||||
|
||||
function PropertiesColor({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value: string;
|
||||
format: JSONColorFormat;
|
||||
}) {
|
||||
const color = new Color(value);
|
||||
|
||||
const properties = [
|
||||
{
|
||||
key: "hex",
|
||||
value: color.hex(),
|
||||
},
|
||||
{
|
||||
key: "rgb",
|
||||
value: color.rgb().string(),
|
||||
},
|
||||
{
|
||||
key: "hsl",
|
||||
value: color.hsl().string(),
|
||||
},
|
||||
{
|
||||
key: "luminosity",
|
||||
value: color.luminosity().toFixed(4),
|
||||
},
|
||||
{
|
||||
key: "contrastRatio",
|
||||
value: color.isLight() ? "light" : "dark",
|
||||
},
|
||||
];
|
||||
|
||||
return <DataTable rows={properties} />;
|
||||
}
|
||||
|
||||
function PropertiesUri({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value: string;
|
||||
format: JSONURIFormat;
|
||||
}) {
|
||||
let uri = new URL(value);
|
||||
|
||||
let standardProperties: DataTableRow[] = [
|
||||
{
|
||||
key: "href",
|
||||
value: uri.href,
|
||||
},
|
||||
{
|
||||
key: "origin",
|
||||
value: uri.origin,
|
||||
},
|
||||
{
|
||||
key: "protocol",
|
||||
value: uri.protocol,
|
||||
},
|
||||
{
|
||||
key: "hostname",
|
||||
value: uri.hostname,
|
||||
},
|
||||
{
|
||||
key: "pathname",
|
||||
value: uri.pathname,
|
||||
},
|
||||
];
|
||||
|
||||
if (uri.search) {
|
||||
standardProperties.push({
|
||||
key: "search",
|
||||
value: uri.search,
|
||||
});
|
||||
}
|
||||
|
||||
if (uri.hash) {
|
||||
standardProperties.push({
|
||||
key: "hash",
|
||||
value: uri.hash,
|
||||
});
|
||||
}
|
||||
|
||||
if (format.contentType != null) {
|
||||
standardProperties.push({
|
||||
key: "mimeType",
|
||||
value: format.contentType,
|
||||
});
|
||||
}
|
||||
|
||||
return <DataTable rows={standardProperties} />;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useSelectedInfo } from "~/hooks/useSelectedInfo";
|
||||
import { PropertiesNumber } from "./PropertiesNumber";
|
||||
import { PropertiesString } from "./PropertiesString";
|
||||
|
||||
export function PropertiesValue() {
|
||||
const info = useSelectedInfo();
|
||||
|
||||
if (!info) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
switch (info.name) {
|
||||
case "float":
|
||||
case "int":
|
||||
return <PropertiesNumber type={info} />;
|
||||
case "string":
|
||||
return <PropertiesString type={info} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Title } from "~/components/Primitives/Title";
|
||||
import { useJson } from "../hooks/useJson";
|
||||
import { Mono } from "./Primitives/Mono";
|
||||
import { SmallBody } from "./Primitives/SmallBody";
|
||||
import { generateNodesToPath } from "~/utilities/jsonColumnView";
|
||||
import { useJsonColumnViewState } from "../hooks/useJsonColumnView";
|
||||
import {
|
||||
RelatedValuesGroup,
|
||||
calculateRelatedValuesGroups,
|
||||
} from "~/utilities/relatedValues";
|
||||
import { PathPreview } from "./PathPreview";
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
|
||||
export function RelatedValues() {
|
||||
const [json] = useJson();
|
||||
const { selectedNodeId } = useJsonColumnViewState();
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
|
||||
const relatedValuesGroups = useMemo<Array<RelatedValuesGroup>>(() => {
|
||||
if (!selectedNodeId) {
|
||||
return [];
|
||||
}
|
||||
return calculateRelatedValuesGroups(selectedNodeId, json);
|
||||
}, [json, selectedNodeId]);
|
||||
|
||||
const toggleOpen = (id: string) => {
|
||||
if (openId === id) {
|
||||
setOpenId(null);
|
||||
} else {
|
||||
setOpenId(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{relatedValuesGroups.length > 0 && (
|
||||
<div className="my-4">
|
||||
<Title className="mb-2 text-slate-700 transition dark:text-slate-400">
|
||||
Related values
|
||||
</Title>
|
||||
{relatedValuesGroups.map((relatedValuesGroup, i) => {
|
||||
return (
|
||||
<RelatedValuesGroupItem
|
||||
group={relatedValuesGroup}
|
||||
key={relatedValuesGroup.value}
|
||||
isOpen={relatedValuesGroup.value === openId}
|
||||
toggleOpen={() => toggleOpen(relatedValuesGroup.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RelatedValuesGroupItem({
|
||||
group,
|
||||
isOpen,
|
||||
toggleOpen,
|
||||
}: {
|
||||
group: RelatedValuesGroup;
|
||||
isOpen: boolean;
|
||||
toggleOpen: () => void;
|
||||
}) {
|
||||
const isLinkable = group.value !== "undefined";
|
||||
const isHighlighted = group.value === "undefined" || group.value === "null";
|
||||
|
||||
return (
|
||||
<div className="mb-1 transition dark:text-slate-300">
|
||||
<div
|
||||
className={`flex rounded-sm transition hover:cursor-pointer ${
|
||||
isOpen
|
||||
? "bg-slate-200 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-700"
|
||||
: "bg-slate-100 hover:bg-slate-200 dark:bg-slate-600 dark:hover:bg-slate-700"
|
||||
}`}
|
||||
onClick={() => toggleOpen()}
|
||||
>
|
||||
<div className="flex items-center rounded-sm px-1 bg-slate-200 dark:bg-slate-700">
|
||||
{isOpen ? (
|
||||
<ChevronDownIcon className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronRightIcon className="w-3 h-3" />
|
||||
)}
|
||||
<SmallBody className="ml-1">{group.paths.length}</SmallBody>
|
||||
</div>
|
||||
<Mono
|
||||
className={`truncate px-2 text-slate-700 dark:text-slate-200 ${
|
||||
isHighlighted ? "italic" : ""
|
||||
}`}
|
||||
>
|
||||
{group.value}
|
||||
</Mono>
|
||||
</div>
|
||||
{isOpen &&
|
||||
group.paths.map((path) => {
|
||||
return (
|
||||
<div
|
||||
className="p-0.5 bg-slate-100 dark:bg-slate-700 dark:bg-opacity-60"
|
||||
key={path}
|
||||
>
|
||||
<PathLink path={path} enabled={isLinkable} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PathLink({ path, enabled }: { path: string; enabled: boolean }) {
|
||||
const [json] = useJson();
|
||||
|
||||
const selectedNodes = useMemo(() => {
|
||||
return generateNodesToPath(json, path);
|
||||
}, [json, path]);
|
||||
|
||||
return (
|
||||
<PathPreview nodes={selectedNodes} maxComponents={4} enabled={enabled} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
type ResizableProps = {
|
||||
children: React.ReactNode;
|
||||
isHorizontal: boolean;
|
||||
initialSize: number;
|
||||
minimumSize: number;
|
||||
maximumSize: number;
|
||||
};
|
||||
|
||||
export default function Resizable({
|
||||
children,
|
||||
isHorizontal = true,
|
||||
initialSize,
|
||||
minimumSize,
|
||||
maximumSize,
|
||||
}: ResizableProps) {
|
||||
const [dimension, setDimension] = useState(initialSize);
|
||||
const [previousDragPosition, setPreviousDragPosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleDragStart = (e: React.MouseEvent<HTMLDivElement>): void => {
|
||||
setPreviousDragPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDrag = (e: MouseEvent) => {
|
||||
if (previousDragPosition === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
let offset = 0;
|
||||
if (isHorizontal) {
|
||||
offset = e.clientX - previousDragPosition.x;
|
||||
} else {
|
||||
offset = e.clientY - previousDragPosition.y;
|
||||
}
|
||||
let newValue = dimension - offset;
|
||||
if (minimumSize != null) {
|
||||
newValue = Math.max(minimumSize, newValue);
|
||||
}
|
||||
if (maximumSize != null) {
|
||||
newValue = Math.min(maximumSize, newValue);
|
||||
}
|
||||
setDimension(newValue);
|
||||
setPreviousDragPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setPreviousDragPosition(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("mousemove", handleDrag);
|
||||
window.addEventListener("mouseup", handleDragEnd);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleDrag);
|
||||
window.removeEventListener("mouseup", handleDragEnd);
|
||||
};
|
||||
}, [handleDrag, handleDragEnd]);
|
||||
|
||||
const style = () => {
|
||||
let formatted = dimension + "px";
|
||||
|
||||
if (isHorizontal) {
|
||||
return {
|
||||
width: formatted,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
height: formatted,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const classes = () => {
|
||||
if (isHorizontal) {
|
||||
return "flex flex-none relative";
|
||||
} else {
|
||||
return "flex flex-none relative";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style()} className={classes()}>
|
||||
<div className={"flex-grow"} style={{ width: "inherit" }}>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
isHorizontal
|
||||
? "w-1 h-full absolute my-0 -ml-[1px] transition-all cursor-col-resize hover:bg-indigo-700 hover:opacity-100"
|
||||
: "h-1 w-full transition-all cursor-row-resize hover:bg-indigo-700 hover:opacity-100"
|
||||
}
|
||||
onMouseDown={handleDragStart}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ExampleUrl } from "./ExampleUrl";
|
||||
|
||||
export function SampleUrls() {
|
||||
return (
|
||||
<div className="flex justify-start flex-wrap md:gap-x-3">
|
||||
<ExampleUrl
|
||||
url="https://gist.githubusercontent.com/ericallam/332aca65c34dbc0fdb1b3cffbdf3f7a4/raw/4c6e264c3afd3432b608bad628290a7d71035253/unsplash.json"
|
||||
title="Unsplash API Example"
|
||||
displayTitle="Unsplash API"
|
||||
/>
|
||||
|
||||
<ExampleUrl
|
||||
url="https://gist.githubusercontent.com/ericallam/77e37e93e370b32387ce8d598dd06fc8/raw/9306a2c039fc27ab42bdadb80cbd6dbca49d27ec/tweet.json"
|
||||
title="Tweet API Example"
|
||||
displayTitle="Tweet JSON"
|
||||
/>
|
||||
|
||||
<ExampleUrl
|
||||
url="https://gist.githubusercontent.com/ericallam/f11f4981adf72b0427427c349afb3a09/raw/37eee4234b628f3278c9d4f644fee4a1c4987593/Airtable.json"
|
||||
title="Airtable Product Catalog"
|
||||
displayTitle="Airtable API"
|
||||
/>
|
||||
|
||||
<ExampleUrl
|
||||
url="https://gist.githubusercontent.com/ericallam/51b77c0051c21a34ea33d601bb0b1bef/raw/0b099f7d3bb5fe47f1fd55007d6a5fe9c5fd046b/GitHub.json"
|
||||
title="List of Github Repos"
|
||||
displayTitle="Github API"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function ScrollingColumnView({
|
||||
children,
|
||||
selectedPath,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
selectedPath: string[];
|
||||
}) {
|
||||
const columnsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
invariant(columnsRef.current, "columnsRef.current is null");
|
||||
|
||||
//get the selected column
|
||||
const scrollToColumnIndex = Math.max(0, selectedPath.length - 1);
|
||||
const scrollNode = columnsRef.current.children[scrollToColumnIndex];
|
||||
if (scrollNode == null) return;
|
||||
const scrollHTMLElement = scrollNode as HTMLElement;
|
||||
|
||||
//try center the selected column in the viewport
|
||||
const columnCenter =
|
||||
scrollHTMLElement.offsetLeft - scrollHTMLElement.clientWidth / 2;
|
||||
const containerWidth = columnsRef.current.clientWidth;
|
||||
const scrollPosition = Math.max(0, columnCenter - containerWidth / 2);
|
||||
|
||||
columnsRef.current.scrollLeft = scrollPosition;
|
||||
}, [selectedPath, columnsRef, children]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="columns flex flex-grow overflow-x-auto no-scrollbar focus:outline-none"
|
||||
ref={columnsRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { ClipboardIcon } from "@heroicons/react/outline";
|
||||
import { useSelectedInfo } from "~/hooks/useSelectedInfo";
|
||||
import { useJsonColumnViewState } from "~/hooks/useJsonColumnView";
|
||||
|
||||
const buttonDefault = (
|
||||
<>
|
||||
<ClipboardIcon className="h-4 w-4 mr-[2px]" />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
);
|
||||
|
||||
export function Share() {
|
||||
useEffect(() => {
|
||||
setLink(window.location.href);
|
||||
}, []);
|
||||
const [link, setLink] = useState("");
|
||||
|
||||
const [copyText, setCopyText] = useState<React.ReactNode>(buttonDefault);
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
const timeout = setTimeout(() => {
|
||||
setCopyText(buttonDefault);
|
||||
setCopied(false);
|
||||
}, 1800);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [copied]);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(link).then(
|
||||
function () {
|
||||
setCopyText(<span>Copied!</span>);
|
||||
setCopied(true);
|
||||
},
|
||||
function (err) {
|
||||
setCopyText(<span>Failed to copy</span>);
|
||||
setCopied(true);
|
||||
}
|
||||
);
|
||||
}, [link, setCopyText]);
|
||||
|
||||
const { selectedNodeId } = useJsonColumnViewState();
|
||||
|
||||
const handleIncludesPath = useCallback(
|
||||
(includesPath: boolean) => {
|
||||
if (!selectedNodeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (includesPath) {
|
||||
const url = new URL(window.location.href);
|
||||
for (const [key] of url.searchParams) {
|
||||
url.searchParams.delete(key);
|
||||
}
|
||||
|
||||
url.searchParams.append("path", selectedNodeId);
|
||||
|
||||
setLink(url.href);
|
||||
} else {
|
||||
setLink(window.location.href);
|
||||
}
|
||||
},
|
||||
[link, selectedNodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-indigo-700 text-white rounded-sm shadow-md w-[340px] p-3 transition">
|
||||
<Body className="text-sm mb-2 text-slate-500 transition dark:text-slate-300">
|
||||
Anyone with this link can view this json file.
|
||||
</Body>
|
||||
<div className="flex">
|
||||
<div className="flex-grow whitespace-nowrap overflow-hidden rounded-l-sm bg-indigo-900 text-sm p-2 select-all">
|
||||
{link}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center text-lg text-slate-800 min-w-[74px] bg-white bg-opacity-80 rounded-r-sm transition hover:bg-opacity-100 cursor-pointer"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copyText}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-check form-check-inline mt-2">
|
||||
<label className="flex items-center text-sm form-check-label text-slate-800 select-none hover:cursor-pointer transition dark:text-white">
|
||||
<input
|
||||
className="form-check-input appearance-none h-4 w-4 border border-slate-300 rounded-sm bg-white checked:bg-indigo-700 checked:border-indigo-700 focus:outline-none duration-200 align-top bg-no-repeat bg-center bg-contain float-left mr-2 hover:cursor-pointer transition dark:border-slate-300 dark:bg-slate-200 dark:checked:bg-lime-500 dark:checked:border-lime-500"
|
||||
type="checkbox"
|
||||
id="inlineCheckbox"
|
||||
value="option"
|
||||
onChange={(e) => handleIncludesPath(e.target.checked)}
|
||||
></input>
|
||||
Link includes path
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { TemplateIcon, CodeIcon, TerminalIcon } from "@heroicons/react/outline";
|
||||
import { TreeIcon } from "~/components/Icons/TreeIcon";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Link, useLocation, useNavigate } from "remix";
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
import { ToolTip } from "./ToolTip";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { ShortcutIcon } from "./Icons/ShortcutIcon";
|
||||
|
||||
export function SideBar() {
|
||||
const { doc } = useJsonDoc();
|
||||
|
||||
return (
|
||||
<div className="side-bar flex flex-col align-center justify-between h-full p-1 bg-slate-200 transition dark:bg-slate-800">
|
||||
<ol className="relative">
|
||||
<SidebarLink to={`/j/${doc.id}`} hotKey="cmd+1">
|
||||
<ToolTip arrow="left">
|
||||
<Body>Column view</Body>
|
||||
<ShortcutIcon>⌘</ShortcutIcon>
|
||||
<ShortcutIcon>1</ShortcutIcon>
|
||||
</ToolTip>
|
||||
<TemplateIcon className="p-2 w-full h-full" />
|
||||
</SidebarLink>
|
||||
<SidebarLink to={`/j/${doc.id}/editor`} hotKey="cmd+2">
|
||||
<ToolTip arrow="left">
|
||||
<Body>JSON view</Body>
|
||||
<ShortcutIcon>⌘</ShortcutIcon>
|
||||
<ShortcutIcon>2</ShortcutIcon>
|
||||
</ToolTip>
|
||||
<CodeIcon className="p-2 w-full h-full" />
|
||||
</SidebarLink>
|
||||
<SidebarLink to={`/j/${doc.id}/tree`} hotKey="cmd+3">
|
||||
<ToolTip arrow="left">
|
||||
<Body>Tree view</Body>
|
||||
<ShortcutIcon>⌘</ShortcutIcon>
|
||||
<ShortcutIcon>3</ShortcutIcon>
|
||||
</ToolTip>
|
||||
<TreeIcon className="p-2 w-full h-full" />
|
||||
</SidebarLink>
|
||||
</ol>
|
||||
<ol>
|
||||
<SidebarLink to={`/j/${doc.id}/terminal`} hotKey="cmd+4">
|
||||
<ToolTip arrow="left">
|
||||
<Body>Terminal</Body>
|
||||
<ShortcutIcon>⌘</ShortcutIcon>
|
||||
<ShortcutIcon>4</ShortcutIcon>
|
||||
</ToolTip>
|
||||
<TerminalIcon className="p-2 w-full h-full" />
|
||||
</SidebarLink>
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarLink({
|
||||
children,
|
||||
to,
|
||||
hotKey,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
to?: string;
|
||||
hotKey?: string;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
|
||||
const isActive = location.pathname === to;
|
||||
|
||||
if (hotKey) {
|
||||
const navigate = useNavigate();
|
||||
useHotkeys(
|
||||
hotKey,
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
if (!isActive && to) {
|
||||
navigate(to);
|
||||
}
|
||||
},
|
||||
[navigate, isActive, to]
|
||||
);
|
||||
}
|
||||
|
||||
const classes = isActive
|
||||
? "relative w-10 h-10 mb-1 text-white bg-indigo-700 rounded-sm cursor:pointer transition"
|
||||
: "relative w-10 h-10 mb-1 text-slate-700 hover:bg-slate-300 rounded-sm cursor:pointer transition dark:text-white dark:hover:bg-slate-700";
|
||||
|
||||
return !!to ? (
|
||||
<Link to={to} prefetch={isActive ? "none" : "render"}>
|
||||
<li className={classes}>{children}</li>
|
||||
</Link>
|
||||
) : (
|
||||
<li className={classes}>{children}</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { MoonIcon } from "./Icons/MoonIcon";
|
||||
import { SunIcon } from "./Icons/SunIcon";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
|
||||
export function ThemeModeToggler() {
|
||||
const [theme, setTheme] = useTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
|
||||
};
|
||||
const SwitchIcon = theme === "light" ? MoonIcon : SunIcon;
|
||||
|
||||
useHotkeys("alt+t", () => toggleTheme(), [toggleTheme]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`flex text-xl items-center pr-2 ${
|
||||
theme === "light" ? "text-slate-800" : "text-white"
|
||||
}`}
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<SwitchIcon />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import type { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { useFetcher } from "remix";
|
||||
|
||||
export type Theme = "dark" | "light";
|
||||
|
||||
type ThemeContextType = [
|
||||
Theme | undefined,
|
||||
Dispatch<SetStateAction<Theme | undefined>>
|
||||
];
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
const prefersLightMQ = "(prefers-color-scheme: light)";
|
||||
const getPreferredTheme = () =>
|
||||
window.matchMedia(prefersLightMQ).matches ? "light" : "dark";
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
specifiedTheme,
|
||||
themeOverride,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
specifiedTheme?: Theme;
|
||||
themeOverride?: Theme;
|
||||
}) {
|
||||
const [theme, setTheme] = useState<Theme | undefined>(() => {
|
||||
if (specifiedTheme) {
|
||||
if (specifiedTheme === "light" || specifiedTheme === "dark") {
|
||||
return specifiedTheme;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// there's no way for us to know what the theme should be in this context
|
||||
// the client will have to figure it out before hydration.
|
||||
if (typeof window !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
return getPreferredTheme();
|
||||
});
|
||||
|
||||
const mountRun = useRef(false);
|
||||
const persistTheme = useFetcher();
|
||||
|
||||
useEffect(() => {
|
||||
if (!mountRun.current) {
|
||||
mountRun.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistTheme.submit(
|
||||
{ theme },
|
||||
{ action: "actions/setTheme", method: "post" }
|
||||
);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={[themeOverride ?? theme, setTheme]}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme(): ThemeContextType {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
const clientThemeCode = `
|
||||
;(() => {
|
||||
const theme = window.matchMedia(${JSON.stringify(prefersLightMQ)}).matches
|
||||
? 'light'
|
||||
: 'dark';
|
||||
const cl = document.documentElement.classList;
|
||||
const themeAlreadyApplied = cl.contains('light') || cl.contains('dark');
|
||||
if (themeAlreadyApplied) {
|
||||
// this script shouldn't exist if the theme is already applied!
|
||||
console.warn(
|
||||
"Hi there, could you let us know you're seeing this message? Thanks!",
|
||||
);
|
||||
} else {
|
||||
cl.add(theme);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
export function NonFlashOfWrongThemeEls({ ssrTheme }: { ssrTheme: boolean }) {
|
||||
return (
|
||||
<>
|
||||
{ssrTheme ? null : (
|
||||
<script dangerouslySetInnerHTML={{ __html: clientThemeCode }} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function isTheme(value: unknown): value is Theme {
|
||||
return typeof value === "string" && ["light", "dark"].includes(value);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export type ToolTipProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
arrow?: ArrowDirection;
|
||||
};
|
||||
|
||||
export type ArrowDirection = "top" | "bottom" | "left" | "right";
|
||||
|
||||
export function ToolTip({ children, className, arrow }: ToolTipProps) {
|
||||
const [isShown, setIsShown] = useState(false);
|
||||
const arrowStyle = () => {
|
||||
if (!arrow) {
|
||||
return "";
|
||||
}
|
||||
switch (arrow) {
|
||||
case "top":
|
||||
return "top-[40px] after:bg-white after:border-[1px] after:border-t-slate-300 after:border-r-transparent after:border-b-transparent after:border-l-slate-300 after:dark:border-t-slate-600 after:dark:border-r-transprent after:dark:border-b-transparent after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:top-[-8px] after:left-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45";
|
||||
case "bottom":
|
||||
return "bottom-[49px] after:bg-white after:border-[1px] after:border-t-transparent after:border-r-transparent after:border-b-slate-300 after:border-l-slate-300 after:dark:border-t-transprent after:dark:border-r-transprent after:dark:border-b-slate-600 after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:left-[-8px] after:top-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45";
|
||||
case "left":
|
||||
return "left-[49px] after:bg-white after:border-[1px] after:border-t-transparent after:border-r-transparent after:border-b-slate-300 after:border-l-slate-300 after:dark:border-t-transprent after:dark:border-r-transprent after:dark:border-b-slate-600 after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:left-[-8px] after:top-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45";
|
||||
case "right":
|
||||
return "right-[49px] after:bg-white after:border-[1px] after:border-t-transparent after:border-r-transparent after:border-b-slate-300 after:border-l-slate-300 after:dark:border-t-transprent after:dark:border-r-transprent after:dark:border-b-slate-600 after:dark:border-l-slate-600 after:dark:bg-slate-700 after:h-[14px] after:w-[14px] after:left-[-8px] after:top-[calc(50%-7px)] after:content-[''] after:absolute after:bg-white after:rotate-45";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{}}
|
||||
initial={{ scale: 0.97, opacity: 0.5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1, opacity: 1 }}
|
||||
whileTap={{
|
||||
scale: 1.05,
|
||||
}}
|
||||
onMouseOver={() => setIsShown(true)}
|
||||
onMouseOut={() => setIsShown(false)}
|
||||
className={`${className} absolute flex justify-center top-0 text-center z-10 h-full w-full text-slate-800 transtition dark:text-slate-200`}
|
||||
>
|
||||
<div
|
||||
className={`absolute flex items-center ${
|
||||
isShown
|
||||
? `${arrowStyle()} pl-3 pr-2 py-2 w-max shadow rounded-sm border-slate-300 border-[1px] bg-white dark:bg-slate-700 dark:border-slate-600`
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{isShown && children}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import type { ComponentPropsWithoutRef } from "@radix-ui/react-primitive";
|
||||
import React from "react";
|
||||
|
||||
export const Popover = PopoverPrimitive.Root;
|
||||
|
||||
export const PopoverTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Trigger>,
|
||||
ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>
|
||||
>((props, ref) => {
|
||||
return <PopoverPrimitive.Trigger asChild ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<PopoverPrimitive.Content {...props} ref={ref}>
|
||||
{children}
|
||||
</PopoverPrimitive.Content>
|
||||
);
|
||||
});
|
||||
|
||||
export const PopoverArrow = PopoverPrimitive.Arrow;
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import type { ComponentPropsWithoutRef } from "@radix-ui/react-primitive";
|
||||
import React from "react";
|
||||
import cx from "~/utilities/classnames";
|
||||
|
||||
export type TabProps = {
|
||||
tabs: Array<{ value: string; label: string }>;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Tabs({ tabs, children }: TabProps) {
|
||||
return (
|
||||
<TabsPrimitive.Root defaultValue={tabs[0].value}>
|
||||
<TabsPrimitive.List className="">
|
||||
{tabs.map(({ value, label }) => (
|
||||
<TabsPrimitive.Trigger
|
||||
value={value}
|
||||
key={`tab-trigger-${value}`}
|
||||
className={cx(
|
||||
"group",
|
||||
"mr-1 px-4 py-1 rounded-t-sm transition",
|
||||
"text-slate-500 hover:bg-slate-100 hover:bg-opacity-50 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:bg-opacity-40",
|
||||
"radix-state-active:bg-slate-100 radix-state-active:bg-opacity-50 radix-state-active:dark:bg-slate-900 radix-state-active:dark:text-white"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</TabsPrimitive.Trigger>
|
||||
))}
|
||||
</TabsPrimitive.List>
|
||||
{children}
|
||||
</TabsPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export const TabContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>((props, ref) => {
|
||||
return <TabsPrimitive.TabsContent ref={ref} {...props} />;
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Form } from "remix";
|
||||
|
||||
export type UrlFormProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function UrlForm({ className }: UrlFormProps) {
|
||||
return (
|
||||
<Form
|
||||
method="post"
|
||||
action="/actions/createFromUrl"
|
||||
className={`${className}`}
|
||||
>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
name="jsonUrl"
|
||||
id="jsonUrl"
|
||||
className="block flex-grow text-base text-slate-200 placeholder:text-slate-300 bg-slate-800 border border-slate-600 rounded-l-sm py-2 px-3 transition duration-300 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="Got a URL to a JSON file? Paste it in here and we'll take care of the rest."
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
value="Go"
|
||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-r-sm text-white bg-lime-500 transition hover:bg-lime-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lime-500"
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { FunctionComponent } from "react";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
AtSymbolIcon,
|
||||
CalendarIcon,
|
||||
ChatAlt2Icon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
CodeIcon,
|
||||
CollectionIcon,
|
||||
ColorSwatchIcon,
|
||||
CreditCardIcon,
|
||||
CubeIcon,
|
||||
CurrencyDollarIcon,
|
||||
DocumentTextIcon,
|
||||
EmojiHappyIcon,
|
||||
EyeOffIcon,
|
||||
GlobeAltIcon,
|
||||
GlobeIcon,
|
||||
HashtagIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
PhoneIcon,
|
||||
PhotographIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { JSONValueType } from "@jsonhero/json-infer-types";
|
||||
import { colorForTypeName } from "../utilities/colors";
|
||||
import { StringIcon } from "./Icons/StringIcon";
|
||||
|
||||
type ValueIconProps = {
|
||||
type: JSONValueType;
|
||||
size?: ValueIconSize;
|
||||
monochrome?: boolean;
|
||||
};
|
||||
|
||||
export enum ValueIconSize {
|
||||
Small,
|
||||
Medium,
|
||||
}
|
||||
|
||||
export const ValueIcon: FunctionComponent<ValueIconProps> = ({
|
||||
type,
|
||||
size = ValueIconSize.Small,
|
||||
monochrome = false,
|
||||
}) => {
|
||||
let classes = monochrome ? `text-gray-600` : colorForTypeName(type.name);
|
||||
switch (size) {
|
||||
case ValueIconSize.Small:
|
||||
classes += " h-4 w-4";
|
||||
break;
|
||||
case ValueIconSize.Medium:
|
||||
classes += " h-6 w-6";
|
||||
break;
|
||||
}
|
||||
|
||||
switch (type.name) {
|
||||
case "object": {
|
||||
return <CubeIcon className={classes} />;
|
||||
}
|
||||
case "array": {
|
||||
return <CollectionIcon className={classes} />;
|
||||
}
|
||||
case "null": {
|
||||
return <EyeOffIcon className={classes} />;
|
||||
}
|
||||
case "bool": {
|
||||
return <CheckCircleIcon className={classes} />;
|
||||
}
|
||||
case "int":
|
||||
case "float": {
|
||||
return <HashtagIcon className={classes} />;
|
||||
}
|
||||
case "string": {
|
||||
if (type.format == null) {
|
||||
return <StringIcon className={classes} />;
|
||||
}
|
||||
|
||||
switch (type.format.name) {
|
||||
case "timestamp": {
|
||||
return <CalendarIcon className={classes} />;
|
||||
}
|
||||
case "datetime": {
|
||||
switch (type.format.parts) {
|
||||
case "time":
|
||||
return <ClockIcon className={classes} />;
|
||||
}
|
||||
return <CalendarIcon className={classes} />;
|
||||
}
|
||||
case "email": {
|
||||
return <AtSymbolIcon className={classes} />;
|
||||
}
|
||||
case "hostname":
|
||||
case "tld":
|
||||
case "ip": {
|
||||
return <GlobeAltIcon className={classes} />;
|
||||
}
|
||||
case "uri": {
|
||||
switch (type.format.contentType) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
case "image/gif":
|
||||
case "image/webm":
|
||||
return <PhotographIcon className={classes} />;
|
||||
case "application/json":
|
||||
return <CodeIcon className={classes} />;
|
||||
default:
|
||||
return <GlobeAltIcon className={classes} />;
|
||||
}
|
||||
}
|
||||
case "phoneNumber": {
|
||||
return <PhoneIcon className={classes} />;
|
||||
}
|
||||
case "currency": {
|
||||
return <CurrencyDollarIcon className={classes} />;
|
||||
}
|
||||
case "country": {
|
||||
return <GlobeIcon className={classes} />;
|
||||
}
|
||||
case "emoji": {
|
||||
return <EmojiHappyIcon className={classes} />;
|
||||
}
|
||||
case "language": {
|
||||
return <ChatAlt2Icon className={classes} />;
|
||||
}
|
||||
case "filesize": {
|
||||
return <ArchiveIcon className={classes} />;
|
||||
}
|
||||
case "uuid": {
|
||||
return <IdentificationIcon className={classes} />;
|
||||
}
|
||||
case "json":
|
||||
case "jsonPointer": {
|
||||
return <CodeIcon className={classes} />;
|
||||
}
|
||||
case "jwt": {
|
||||
return <KeyIcon className={classes} />;
|
||||
}
|
||||
case "semver": {
|
||||
return <DocumentTextIcon className={classes} />;
|
||||
}
|
||||
case "color": {
|
||||
return <ColorSwatchIcon className={classes} />;
|
||||
}
|
||||
case "creditcard": {
|
||||
switch (type.format.variant) {
|
||||
case "visa": {
|
||||
return <CreditCardIcon className={classes} />;
|
||||
}
|
||||
case "mastercard": {
|
||||
return <CreditCardIcon className={classes} />;
|
||||
}
|
||||
case "amex": {
|
||||
return <CreditCardIcon className={classes} />;
|
||||
}
|
||||
case "discover": {
|
||||
return <CreditCardIcon className={classes} />;
|
||||
}
|
||||
case "dinersclub": {
|
||||
return <CreditCardIcon className={classes} />;
|
||||
}
|
||||
default:
|
||||
return <CreditCardIcon className={classes} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
declare module "json-source-map" {
|
||||
export interface ParseOptions {
|
||||
bigint?: boolean;
|
||||
}
|
||||
|
||||
export type PointerProp = "value" | "valueEnd" | "key" | "keyEnd";
|
||||
|
||||
export interface Location {
|
||||
line: number;
|
||||
column: number;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
export type Pointers = Record<string, Record<PointerProp, Location>>;
|
||||
|
||||
export interface ParseResult {
|
||||
data: any;
|
||||
pointers: Pointers;
|
||||
}
|
||||
|
||||
export function parse(
|
||||
source: string,
|
||||
_reviver?: any,
|
||||
options?: ParseOptions
|
||||
): ParseResult;
|
||||
|
||||
export interface StringifyOptions {
|
||||
space?: string | number;
|
||||
es6?: boolean;
|
||||
}
|
||||
|
||||
export interface StringifyResult {
|
||||
json: string;
|
||||
pointers: Pointers;
|
||||
}
|
||||
|
||||
export function stringify(
|
||||
data: any,
|
||||
_replacer?: any,
|
||||
options?: string | number | StringifyOptions
|
||||
): StringifyResult;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { hydrate } from "react-dom";
|
||||
import { RemixBrowser } from "remix";
|
||||
import { load } from "fathom-client";
|
||||
|
||||
hydrate(<RemixBrowser />, document);
|
||||
|
||||
load("ROBFNTET", {
|
||||
spa: "history",
|
||||
excludedDomains: ["localhost"],
|
||||
includedDomains: ["jsonhero.io"],
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { RemixServer } from "remix";
|
||||
import type { EntryContext } from "remix";
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
const markup = renderToString(
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
return new Response("<!DOCTYPE html>" + markup, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export async function sendEvent(event: Record<string, any>): Promise<void> {
|
||||
const payload = {
|
||||
api_key: GRAPH_JSON_API_KEY,
|
||||
collection: GRAPH_JSON_COLLECTION,
|
||||
json: JSON.stringify(event),
|
||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
|
||||
await fetch("https://api.graphjson.com/api/log", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { RefObject, useEffect, useRef } from "react";
|
||||
|
||||
export function useClickOutside(
|
||||
elementRef: RefObject<HTMLElement>,
|
||||
callback: (event: MouseEvent) => void
|
||||
) {
|
||||
const callbackRef = useRef<(event: MouseEvent) => void>();
|
||||
callbackRef.current = callback;
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (!elementRef?.current || !callbackRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target instanceof Element) {
|
||||
if (!elementRef.current.contains(e.target)) {
|
||||
callbackRef.current(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleClickOutside, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside, true);
|
||||
};
|
||||
}, [callbackRef, elementRef]);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useRef, useEffect, useCallback } from "react";
|
||||
|
||||
export function useIsMounted(): () => boolean {
|
||||
const ref = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = true;
|
||||
return () => {
|
||||
ref.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useCallback(() => ref.current, [ref]);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { stableJson } from "~/utilities/stableJson";
|
||||
|
||||
type JsonContextType = [unknown, Dispatch<SetStateAction<unknown>>];
|
||||
|
||||
const JsonContext = createContext<JsonContextType | undefined>(undefined);
|
||||
|
||||
export function JsonProvider({
|
||||
children,
|
||||
initialJson,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
initialJson: unknown;
|
||||
}) {
|
||||
const stablizedJson = useMemo(() => stableJson(initialJson), [initialJson]);
|
||||
|
||||
const [json, setJson] = useState<unknown>(stablizedJson);
|
||||
|
||||
return (
|
||||
<JsonContext.Provider value={[json, setJson]}>
|
||||
{children}
|
||||
</JsonContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useJson(): JsonContextType {
|
||||
const context = useContext(JsonContext);
|
||||
|
||||
invariant(context, "useJson must be used within a JsonProvider");
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { pick } from "lodash-es";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { createContext, ReactNode, useContext } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import {
|
||||
ColumnViewState,
|
||||
ColumnViewAction,
|
||||
useColumnView,
|
||||
ColumnViewInstanceState,
|
||||
ColumnViewAPI,
|
||||
} from "~/useColumnView";
|
||||
import {
|
||||
generateColumnViewNode,
|
||||
calculateStablePath,
|
||||
firstChildToDescendant,
|
||||
} from "~/utilities/jsonColumnView";
|
||||
import { useJson } from "./useJson";
|
||||
import { useJsonDoc } from "./useJsonDoc";
|
||||
|
||||
export type JsonColumnViewState = ColumnViewInstanceState;
|
||||
export type JsonColumnViewAPI = ColumnViewAPI;
|
||||
|
||||
const JsonColumnViewStateContext = createContext<JsonColumnViewState>(
|
||||
{} as JsonColumnViewState
|
||||
);
|
||||
|
||||
const JsonColumnViewAPIContext = createContext<JsonColumnViewAPI>(
|
||||
{} as JsonColumnViewAPI
|
||||
);
|
||||
|
||||
export function JsonColumnViewProvider({ children }: { children: ReactNode }) {
|
||||
const [json] = useJson();
|
||||
const { doc, path: initialNodeId } = useJsonDoc();
|
||||
|
||||
const rootNode = React.useMemo(() => {
|
||||
return generateColumnViewNode(json);
|
||||
}, [json]);
|
||||
|
||||
const jsonReducer = React.useCallback(
|
||||
(
|
||||
state: ColumnViewState,
|
||||
action: ColumnViewAction,
|
||||
changes: ColumnViewState
|
||||
): ColumnViewState => {
|
||||
if (action.type === "MOVE_UP" || action.type == "MOVE_DOWN") {
|
||||
const { selectedNodeId } = state;
|
||||
const { highlightedNodeId } = changes;
|
||||
|
||||
invariant(selectedNodeId, "expected selectedNodeId");
|
||||
invariant(highlightedNodeId, "expected highlightedNodeId");
|
||||
|
||||
const calculatedPath = calculateStablePath(
|
||||
selectedNodeId,
|
||||
highlightedNodeId,
|
||||
json
|
||||
);
|
||||
|
||||
return {
|
||||
...changes,
|
||||
selectedNodeId: calculatedPath,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
action.type === "MOVE_TO_PARENT" &&
|
||||
action.source &&
|
||||
action.source.altKey
|
||||
) {
|
||||
const { selectedNodeId } = state;
|
||||
|
||||
return {
|
||||
...changes,
|
||||
selectedNodeId,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === "MOVE_TO_CHILDREN") {
|
||||
const { selectedNodeId, highlightedNodeId } = state;
|
||||
|
||||
invariant(selectedNodeId, "expected selectedNodeId");
|
||||
invariant(highlightedNodeId, "expected highlightedNodeId");
|
||||
|
||||
// If the previous highlightedNodeId is an ancestor of the previous selectedNodeId
|
||||
if (isAncestorOf(highlightedNodeId, selectedNodeId)) {
|
||||
// Get the next child of the highlightedNodeId in the path of selectedNodeId
|
||||
// And make the highlightedNodeId that next child
|
||||
// And keep the selectedNodeId unchanged
|
||||
const highlightedPath = new JSONHeroPath(highlightedNodeId);
|
||||
const selectedPath = new JSONHeroPath(selectedNodeId);
|
||||
|
||||
const childPath = firstChildToDescendant(
|
||||
highlightedPath,
|
||||
selectedPath
|
||||
);
|
||||
|
||||
if (!childPath) {
|
||||
return changes;
|
||||
}
|
||||
|
||||
return {
|
||||
...changes,
|
||||
highlightedNodeId: childPath,
|
||||
selectedNodeId,
|
||||
};
|
||||
} else {
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
},
|
||||
[json]
|
||||
);
|
||||
|
||||
const { state, api } = useColumnView({
|
||||
rootNode,
|
||||
initialState: initialNodeId ?? "$",
|
||||
stateReducer: jsonReducer,
|
||||
});
|
||||
|
||||
const isStateRestored = useRef<boolean>(!!initialNodeId);
|
||||
|
||||
useEffect(() => {
|
||||
if (isStateRestored.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isStateRestored.current = true;
|
||||
|
||||
const storage = localStorage.getItem(doc.id);
|
||||
if (storage == null) return;
|
||||
|
||||
const restoredState = JSON.parse(storage) as ColumnViewInstanceState;
|
||||
if (!restoredState.selectedNodeId) return;
|
||||
|
||||
api.goToNodeId(restoredState.selectedNodeId);
|
||||
}, [doc.id, isStateRestored.current, state, api]);
|
||||
|
||||
useEffect(() => {
|
||||
if (doc == null) {
|
||||
return;
|
||||
}
|
||||
if (!isStateRestored.current) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(
|
||||
doc.id,
|
||||
JSON.stringify(pick(state, "selectedNodeId", "highlightedNodeId"))
|
||||
);
|
||||
}, [
|
||||
isStateRestored.current,
|
||||
doc.id,
|
||||
state.selectedNodeId,
|
||||
state.highlightedNodeId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<JsonColumnViewAPIContext.Provider value={api}>
|
||||
<JsonColumnViewStateContext.Provider value={state}>
|
||||
{children}
|
||||
</JsonColumnViewStateContext.Provider>
|
||||
</JsonColumnViewAPIContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useJsonColumnViewState(): JsonColumnViewState {
|
||||
const context = useContext(JsonColumnViewStateContext);
|
||||
|
||||
invariant(
|
||||
context,
|
||||
"useJsonColumnViewState must be used within a JsonColumnViewStateContext.Provider"
|
||||
);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useJsonColumnViewAPI(): JsonColumnViewAPI {
|
||||
const context = useContext(JsonColumnViewAPIContext);
|
||||
|
||||
invariant(
|
||||
context,
|
||||
"useJsonColumnViewAPI must be used within a JsonColumnViewAPIContext.Provider"
|
||||
);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function isAncestorOf(ancestor: string, descendant: string) {
|
||||
return ancestor != descendant && descendant.startsWith(ancestor);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user