初始化
41
jsonhero-web/.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/workflows/main.yml"
|
||||
- "app/**/*.ts"
|
||||
- "app/**/*.tsx"
|
||||
- "public/*"
|
||||
- "styles/*"
|
||||
- "worker/*"
|
||||
- "tests/*"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "remix.config.js"
|
||||
- "tsconfig.json"
|
||||
- "wrangler.toml"
|
||||
- "remix.env.d.ts"
|
||||
- "tailwind.config.js"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
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"
|
||||
14
jsonhero-web/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
|
||||
/.cache
|
||||
/build
|
||||
/public/build
|
||||
.env
|
||||
/app/tailwind.css
|
||||
/jsonDocs
|
||||
.DS_Store
|
||||
/dist
|
||||
.mf
|
||||
/meta.json
|
||||
/stats.html
|
||||
public/entry.worker.js
|
||||
40
jsonhero-web/.vscode/launch.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
jsonhero-web/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": ["$tsc-watch"],
|
||||
"group": "build",
|
||||
"label": "tsc: watch - tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
jsonhero-web/CONTRIBUTING.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## ⚡️ JSON Hero Contributing Guide
|
||||
|
||||
First of all, thanks for considering contributing to this project! If you have any questions please don't hesitate to reach out to [eric@jsonhero.io](mailto:eric@jsonhero.io) or join us on [Discord](https://discord.gg/JtBAxBr2m3).
|
||||
|
||||
JSON Hero is a Typescript React application built with [remix.run](https://remix.run), with support for deploying to Cloudflare workers.
|
||||
|
||||
To get started with contributing, please read our [Development guide](https://github.com/triggerdotdev/jsonhero-web/blob/main/DEVELOPMENT.md) first to get JSON Hero running locally.
|
||||
|
||||
### Running tests
|
||||
|
||||
Although there is less test-coverage for JSON Hero than there should be, tests should still be run to ensure builds have not been broken:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
You can also run tests in "watch" mode:
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
### Making changes
|
||||
|
||||
Please make any changes to your forked repository in a branch other than `main`. If you are working on a bug fix, please use the `bug/` prefix for your branch name. If you are working on a feature, please use `features/`. If you are working on a specific issue please name the branch `issue-<issue number>`
|
||||
|
||||
Make sure to run the `npm lint` command to ensure there are no Typescript compile-time errors.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Please open a Pull Request against the `main` branch in the `triggerdotdev/jsonhero-web` repository. We will aim to address all newly opened PRs by the following Friday. If you haven't opened a Pull Request before, please check out GitHub's [Pull Request documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests)
|
||||
|
||||
### Other JSON Hero projects
|
||||
|
||||
If you'd like to contribute to the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode), please see the [triggerdotdev/vscode-extension](https://github.com/triggerdotdev/vscode-extension) repo.
|
||||
|
||||
For issues related to the JSON Schema inference, please check out [triggerdotdev/schema-infer](https://github.com/triggerdotdev/schema-infer).
|
||||
|
||||
The "Smart Preview" feature is in-part powered by the [@jsonhero/json-infer-types](https://github.com/triggerdotdev/json-infer-types) project.
|
||||
|
||||
If it's related to the Search functionality, please see the [triggerdotdev/fuzzy-json-search](https://github.com/triggerdotdev/fuzzy-json-search) repo.
|
||||
83
jsonhero-web/DEVELOPMENT.md
Normal file
@@ -0,0 +1,83 @@
|
||||
## 👩🏽💻 JSON Hero Local Development Guide
|
||||
|
||||
Welcome to JSON Hero development and thanks for being here! If you'd like to run JSON Hero locally, please use the following guide to get started. If you have any issues with this guide please feel free to email me at [eric@jsonhero.io](mailto:eric@jsonhero.io) or come leave a message in our open [Discord Channel](https://discord.gg/JtBAxBr2m3).
|
||||
|
||||
For more information about contributing to JSON Hero please see the [Contributing doc](https://github.com/triggerdotdev/jsonhero-web/blob/main/CONTRIBUTING.md).
|
||||
|
||||
### Install dependencies
|
||||
|
||||
Before you can run JSON Hero locally, you will need to install the following dependencies on your machine:
|
||||
|
||||
#### Git
|
||||
|
||||
You most likely already have git installed on your machine, but if not, you can install it from the [Git website](https://git-scm.com).
|
||||
|
||||
#### Node.js 16
|
||||
|
||||
Even though JSON Hero runs on [Cloudflare Workers](https://workers.cloudflare.com), which isn't a Node.js environment, you will still need Node.js 16 to run it locally. The recommended way to install Node.js is to download a pre-built package from the [Node.js website](https://nodejs.org/en/)
|
||||
|
||||
#### NPM
|
||||
|
||||
If you install Node.js through the above link, you should also have NPM automatically installed as well. To make sure, run the following command in your preferred Terminal:
|
||||
|
||||
```bash
|
||||
npm ---version
|
||||
```
|
||||
|
||||
### Fork JSON Hero on GitHub (optional)
|
||||
|
||||
To contribute code to JSON Hero, you should first create a fork of the [jsonhero-web](https://github.com/triggerdotdev/jsonhero-web) repository on GitHub. Follow [these instructions](https://docs.github.com/en/get-started/quickstart/fork-a-repo) on repository forking.
|
||||
|
||||
### Clone the repo
|
||||
|
||||
In your terminal, issue the following command to clone the repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/triggerdotdev/jsonhero-web.git
|
||||
```
|
||||
|
||||
Or if you've forked the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<github username>/jsonhero-web.git
|
||||
```
|
||||
|
||||
Then `cd` into the repository:
|
||||
|
||||
```bash
|
||||
cd jsonhero-web
|
||||
```
|
||||
|
||||
### Prepare the repo
|
||||
|
||||
First, install npm dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Run the following command to create the `.env` file with a new `SESSION_SECRET` environment variable:
|
||||
|
||||
```bash
|
||||
echo "SESSION_SECRET=$(openssl rand -hex 32)" > .env
|
||||
```
|
||||
|
||||
Then, run `npm run build` or `npm run dev` to build.
|
||||
|
||||
Start the development server:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
You should now be able to access your local JSON Hero server on [localhost:8787](http://localhost:8787)
|
||||
|
||||
> **Note** JSON documents created locally are not persisted across server restarts
|
||||
|
||||
### Previewing URLs
|
||||
|
||||
We currently use [OpenGraph Ninja](https://opengraph.ninja/) to power some of the Preview URL functionality.
|
||||
|
||||
### Deploying to Cloudflare
|
||||
|
||||
_Coming Soon_
|
||||
12
jsonhero-web/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
# Builder
|
||||
FROM node:16.17.0 as builder
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
|
||||
# App
|
||||
RUN cd /src
|
||||
RUN npm install
|
||||
RUN echo "SESSION_SECRET=abc123" > .env
|
||||
RUN npm run build
|
||||
|
||||
CMD npm start
|
||||
201
jsonhero-web/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
159
jsonhero-web/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/4a157bda-2a99-4ac3-6bc7-be08b4a46600/public">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/31447544-b16f-49dd-c206-74b1802c6700/public">
|
||||
<img width=200 alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/4a157bda-2a99-4ac3-6bc7-be08b4a46600/public">
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
<p align="center">
|
||||
<a href="https://console.algora.io/org/triggerdotdev/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Ftriggerdotdev%2Fbounties%3Fstatus%3Dopen" alt="Open Bounties" /></a>
|
||||
<a href="https://console.algora.io/org/triggerdotdev/bounties?status=completed"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Ftriggerdotdev%2Fbounties%3Fstatus%3Dcompleted" alt="Rewarded Bounties" /></a>
|
||||
</p>
|
||||
|
||||
# Brought to you by Trigger.dev
|
||||
|
||||
JSON Hero was created and is maintained by the team behind [Trigger.dev](https://trigger.dev). With Trigger.dev you can trigger workflows from APIs, on a schedule, or on demand. We make API calls easy with authentication handled for you, and you can add durable delays that survive server restarts.
|
||||
|
||||
# JSON Hero
|
||||
|
||||
JSON Hero makes reading and understand JSON files easy by giving you a clean and beautiful UI packed with extra features.
|
||||
|
||||
- View JSON any way you'd like: Column View, Tree View, Editor View, and more.
|
||||
- Automatically infers the contents of strings and provides useful previews
|
||||
- Creates an inferred JSON Schema that could be used to validate your JSON
|
||||
- Quickly scan related values to check for edge cases
|
||||
- Search your JSON files (both keys and values)
|
||||
- Keyboard accessible
|
||||
- Easily sharable URLs with path support
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
### Send to JSON Hero
|
||||
|
||||
Send your JSON to JSON Hero in a variety of ways
|
||||
|
||||
- Head to [jsonhero.io](https://jsonhero.io) and Drag and Drop a JSON file, or paste JSON or a JSON url in the provided form
|
||||
- Include a Base64 encoded string of a JSON payload: [jsonhero.io/new?j=eyAiZm9vIjogImJhciIgfQ==](https://jsonhero.io/new?j=eyAiZm9vIjogImJhciIgfQ==)
|
||||
- Include a JSON URL to the `new` endpoint: [jsonhero.io/new?url=https://jsonplaceholder.typicode.com/todos/1](https://jsonhero.io/new?url=https://jsonplaceholder.typicode.com/todos/1)
|
||||
- Install the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=JSONHero.jsonhero-vscode) and open JSON from VS Code
|
||||
- Raycast user? Check out our extension [here](https://www.raycast.com/maverickdotdev/open-in-json-hero)
|
||||
- Use the unofficial API:
|
||||
|
||||
- Make a `POST` request to `jsonhero.io/api/create.json` with the following JSON body:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "test 123",
|
||||
"content": { "foo": "bar" },
|
||||
"readOnly": false, // this is optional, will make it so the document title cannot be edited or document cannot be deleted
|
||||
"ttl": 3600 // this will expire the document after 3600 seconds, also optional
|
||||
}
|
||||
```
|
||||
|
||||
The JSON response will be the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "YKKduNySH7Ub",
|
||||
"title": "test 123",
|
||||
"location": "https://jsonhero.io/j/YKKduNySH7Ub"
|
||||
}
|
||||
```
|
||||
|
||||
### Column view
|
||||
|
||||
Inspired by macOS Finder, Column View is a new way to browse a JSON document.
|
||||
|
||||

|
||||
|
||||
It has all the features you'd expect: Keyboard navigation, Path bar, history.
|
||||
|
||||
It also has a nifty feature that allows you to "hold" a descendent selected and travel up through the hierarchy, and then move between siblings and view the different values found at that path. It's hard to describe, but here is an animation to help demonstrate:
|
||||
|
||||

|
||||
|
||||
As you can see, holding the `Option` (or `Alt` key on Windows) while moving to a parent keeps the part of the document selected and shows it in context of it's surrounding JSON. Then you can traverse between items in an array and compare the values of the selection across deep hierarchy cahnges.
|
||||
|
||||
### Editor view
|
||||
|
||||
View your entire JSON document in an editor, but keep the nice previews and related values you get from the sidebar as you move around the document:
|
||||
|
||||

|
||||
|
||||
### Tree view
|
||||
|
||||
Use a traditional tree view to traverse your JSON document, with collapsible sections and keyboard shortcuts. All while keeping the nice previews:
|
||||
|
||||

|
||||
|
||||
### Search
|
||||
|
||||
Quickly open a search panel and fuzzy search your entire JSON file in milliseconds. Searches through key names, key paths, values, and even pretty formatted values (e.g. Searching for `"Dec"` will find datetime strings in the month of December.)
|
||||
|
||||

|
||||
|
||||
### Content Previews
|
||||
|
||||
JSON Hero automatically infers the content of strings and provides useful previews and properties of the value you've selected. It's "Show Don't Tell" for JSON:
|
||||
|
||||
#### Dates and Times
|
||||
|
||||

|
||||
|
||||
#### Image URLs
|
||||
|
||||

|
||||
|
||||
#### Website URLs
|
||||
|
||||

|
||||
|
||||
#### Tweet URLS
|
||||
|
||||

|
||||
|
||||
#### JSON URLs
|
||||
|
||||

|
||||
|
||||
#### Colors
|
||||
|
||||

|
||||
|
||||
### Related Values
|
||||
|
||||
Easily see all the related values across your entire JSON document for a specific field, including any `undefined` or `null` values.
|
||||
|
||||

|
||||
|
||||
<!-- TODO -->
|
||||
|
||||
## Bugs and Feature Requests
|
||||
|
||||
Have a bug or a feature request? Feel free to [open a new issue](https://github.com/triggerdotdev/jsonhero-web/issues).
|
||||
|
||||
You can also join our [Discord channel](https://discord.gg/JtBAxBr2m3) to hang out and discuss anything you'd like.
|
||||
|
||||
## Developing
|
||||
|
||||
To run locally, first clone the repo and install the dependencies:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/triggerdotdev/jsonhero-web.git
|
||||
cd jsonhero-web
|
||||
npm install
|
||||
```
|
||||
|
||||
Then, create a file at the root of the repo called `.env` and set the `SESSION_SECRET` value:
|
||||
|
||||
```
|
||||
SESSION_SECRET=abc123
|
||||
```
|
||||
|
||||
Then, run `npm run build` or `npm run dev` to build.
|
||||
|
||||
Now, run `npm start` and open your browser to `http://localhost:8787`
|
||||
40
jsonhero-web/SELF_HOSTING.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Deploying to Cloudflare
|
||||
|
||||
### Install and login to wrangler
|
||||
```bash
|
||||
npm install -g wrangler
|
||||
wrangler login
|
||||
```
|
||||
|
||||
### Create service
|
||||
Go to workers tab from your [cloudflare profile](https://dash.cloudflare.com/profile) and create a new worker. Use HTTP Handler as service type. The name of worker must match the `name` field in `wrangler.toml`.
|
||||
|
||||
### Setup wrangler.toml
|
||||
Edit the following variables in `wrangler.toml` and `wrangler.toml.dev`:
|
||||
- `account_id`: Get account id by using
|
||||
```bash
|
||||
wrangler whoami
|
||||
```
|
||||
- `kv_namespaces`: Run the following comands to create a new KV namespace.
|
||||
```bash
|
||||
wrangler kv:namespace create DOCUMENTS # gives namespace id
|
||||
wrangler kv:namespace create DOCUMENTS --preview # gives preview id for namespace
|
||||
```
|
||||
Replace current entry for `kv_namespaces` as:
|
||||
```toml
|
||||
kv_namespaces = [
|
||||
{ binding = "DOCUMENTS", id = <YOUR_ID>, preview_id = <YOUR_PREVIEW_ID> }
|
||||
]
|
||||
```
|
||||
|
||||
### Configure Environment Variables
|
||||
Set `SESSION_SECRET` environment for worker.
|
||||
```bash
|
||||
wrangler secret put SESSION_SECRET
|
||||
```
|
||||
Optionally set other secrets listed at the end of `wrangler.toml`.
|
||||
|
||||
### Publish worker
|
||||
```bash
|
||||
wrangler publish
|
||||
```
|
||||
BIN
jsonhero-web/app/assets/home/JsonHero2.mp4
Normal file
BIN
jsonhero-web/app/assets/home/JsonHeroSearch.mp4
Normal file
BIN
jsonhero-web/app/assets/home/JsonHeroShare.mp4
Normal file
BIN
jsonhero-web/app/assets/home/UncoverEdgeCases.mp4
Normal file
BIN
jsonhero-web/app/assets/images/opengraph.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
jsonhero-web/app/assets/images/td-triangle.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
jsonhero-web/app/assets/images/trigger-dev-logo-dark.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
jsonhero-web/app/assets/images/trigger-dev-logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
4
jsonhero-web/app/assets/svgs/CopyIcon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="#64748B">
|
||||
<path d="M9 2a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2V6.414A2 2 0 0016.414 5L14 2.586A2 2 0 0012.586 2H9z" />
|
||||
<path d="M3 8a2 2 0 012-2v10h8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
4
jsonhero-web/app/assets/svgs/EyeIcon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="#94A3B8">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
3
jsonhero-web/app/assets/svgs/TickIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.7071 5.29289C17.0976 5.68342 17.0976 6.31658 16.7071 6.70711L8.70711 14.7071C8.31658 15.0976 7.68342 15.0976 7.29289 14.7071L3.29289 10.7071C2.90237 10.3166 2.90237 9.68342 3.29289 9.29289C3.68342 8.90237 4.31658 8.90237 4.70711 9.29289L8 12.5858L15.2929 5.29289C15.6834 4.90237 16.3166 4.90237 16.7071 5.29289Z" fill="#64748B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 486 B |
9
jsonhero-web/app/bindings.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
const DOCUMENTS: KVNamespace;
|
||||
const SESSION_SECRET: string;
|
||||
const GRAPH_JSON_API_KEY: string;
|
||||
const GRAPH_JSON_COLLECTION: string;
|
||||
const APIHERO_PROJECT_KEY: string;
|
||||
}
|
||||
30
jsonhero-web/app/components/AutoplayVideo.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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 (elementRef.current == null) return;
|
||||
|
||||
elementRef.current.muted = true;
|
||||
elementRef.current.playsInline = true;
|
||||
|
||||
if (isOnScreen) {
|
||||
elementRef.current.play();
|
||||
} else {
|
||||
elementRef.current.pause();
|
||||
}
|
||||
}, [isOnScreen]);
|
||||
|
||||
return (
|
||||
<video
|
||||
src={src}
|
||||
ref={elementRef}
|
||||
loop={true}
|
||||
muted={true}
|
||||
autoPlay={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
jsonhero-web/app/components/BlankColumn.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { memo } from "react";
|
||||
|
||||
function BlankColumnElement() {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"column flex-none border-r-[1px] border-slate-300 w-80 transition dark:border-slate-600"
|
||||
}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
export const BlankColumn = memo(BlankColumnElement);
|
||||
122
jsonhero-web/app/components/CodeEditor.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import {
|
||||
EditorView,
|
||||
TransactionSpec,
|
||||
useCodeMirror,
|
||||
ViewUpdate,
|
||||
} from "@uiw/react-codemirror";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
import { getEditorSetup } from "~/utilities/codeMirrorSetup";
|
||||
import { darkTheme, lightTheme } from "~/utilities/codeMirrorTheme";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
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 selectionStart = selection?.start ?? defaultProps.selection.start;
|
||||
const selectionEnd = selection?.end ?? defaultProps.selection.end;
|
||||
|
||||
const lineNumber = state?.doc.lineAt(selectionStart).number;
|
||||
|
||||
const transactionSpec: TransactionSpec = {
|
||||
selection: { anchor: selectionStart, head: selectionEnd },
|
||||
effects: EditorView.scrollIntoView(selectionStart, {
|
||||
y: "start",
|
||||
yMargin: 100,
|
||||
}),
|
||||
};
|
||||
|
||||
view.dispatch(transactionSpec);
|
||||
}
|
||||
}, [selection, view, setSelectionRef.current]);
|
||||
|
||||
const { minimal } = useJsonDoc();
|
||||
|
||||
useHotkeys(
|
||||
"ctrl+a,meta+a,command+a",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });
|
||||
},
|
||||
[view, state]
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={`${
|
||||
minimal ? "h-jsonViewerHeightMinimal" : "h-jsonViewerHeight"
|
||||
} overflow-y-auto no-scrollbar`}
|
||||
ref={editor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
jsonhero-web/app/components/CodeViewer.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
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";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
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, view, state } = 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]);
|
||||
|
||||
useHotkeys(
|
||||
"ctrl+a,meta+a,command+a",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });
|
||||
},
|
||||
[view, state]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={editor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
jsonhero-web/app/components/Column.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Title } from "./Primitives/Title";
|
||||
import { colorForItemAtPath } from "~/utilities/colors";
|
||||
import { IconComponent } from "~/useColumnView";
|
||||
import { useJson } from "../hooks/useJson";
|
||||
import { memo, useMemo } from "react";
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
|
||||
export type ColumnProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: IconComponent;
|
||||
hasHighlightedElement: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
function ColumnElement(column: ColumnProps) {
|
||||
const { id, title, children } = column;
|
||||
const [json] = useJson();
|
||||
const { minimal } = useJsonDoc();
|
||||
const iconColor = useMemo(() => colorForItemAtPath(id, json), [id, json]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"column flex-none border-r-[1px] border-slate-300 w-80 transition dark:border-slate-600"
|
||||
}
|
||||
>
|
||||
<div className="flex items-center 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="h-6 w-6 mr-1" />}
|
||||
<Title className="text-ellipsis overflow-hidden">{title}</Title>
|
||||
</div>
|
||||
<div
|
||||
className={`overflow-y-auto ${
|
||||
minimal ? "h-viewerHeightMinimal" : "h-viewerHeight"
|
||||
} no-scrollbar`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Column = memo(ColumnElement);
|
||||
93
jsonhero-web/app/components/ColumnItem.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { Mono } from "./Primitives/Mono";
|
||||
import { memo, useEffect, useMemo, useRef } from "react";
|
||||
import { ColumnViewNode } from "~/useColumnView";
|
||||
import { colorForItemAtPath } from "~/utilities/colors";
|
||||
import { Body } from "./Primitives/Body";
|
||||
|
||||
export type ColumnItemProps = {
|
||||
item: ColumnViewNode;
|
||||
json: unknown;
|
||||
isSelected: boolean;
|
||||
isHighlighted: boolean;
|
||||
onClick?: (id: string) => void;
|
||||
};
|
||||
|
||||
function ColumnItemElement({
|
||||
item,
|
||||
json,
|
||||
isSelected,
|
||||
isHighlighted,
|
||||
onClick,
|
||||
}: ColumnItemProps) {
|
||||
const htmlElement = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showArrow = item.children.length > 0;
|
||||
|
||||
const stateStyle = useMemo<string>(() => {
|
||||
if (isHighlighted) {
|
||||
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-100";
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
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-200";
|
||||
}
|
||||
|
||||
return "hover:bg-slate-100 transition duration-75 ease-out dark:hover:bg-white dark:hover:bg-opacity-[5%] dark:text-slate-400";
|
||||
}, [isSelected, isHighlighted]);
|
||||
|
||||
const iconColor = useMemo<string>(
|
||||
() => colorForItemAtPath(item.id, json),
|
||||
[item.id, json]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected || isHighlighted) {
|
||||
htmlElement.current?.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
}
|
||||
}, [isSelected, isHighlighted]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex h-9 items-center justify-items-stretch mx-1 px-1 py-1 my-1 rounded-sm ${stateStyle}`}
|
||||
onClick={() => onClick && onClick(item.id)}
|
||||
ref={htmlElement}
|
||||
>
|
||||
<div className="w-4 flex-none flex-col justify-items-center">
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className={`h-5 w-5 ${
|
||||
isSelected && isHighlighted
|
||||
? "text-slate-900 dark:text-slate-300"
|
||||
: "text-slate-500"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow flex-shrink items-baseline justify-between truncate">
|
||||
<Body className="flex-grow flex-shrink-0 pl-3 pr-2 ">{item.title}</Body>
|
||||
{item.subtitle && (
|
||||
<Mono
|
||||
className={`truncate pr-1 transition duration-75 ${
|
||||
isHighlighted
|
||||
? "text-gray-500 dark:text-slate-100"
|
||||
: "text-gray-400 dark:text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{item.subtitle}
|
||||
</Mono>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showArrow && (
|
||||
<ChevronRightIcon className="flex-none w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ColumnItem = memo(ColumnItemElement);
|
||||
61
jsonhero-web/app/components/Columns.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { memo, useMemo } from "react";
|
||||
import { useJson } from "~/hooks/useJson";
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "~/hooks/useJsonColumnView";
|
||||
import { ColumnDefinition } from "~/useColumnView";
|
||||
import { BlankColumn } from "./BlankColumn";
|
||||
import { Column } from "./Column";
|
||||
import { ColumnItem } from "./ColumnItem";
|
||||
|
||||
function ColumnsElement({ columns }: { columns: ColumnDefinition[] }) {
|
||||
const [json] = useJson();
|
||||
const { selectedPath, highlightedPath, highlightedNodeId } =
|
||||
useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
const highlightedItemIsValue = useMemo<boolean>(() => {
|
||||
if (highlightedNodeId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = new JSONHeroPath(highlightedNodeId);
|
||||
let item = path.first(json);
|
||||
|
||||
return typeof item !== "object";
|
||||
}, [highlightedPath, json]);
|
||||
|
||||
return (
|
||||
<div className="columns flex flex-grow overflow-x-auto focus:outline-none no-scrollbar">
|
||||
{columns.map((column) => {
|
||||
return (
|
||||
<Column
|
||||
key={column.id}
|
||||
id={column.id}
|
||||
title={column.title}
|
||||
icon={column.icon}
|
||||
hasHighlightedElement={
|
||||
highlightedPath[highlightedPath.length - 2] === column.id
|
||||
}
|
||||
>
|
||||
{column.items.map((item) => (
|
||||
<ColumnItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
json={json}
|
||||
isSelected={selectedPath.includes(item.id)}
|
||||
isHighlighted={
|
||||
highlightedPath[highlightedPath.length - 1] === item.id
|
||||
}
|
||||
onClick={(id) => goToNodeId(id, "columnView")}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
);
|
||||
})}
|
||||
{highlightedItemIsValue ? <BlankColumn /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const Columns = memo(ColumnsElement);
|
||||
74
jsonhero-web/app/components/ContainerInfo.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
20
jsonhero-web/app/components/CopySelectedNode.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useSelectedInfo } from "../hooks/useSelectedInfo";
|
||||
|
||||
export function CopySelectedNodeShortcut() {
|
||||
const selectedInfo = useSelectedInfo();
|
||||
|
||||
useHotkeys(
|
||||
'shift+c,shift+C',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const selectedJSON = selectedInfo?.name === "string"
|
||||
? selectedInfo?.value
|
||||
: JSON.stringify(selectedInfo?.value, null, 2);
|
||||
navigator.clipboard.writeText(selectedJSON);
|
||||
},
|
||||
[selectedInfo]
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
28
jsonhero-web/app/components/CopyText.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
export type CopyTextProps = {
|
||||
children?: React.ReactNode;
|
||||
value: string;
|
||||
className?: string;
|
||||
onCopied?: () => void;
|
||||
};
|
||||
|
||||
export function CopyText({
|
||||
children,
|
||||
value,
|
||||
className,
|
||||
onCopied,
|
||||
}: CopyTextProps) {
|
||||
const onClick = useCallback(() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
if (onCopied) {
|
||||
onCopied();
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div onClick={onClick} className={`${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
jsonhero-web/app/components/CopyTextButton.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ClipboardIcon } from "@heroicons/react/outline";
|
||||
import { useCallback, useState } from "react";
|
||||
import { CopyText } from "./CopyText";
|
||||
import { Body } from "./Primitives/Body";
|
||||
|
||||
export type CopyTextButtonProps = {
|
||||
value: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function CopyTextButton({ value, className }: CopyTextButtonProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const onCopied = useCallback(() => {
|
||||
setCopied(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1500);
|
||||
}, [value]);
|
||||
return (
|
||||
<CopyText className={`${className}`} value={value} onCopied={onCopied}>
|
||||
{copied ? (
|
||||
<Body>Copied!</Body>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<ClipboardIcon className="h-4 w-4 mr-[2px]" />
|
||||
<Body>Copy</Body>
|
||||
</div>
|
||||
)}
|
||||
</CopyText>
|
||||
);
|
||||
}
|
||||
73
jsonhero-web/app/components/DataTable.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { FunctionComponent, useState } from "react";
|
||||
import { CopyTextButton } from "./CopyTextButton";
|
||||
import { Title } from "./Primitives/Title";
|
||||
|
||||
export type DataTableProps = {
|
||||
rows: DataTableRow[];
|
||||
};
|
||||
|
||||
export type DataTableRow = {
|
||||
key: string;
|
||||
value: string;
|
||||
icon?: JSX.Element;
|
||||
};
|
||||
|
||||
type DataRowProps = {
|
||||
title: string;
|
||||
value: string;
|
||||
icon?: JSX.Element;
|
||||
};
|
||||
|
||||
const DataRow: FunctionComponent<DataRowProps> = ({ title, value, icon }) => {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
return (
|
||||
<tr 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">{title}</div>
|
||||
</td>
|
||||
<td
|
||||
onMouseOver={() => setHovering(true)}
|
||||
onMouseOut={() => setHovering(false)}
|
||||
className={`relative w-full h-full pl-2 py-2 text-base text-slate-800 transition dark:text-slate-300 break-all ${
|
||||
hovering ? "bg-slate-100 dark:bg-slate-700" : "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
<div
|
||||
className={`absolute top-0 right-0 flex justify-end h-full w-full transition ${
|
||||
hovering ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CopyTextButton
|
||||
className="bg-slate-200 hover:bg-slate-300 h-fit mt-1 mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-600 dark:hover:bg-slate-500"
|
||||
value={value}
|
||||
></CopyTextButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<DataRow
|
||||
key={row.key}
|
||||
title={row.key}
|
||||
value={row.value}
|
||||
icon={row.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
81
jsonhero-web/app/components/DocumentTitle.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { PencilAltIcon } from "@heroicons/react/outline";
|
||||
import { 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]);
|
||||
|
||||
if (doc.readOnly) {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center items-center w-full"
|
||||
title={doc.title}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"min-w-[15vw] border-none text-ellipsis text-slate-300 px-2 pl-10 py-1 rounded-sm bg-transparent placeholder:text-slate-400 focus:bg-black/30 focus:outline-none focus:border-none 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"
|
||||
}
|
||||
>
|
||||
{doc.title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<updateDoc.Form method="post" action={`/actions/${doc.id}/update`}>
|
||||
<div
|
||||
className="flex justify-center items-center w-full"
|
||||
title={doc.title}
|
||||
>
|
||||
<label className="relative block group">
|
||||
<PencilAltIcon className="h-5 w-5 absolute top-1/2 transform -translate-y-1/2 left-3 text-white opacity-0 transition pointer-events-none group-hover:opacity-80 group-focus:opacity-80" />
|
||||
<input
|
||||
ref={ref}
|
||||
className={
|
||||
"min-w-[15vw] border-none text-ellipsis text-slate-300 px-2 pl-10 py-1 rounded-sm bg-transparent placeholder:text-slate-400 focus:bg-black/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)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{match(editedTitle)
|
||||
.with(doc.title, () => (
|
||||
<p className="ml-2 text-transparent">Save</p>
|
||||
))
|
||||
.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>
|
||||
);
|
||||
}
|
||||
}
|
||||
90
jsonhero-web/app/components/DragAndDropForm.tsx
Normal file
@@ -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] cursor-pointer rounded-md border-2 border-dashed border-slate-600 bg-slate-900/40 p-4 text-base text-slate-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="flex items-center">
|
||||
<ArrowCircleDownIcon
|
||||
className={`mr-3 inline h-6 w-6 ${
|
||||
isDragActive ? "text-lime-500" : ""
|
||||
}`}
|
||||
/>
|
||||
<p className={`${isDragActive ? "text-lime-500" : ""}`}>
|
||||
{isDragActive
|
||||
? "现在释放以打开…"
|
||||
: "在此处拖放 JSON 文件,或点击选择"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="filename" ref={filenameInputRef} />
|
||||
<input type="hidden" name="rawJson" ref={rawJsonInputRef} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
20
jsonhero-web/app/components/ExampleDoc.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Link } from "remix";
|
||||
|
||||
export function ExampleDoc({
|
||||
id,
|
||||
title,
|
||||
path,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
path?: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
to={`/j/${id}${path ? `?path=${path}` : ""}`}
|
||||
className="bg-slate-900 px-4 py-2 rounded-md whitespace-nowrap text-lime-300 transition hover:text-lime-500"
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
28
jsonhero-web/app/components/ExampleUrl.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Form } from "remix";
|
||||
|
||||
export function ExampleUrl({
|
||||
url,
|
||||
title,
|
||||
displayTitle,
|
||||
}: {
|
||||
url: string;
|
||||
title: string;
|
||||
displayTitle?: string;
|
||||
}) {
|
||||
return (
|
||||
<Form
|
||||
method="post"
|
||||
action="/actions/createFromUrl?utm_source=example_url"
|
||||
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-md whitespace-nowrap text-lime-300 transition hover:text-lime-500"
|
||||
>
|
||||
{displayTitle ?? title}
|
||||
</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
52
jsonhero-web/app/components/FileSelector/FileDropzone.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
65
jsonhero-web/app/components/Footer.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
import { ArrowKeysIcon } from "./Icons/ArrowKeysIcon";
|
||||
import { CopyShortcutIcon } from "./Icons/CopyShortcutIcon";
|
||||
import { EscapeKeyIcon } from "./Icons/EscapeKeyIcon";
|
||||
import { SquareBracketsIcon } from "./Icons/SquareBracketsIcon";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { ThemeModeToggler } from "./ThemeModeToggle";
|
||||
import { GithubStarSmall } from "./UI/GithubStarSmall";
|
||||
import { IndentPreference } from "~/components/IndentPreference";
|
||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import TriggerDevLogoImageDark from "~/assets/images/trigger-dev-logo-dark.png";
|
||||
import TriggerDevLogoImage from "~/assets/images/trigger-dev-logo.png";
|
||||
import TriggerDevLogoTriangleImage from "~/assets/images/td-triangle.png";
|
||||
|
||||
export function Footer() {
|
||||
const { minimal } = useJsonDoc();
|
||||
|
||||
return (
|
||||
<footer className="flex items-center justify-between w-screen h-[32px] flex-shrink-0 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 whitespace-nowrap">
|
||||
Reset path
|
||||
</Body>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<CopyShortcutIcon className="transition text-slate-300 dark:text-slate-500" />
|
||||
<Body className="flex pl-2 pr-4 text-slate-800 transition dark:text-white">
|
||||
Copy
|
||||
<span className="hidden lg:flex whitespace-nowrap">
|
||||
selected
|
||||
</span>
|
||||
node
|
||||
</Body>
|
||||
</li>
|
||||
</ol>
|
||||
<ol className="flex gap-2 items-center h-full invisible md:visible">
|
||||
{minimal && (
|
||||
<li>
|
||||
<GithubStarSmall />
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<IndentPreference />
|
||||
</li>
|
||||
<li>
|
||||
<ThemeModeToggler />
|
||||
</li>
|
||||
</ol>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
94
jsonhero-web/app/components/Header.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ShareIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { DocumentTitle } from "./DocumentTitle";
|
||||
import { DiscordIconTransparent } from "./Icons/DiscordIconTransparent";
|
||||
import { EmailIconTransparent } from "./Icons/EmailIconTransparent";
|
||||
import { GithubStar } from "./UI/GithubStar";
|
||||
import { Logo } from "./Icons/Logo";
|
||||
import { Share } from "./Share";
|
||||
import { NewDocument } from "./NewDocument";
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "./UI/Popover";
|
||||
import { Form } from "remix";
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
import { LogoTriggerdotdev } from "./Icons/LogoTriggerdotdev";
|
||||
|
||||
export function Header() {
|
||||
const { doc } = useJsonDoc();
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between w-screen h-[40px] bg-indigo-700 dark:bg-slate-800 border-b-[1px] border-slate-600">
|
||||
<div className="flex pl-2 gap-1 sm:gap-1.5 pt-0.5 h-8 justify-center items-center">
|
||||
<div className="w-20 sm:w-24">
|
||||
<Logo />
|
||||
</div>
|
||||
<p className="text-slate-300 text-sm font-sans">by</p>
|
||||
<LogoTriggerdotdev className="w-16 sm:w-20 opacity-80 hover:opacity-100 transition duration-300" />
|
||||
</div>
|
||||
<DocumentTitle />
|
||||
<ol className="flex text-sm items-center gap-2 px-4">
|
||||
{!doc.readOnly && (
|
||||
<Form
|
||||
method="delete"
|
||||
onSubmit={(e) =>
|
||||
!confirm(
|
||||
"这将从 jsonhero.io 永久删除此文档,您确定要继续吗?"
|
||||
) && e.preventDefault()
|
||||
}
|
||||
>
|
||||
<button type="submit">
|
||||
<button className="flex items-center justify-center py-1 bg-slate-200 text-slate-800 bg-opacity-80 text-base font-bold px-2 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
|
||||
<TrashIcon className="w-4 h-4 mr-0.5"></TrashIcon>
|
||||
删除
|
||||
</button>
|
||||
</button>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button className="flex items-center justify-center bg-lime-500 text-slate-800 bg-opacity-90 text-base font-bold px-2 py-1 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
|
||||
<PlusIcon className="w-4 h-4 mr-0.5"></PlusIcon>
|
||||
新建
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" sideOffset={8}>
|
||||
<NewDocument />
|
||||
<PopoverArrow
|
||||
className="fill-current text-indigo-700"
|
||||
offset={20}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button className="flex items-center justify-center py-1 bg-slate-200 text-slate-800 bg-opacity-90 text-base font-bold px-2 rounded uppercase hover:cursor-pointer hover:bg-opacity-100 transition">
|
||||
<ShareIcon className="w-4 h-4 mr-1"></ShareIcon>
|
||||
分享
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" sideOffset={8}>
|
||||
<Share />
|
||||
<PopoverArrow
|
||||
className="fill-current text-indigo-700"
|
||||
offset={20}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<li className="opacity-90 transition hover:cursor-pointer hover:opacity-100">
|
||||
<GithubStar />
|
||||
</li>
|
||||
<li className="opacity-90 transition hover:cursor-pointer hover:opacity-100">
|
||||
<a href="https://discord.gg/JtBAxBr2m3" target="_blank">
|
||||
<DiscordIconTransparent />
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
33
jsonhero-web/app/components/Home/HomeApiHeroBanner.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Body } from "../Primitives/Body";
|
||||
import { HomeApiHeroLaptop } from "./HomeApiHeroLaptop";
|
||||
|
||||
export function HomeApiHeroBanner() {
|
||||
return (
|
||||
<div className="flex items-center justify-start md:justify-center w-full h-40 bg-gradient-to-r from-purple-600 via-pink-500 to-purple-600 hover:backdrop-filter hover:backdrop-brightness-75 transition">
|
||||
<div className="relative flex justify-center items-center w-1/2 md:w-full pl-6 md:px-6">
|
||||
<div className="flex flex-col">
|
||||
<Body className=" text-white text-[1rem] sm:text-[1.2rem] font-bold md:text-3xl leading-tight">
|
||||
Early access to ⚡️ API Hero
|
||||
</Body>
|
||||
<p className="mb-2 text-white md:text-xl text-sm">
|
||||
Make every API you use faster and more reliable.
|
||||
</p>
|
||||
<a
|
||||
href="https://apihero.run"
|
||||
target="new"
|
||||
className="flex items-center justify-center px-3 py-2 mt-2 text-center text-md md:text-xl text-slate-800 font-bold bg-lime-500 rounded shadow-md hover:bg-lime-400 transition"
|
||||
>
|
||||
Get started →
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href="https://apihero.run"
|
||||
target="new"
|
||||
className="absolute md:relative -top-5 md:top-auto -right-[20rem] md:right-auto"
|
||||
>
|
||||
<HomeApiHeroLaptop className="w-50 md:w-80 mb-2"></HomeApiHeroLaptop>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
jsonhero-web/app/components/Home/HomeApiHeroLaptop.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import ApiHeroLaptop from "~/assets/images/apihero-laptop.png";
|
||||
|
||||
export type IconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function HomeApiHeroLaptop({ className }: IconProps) {
|
||||
return <img src={ApiHeroLaptop} className={className} />;
|
||||
}
|
||||
29
jsonhero-web/app/components/Home/HomeCollaborateSection.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AutoplayVideo } from "../AutoplayVideo";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
import shareVideo from "~/assets/home/JsonHeroShare.mp4";
|
||||
|
||||
export function HomeCollaborateSection() {
|
||||
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">
|
||||
Collaborate with the whole world (and yourself)
|
||||
</ExtraLargeTitle>
|
||||
<SmallSubtitle className="mb-6 md:mb-10">
|
||||
Easily share your JSON documents with any distant relative. 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={shareVideo} />
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
30
jsonhero-web/app/components/Home/HomeEdgeCasesSection.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AutoplayVideo } from "../AutoplayVideo";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
import edgeCasesVideo from "~/assets/home/UncoverEdgeCases.mp4";
|
||||
|
||||
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={edgeCasesVideo} />
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
93
jsonhero-web/app/components/Home/HomeFeatureGridSection.tsx
Normal file
@@ -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-300">
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
53
jsonhero-web/app/components/Home/HomeFooter.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Link } from "remix";
|
||||
import { DiscordIcon } from "../Icons/DiscordIcon";
|
||||
import { EmailIcon } from "../Icons/EmailIcon";
|
||||
import { GithubIcon } from "../Icons/GithubIcon";
|
||||
import { Logo } from "../Icons/Logo";
|
||||
import { TwitterIcon } from "../Icons/TwitterIcon";
|
||||
|
||||
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 border-t-[1px] pt-9 border-slate-800"
|
||||
style={{ maxWidth: maxWidth }}
|
||||
>
|
||||
<div className="flex flex-grow items-start">
|
||||
<Logo />
|
||||
</div>
|
||||
<ol className="flex ml-2">
|
||||
<li className="mr-2 hover:cursor-pointer text-white/70 hover:text-white transition">
|
||||
<Link to="/privacy">隐私政策</Link>
|
||||
</li>
|
||||
<li className="hover:cursor-pointer">
|
||||
<a
|
||||
href="https://github.com/triggerdotdev/jsonhero-web"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon />
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-2 hover:cursor-pointer">
|
||||
<a href="mailto:hello@jsonhero.io">
|
||||
<EmailIcon />
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-2 hover:cursor-pointer">
|
||||
<a href="https://discord.gg/JtBAxBr2m3" target="_blank">
|
||||
<DiscordIcon />
|
||||
</a>
|
||||
</li>
|
||||
<li className="ml-2 hover:cursor-pointer">
|
||||
<a href="https://twitter.com/triggerdotdev" target="_blank">
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
13
jsonhero-web/app/components/Home/HomeGithubBanner.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Body } from "../Primitives/Body";
|
||||
import { GithubStar } from "../UI/GithubStar";
|
||||
|
||||
export function GithubBanner() {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-14 bg-indigo-600">
|
||||
<div className="flex items-center">
|
||||
<Body className="mr-3 text-xl text-white">Star us on GitHub 👉</Body>
|
||||
<GithubStar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
jsonhero-web/app/components/Home/HomeGridFeatureItem.tsx
Normal file
@@ -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 min-h-[44px] text-indigo-700 mb-3" />
|
||||
<Title className={props.titleClassName}>{props.title}</Title>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
jsonhero-web/app/components/Home/HomeHeader.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { DiscordIconTransparent } from "../Icons/DiscordIconTransparent";
|
||||
import { EmailIconTransparent } from "../Icons/EmailIconTransparent";
|
||||
import { TwitterIcon } from "../Icons/TwitterIcon";
|
||||
import { Logo } from "../Icons/Logo";
|
||||
import { NewDocument } from "../NewDocument";
|
||||
import { GithubStar } from "../UI/GithubStar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "../UI/Popover";
|
||||
import TriggerDevLogoImage from "~/assets/images/trigger-dev-logo.png";
|
||||
import { LogoTriggerdotdev } from "../Icons/LogoTriggerdotdev";
|
||||
|
||||
export function HomeHeader({ fixed }: { fixed?: boolean }) {
|
||||
return (
|
||||
<header
|
||||
className={`${
|
||||
fixed ? "fixed" : ""
|
||||
} z-20 flex h-12 justify-center bg-indigo-700 flex-col`}
|
||||
>
|
||||
<div className="flex items-center justify-between w-screen px-4">
|
||||
<div className="flex gap-1 sm:gap-1.5 h-8 justify-center items-center">
|
||||
<div className="w-24 sm:w-32">
|
||||
<Logo />
|
||||
</div>
|
||||
<p className="text-slate-300 text-sm sm:text-base font-sans">by</p>
|
||||
<LogoTriggerdotdev className="pt-0.5 w-16 sm:w-24 opacity-80 hover:opacity-100 transition duration-300" />
|
||||
</div>
|
||||
<ol className="flex items-center gap-2 sm:pr-4">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button className=" bg-lime-400 text-slate-900 text-lg font-bold px-2 py-0.5 rounded uppercase whitespace-nowrap cursor-pointer opacity-90 hover:opacity-100 transition">
|
||||
立即尝试
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" sideOffset={30}>
|
||||
<NewDocument />
|
||||
<PopoverArrow
|
||||
className="fill-current text-indigo-700"
|
||||
offset={20}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<li className="hover:cursor-pointer hidden sm:block">
|
||||
<GithubStar />
|
||||
</li>
|
||||
<li className="hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block">
|
||||
<a href="mailto:hello@jsonhero.io">
|
||||
<EmailIconTransparent />
|
||||
</a>
|
||||
</li>
|
||||
<li className="hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block">
|
||||
<a href="https://discord.gg/JtBAxBr2m3" target="_blank">
|
||||
<DiscordIconTransparent />
|
||||
</a>
|
||||
</li>
|
||||
<li className="hover:cursor-pointer opacity-90 hover:opacity-100 transition hidden sm:block">
|
||||
<a href="https://twitter.com/triggerdotdev" target="_blank">
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
39
jsonhero-web/app/components/Home/HomeHeroSection.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { AutoplayVideo } from "../AutoplayVideo";
|
||||
import { NewFile } from "../NewFile";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
|
||||
import heroVideo from "~/assets/home/JsonHero2.mp4";
|
||||
|
||||
const jsonHeroTitle = "JSON 很糟糕。";
|
||||
const jsonHeroSlogan = "但我们正在让它变得更好。";
|
||||
|
||||
export function HomeHeroSection() {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-stretch flex-col md:flex-row bg-[rgb(56,52,139)] lg:p-6 lg:pb-16 pt-20 lg:pt-32`}
|
||||
>
|
||||
<div className="self-center md:w-1/2 md:pr-10 flex justify-end">
|
||||
<div className=" max-w-3xl">
|
||||
<AutoplayVideo src={heroVideo} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center flex align-center md:w-1/2 px-6 pb-8 mt-8 lg:mt-0">
|
||||
<div className="max-w-lg">
|
||||
<ExtraLargeTitle className="text-lime-300">
|
||||
{jsonHeroTitle}
|
||||
</ExtraLargeTitle>
|
||||
<ExtraLargeTitle className="text-white mb-4">
|
||||
{jsonHeroSlogan}
|
||||
</ExtraLargeTitle>
|
||||
<SmallSubtitle className="text-slate-200 mb-8">
|
||||
别再在编辑器里盯着几千行的 JSON 文件看了,
|
||||
开始使用世界上最好的 JSON 查看器来查看几千行的 JSON 文件吧。
|
||||
它有一些不错的功能,让 JSON 不再那么 <em>糟糕</em>。
|
||||
</SmallSubtitle>
|
||||
<NewFile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
158
jsonhero-web/app/components/Home/HomeInfoBoxSection.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
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, "home");
|
||||
}, [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 w-full md:w-1/2 flex flex-col justify-center items-center py-5">
|
||||
<div className="pointer-events-none absolute z-10 bottom-0 w-full h-[200px] bg-gradient-to-t from-slate-900 to-transparent mb-5"></div>
|
||||
<div className="pointer-events-auto min-w-full max-w-full p-4 rounded-sm bg-slate-900 h-[65vh] overflow-y-auto custom-scrollbar">
|
||||
<div className="pointer-events-none">
|
||||
<div className="mb-4">
|
||||
<PreviewValue/>
|
||||
</div>
|
||||
<PropertiesValue/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
|
||||
function SampleJSONPreview({
|
||||
children,
|
||||
initialSelection,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
initialSelection: string;
|
||||
}) {
|
||||
return (
|
||||
<JsonDocProvider
|
||||
doc={{
|
||||
id: "sample",
|
||||
title: "Sample",
|
||||
type: "raw",
|
||||
readOnly: false,
|
||||
contents: "",
|
||||
}}
|
||||
path={initialSelection}
|
||||
>
|
||||
<JsonProvider initialJson={json}>
|
||||
<JsonColumnViewProvider>{children}</JsonColumnViewProvider>
|
||||
</JsonProvider>
|
||||
</JsonDocProvider>
|
||||
);
|
||||
}
|
||||
26
jsonhero-web/app/components/Home/HomeSearchSection.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AutoplayVideo } from "../AutoplayVideo";
|
||||
import { ExtraLargeTitle } from "../Primitives/ExtraLargeTitle";
|
||||
import { SmallSubtitle } from "../Primitives/SmallSubtitle";
|
||||
import { HomeSection } from "./HomeSection";
|
||||
|
||||
import searchVideo from "~/assets/home/JsonHeroSearch.mp4";
|
||||
|
||||
export function HomeSearchSection() {
|
||||
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">
|
||||
Quickly search your whole JSON file
|
||||
</ExtraLargeTitle>
|
||||
<SmallSubtitle className="mb-6 md:mb-10">
|
||||
Search for absolutely anything in your JSON file with blistering
|
||||
speed. Use the fuzzy matching and keyboard shortcuts to make
|
||||
navigating your files even faster.
|
||||
</SmallSubtitle>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2">
|
||||
<AutoplayVideo src={searchVideo} />
|
||||
</div>
|
||||
</HomeSection>
|
||||
);
|
||||
}
|
||||
28
jsonhero-web/app/components/Home/HomeSection.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
43
jsonhero-web/app/components/Home/HomeSplitSection.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
18
jsonhero-web/app/components/Icons/ArrayIcon.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function ArrayIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
key="array"
|
||||
>
|
||||
<path d="M5.8899 18.525C5.8899 17.2 4.80855 17.025 3.84224 17.025C3.52013 17.025 3.22104 17.05 2.89893 17.05V2.95C4.00329 2.95 5.8899 3.25 5.8899 1.475C5.8899 0.525001 5.19968 0 4.37141 0H2.04766C0.80526 0 0 0.700001 0 2.1V17.925C0 19.35 0.782252 20 2.04766 20H4.37141C5.19968 20 5.8899 19.475 5.8899 18.525Z" fill="currentColor"/>
|
||||
<path d="M20 17.925V2.1C20 0.700001 19.1947 0 17.9523 0H15.6286C14.8003 0 14.1101 0.525001 14.1101 1.475C14.1101 2.825 15.3065 2.975 16.2958 2.975C16.5489 2.975 16.825 2.95 17.1011 2.95V17.05C16.0197 17.05 14.1101 16.8 14.1101 18.525C14.1101 19.525 14.9154 20 15.7436 20H18.0904C19.3098 20 20 19.25 20 17.925Z" fill="currentColor"/>
|
||||
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
33
jsonhero-web/app/components/Icons/ArrowKeysIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
23
jsonhero-web/app/components/Icons/ArrowKeysUpDownIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
export function ArrowKeysUpDownIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
width="28"
|
||||
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.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"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
23
jsonhero-web/app/components/Icons/CopyShortcutIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
export function CopyShortcutIcon(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" />
|
||||
<rect x="16" width="14" height="14" rx="1.53846" fill="currentColor" />
|
||||
<path
|
||||
d="M5.64,10.22H8.25V7a.39.39,0,0,1,.38-.39H10l-3-3L4,6.65H5.26A.39.39,0,0,1,5.64,7v3.18Zm3,.78H5.26a.38.38,0,0,1-.39-.39V7.43H3.11a.39.39,0,0,1-.28-.66L6.72,2.86a.39.39,0,0,1,.55,0l3.88,3.91a.38.38,0,0,1-.27.66H9v3.18a.38.38,0,0,1-.39.39Z"
|
||||
stroke="#0f172a" strokeWidth="0.35px" fill="#0f172a"
|
||||
/>
|
||||
<path
|
||||
d="M23.81,9.52a1.66,1.66,0,0,0,.53-.27,1.57,1.57,0,0,0,.35-.42,1.07,1.07,0,0,0,.13-.53h1.6a2.26,2.26,0,0,1-.25,1,2.87,2.87,0,0,1-.7.85,3.35,3.35,0,0,1-1,.58,3.56,3.56,0,0,1-1.22.21,3.73,3.73,0,0,1-1.55-.31,3.2,3.2,0,0,1-1.11-.84,3.61,3.61,0,0,1-.67-1.22,4.91,4.91,0,0,1-.23-1.5V6.88a4.91,4.91,0,0,1,.23-1.5,3.54,3.54,0,0,1,.67-1.22,3.33,3.33,0,0,1,1.11-.84,3.94,3.94,0,0,1,2.83-.09,3,3,0,0,1,1,.59,2.66,2.66,0,0,1,.67.92,2.94,2.94,0,0,1,.23,1.17h-1.6a1.48,1.48,0,0,0-.45-1.07,1.65,1.65,0,0,0-.52-.33,1.75,1.75,0,0,0-.65-.12,1.59,1.59,0,0,0-1.44.78,2.27,2.27,0,0,0-.31.8,4.53,4.53,0,0,0-.09.91v.24a4.63,4.63,0,0,0,.09.92,2.46,2.46,0,0,0,.3.8,1.67,1.67,0,0,0,.56.56,1.63,1.63,0,0,0,.89.22A2.05,2.05,0,0,0,23.81,9.52Z"
|
||||
fill="#0f172a"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
18
jsonhero-web/app/components/Icons/DiscordIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
19
jsonhero-web/app/components/Icons/DiscordIconTransparent.tsx
Normal file
13
jsonhero-web/app/components/Icons/EmailIcon.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export function EmailIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
|
||||
<svg width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" fill="#4338CA"/>
|
||||
<path d="M12 0C5.37251 0 0 5.37251 0 12C0 18.6275 5.37251 24 12 24C18.6275 24 24 18.6275 24 12C24 5.37251 18.6275 0 12 0ZM4.02864 7.38421C4.29607 6.6387 4.79768 6.14156 5.58914 5.98842C5.87824 5.9324 6.17928 5.91671 6.47473 5.91596C8.32732 5.90961 10.1799 5.91297 12.0329 5.91297C13.9553 5.91297 15.8781 5.90588 17.8005 5.91596C18.8852 5.92156 19.6614 6.44821 19.9732 7.33155C20.1017 7.69534 20.0629 7.93327 19.6685 8.12674C17.3475 9.26668 15.0508 10.4567 12.7134 11.5611C12.3287 11.743 11.709 11.7397 11.3232 11.5574C8.97087 10.4451 6.65774 9.24988 4.32296 8.09948C3.97634 7.92878 3.9136 7.70468 4.02864 7.38384V7.38421ZM20.0935 15.7821C20.089 17.1977 19.2277 18.0788 17.808 18.0825C13.9467 18.0926 10.085 18.0923 6.22373 18.0825C4.83466 18.0792 3.94572 17.2534 3.92032 15.9031C3.88708 14.1223 3.90986 12.3399 3.91397 10.5586C3.91397 10.4279 3.95879 10.2972 3.99502 10.0966C4.23406 10.2019 4.42268 10.2759 4.60346 10.3659C6.91658 11.52 9.23195 12.6708 11.5376 13.8395C11.8685 14.0072 12.1311 14.0083 12.4624 13.8403C14.7677 12.6716 17.0827 11.5212 19.3954 10.367C19.5777 10.2763 19.7671 10.1993 20.058 10.069C20.0741 10.3752 20.0924 10.5635 20.0928 10.7514C20.095 12.428 20.098 14.1051 20.0928 15.7817L20.0935 15.7821Z" fill="white"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
jsonhero-web/app/components/Icons/EmailIconTransparent.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function EmailIconTransparent(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
|
||||
<svg width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0C5.37251 0 0 5.37251 0 12C0 18.6275 5.37251 24 12 24C18.6275 24 24 18.6275 24 12C24 5.37251 18.6275 0 12 0ZM4.02864 7.38421C4.29607 6.6387 4.79768 6.14156 5.58914 5.98842C5.87824 5.9324 6.17928 5.91671 6.47473 5.91596C8.32732 5.90961 10.1799 5.91297 12.0329 5.91297C13.9553 5.91297 15.8781 5.90588 17.8005 5.91596C18.8852 5.92156 19.6614 6.44821 19.9732 7.33155C20.1017 7.69534 20.0629 7.93327 19.6685 8.12674C17.3475 9.26668 15.0508 10.4567 12.7134 11.5611C12.3287 11.743 11.709 11.7397 11.3232 11.5574C8.97087 10.4451 6.65774 9.24988 4.32296 8.09948C3.97634 7.92878 3.9136 7.70468 4.02864 7.38384V7.38421ZM20.0935 15.7821C20.089 17.1977 19.2277 18.0788 17.808 18.0825C13.9467 18.0926 10.085 18.0923 6.22373 18.0825C4.83466 18.0792 3.94572 17.2534 3.92032 15.9031C3.88708 14.1223 3.90986 12.3399 3.91397 10.5586C3.91397 10.4279 3.95879 10.2972 3.99502 10.0966C4.23406 10.2019 4.42268 10.2759 4.60346 10.3659C6.91658 11.52 9.23195 12.6708 11.5376 13.8395C11.8685 14.0072 12.1311 14.0083 12.4624 13.8403C14.7677 12.6716 17.0827 11.5212 19.3954 10.367C19.5777 10.2763 19.7671 10.1993 20.058 10.069C20.0741 10.3752 20.0924 10.5635 20.0928 10.7514C20.095 12.428 20.098 14.1051 20.0928 15.7817L20.0935 15.7821Z" fill="white"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
26
jsonhero-web/app/components/Icons/EscapeKeyIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
18
jsonhero-web/app/components/Icons/GithubIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
30
jsonhero-web/app/components/Icons/GithubIconSimple.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
export type IconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function GithubIconSimple({ className }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_571_3822)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 0C5.37017 0 0 5.50708 0 12.306C0 17.745 3.44015 22.3532 8.20626 23.9849C8.80295 24.0982 9.02394 23.7205 9.02394 23.3881C9.02394 23.0935 9.01657 22.3229 9.00921 21.2956C5.67219 22.0359 4.96501 19.6487 4.96501 19.6487C4.41989 18.2285 3.63168 17.8508 3.63168 17.8508C2.54144 17.0878 3.71271 17.1029 3.71271 17.1029C4.91344 17.1936 5.55433 18.372 5.55433 18.372C6.62247 20.2531 8.36096 19.7092 9.04604 19.3919C9.15654 18.5987 9.46593 18.0548 9.80479 17.745C7.13812 17.4353 4.33886 16.3777 4.33886 11.6638C4.33886 10.3192 4.80295 9.2238 5.57643 8.36261C5.4512 8.05288 5.03867 6.79887 5.69429 5.1067C5.69429 5.1067 6.7035 4.77432 8.99447 6.36827C9.95212 6.09632 10.9761 5.96034 12 5.95279C13.0166 5.95279 14.0479 6.09632 15.0055 6.36827C17.2965 4.77432 18.3057 5.1067 18.3057 5.1067C18.9613 6.79887 18.5488 8.05288 18.4236 8.36261C19.1897 9.2238 19.6538 10.3192 19.6538 11.6638C19.6538 16.3928 16.8471 17.4278 14.1731 17.7375C14.6004 18.1152 14.9908 18.8706 14.9908 20.0189C14.9908 21.6657 14.9761 22.9877 14.9761 23.3957C14.9761 23.728 15.1897 24.1058 15.8011 23.9849C20.5672 22.3532 24 17.745 24 12.3135C24 5.50708 18.6298 0 12 0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_571_3822">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
27
jsonhero-web/app/components/Icons/LoadingIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
export function LoadingIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 26 26"
|
||||
fill="none"
|
||||
>
|
||||
<circle
|
||||
cx="13"
|
||||
cy="13"
|
||||
r="10"
|
||||
stroke="black"
|
||||
strokeOpacity="0.3"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
d="M13 23C7.47715 23 3 18.5228 3 13"
|
||||
stroke="#4338CA"
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
55
jsonhero-web/app/components/Icons/Logo.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Link } from "remix";
|
||||
|
||||
export function Logo({
|
||||
className,
|
||||
width = "100%",
|
||||
}: {
|
||||
className?: string;
|
||||
width?: string;
|
||||
}) {
|
||||
return (
|
||||
<Link to="/" aria-label="JSON Hero homepage" className="w-40">
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height="50"
|
||||
viewBox="0 0 263 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M94.8087 35.3033V1.39929H102.661L111.501 18.2473L114.829 25.7353H115.037C114.898 23.9326 114.707 21.922 114.465 19.7033C114.222 17.4846 114.101 15.37 114.101 13.3593V1.39929H121.381V35.3033H113.529L104.689 18.4033L101.361 11.0193H101.153C101.326 12.8913 101.517 14.902 101.725 17.0513C101.967 19.2006 102.089 21.2806 102.089 23.2913V35.3033H94.8087Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M73.0419 35.9273C69.9912 35.9273 67.3045 35.2166 64.9819 33.7953C62.6939 32.3739 60.8912 30.3459 59.5739 27.7113C58.2912 25.0419 57.6499 21.8699 57.6499 18.1953C57.6499 14.4859 58.2912 11.3486 59.5739 8.78327C60.8912 6.18327 62.6939 4.20727 64.9819 2.85527C67.3045 1.4686 69.9912 0.775269 73.0419 0.775269C76.0925 0.775269 78.7619 1.4686 81.0499 2.85527C83.3725 4.20727 85.1752 6.18327 86.4579 8.78327C87.7752 11.3833 88.4339 14.5206 88.4339 18.1953C88.4339 21.8699 87.7752 25.0419 86.4579 27.7113C85.1752 30.3459 83.3725 32.3739 81.0499 33.7953C78.7619 35.2166 76.0925 35.9273 73.0419 35.9273ZM73.0419 29.3233C75.3645 29.3233 77.2019 28.3179 78.5539 26.3073C79.9059 24.2966 80.5819 21.5926 80.5819 18.1953C80.5819 14.7979 79.9059 12.1459 78.5539 10.2393C77.2019 8.3326 75.3645 7.37927 73.0419 7.37927C70.7192 7.37927 68.8819 8.3326 67.5299 10.2393C66.1779 12.1459 65.5019 14.7979 65.5019 18.1953C65.5019 21.5926 66.1779 24.2966 67.5299 26.3073C68.8819 28.3179 70.7192 29.3233 73.0419 29.3233Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M40.7154 35.9273C38.4967 35.9273 36.278 35.5113 34.0593 34.6793C31.8753 33.8473 29.9167 32.6339 28.1833 31.0393L32.5513 25.7873C33.7647 26.8273 35.1167 27.6766 36.6073 28.3353C38.098 28.9939 39.5367 29.3233 40.9234 29.3233C42.518 29.3233 43.6967 29.0286 44.4594 28.4393C45.2567 27.8499 45.6553 27.0526 45.6553 26.0473C45.6553 24.9726 45.2047 24.1926 44.3034 23.7073C43.4367 23.1873 42.258 22.6153 40.7673 21.9913L36.3474 20.1193C35.2034 19.6339 34.1114 18.9926 33.0714 18.1953C32.0314 17.3633 31.182 16.3406 30.5233 15.1273C29.8647 13.9139 29.5354 12.4926 29.5354 10.8633C29.5354 8.99127 30.038 7.2926 31.0434 5.76727C32.0834 4.24193 33.5047 3.0286 35.3074 2.12727C37.1447 1.22594 39.242 0.775269 41.5993 0.775269C43.5407 0.775269 45.482 1.1566 47.4234 1.91927C49.3647 2.68194 51.0634 3.79127 52.5194 5.24727L48.6194 10.0833C47.51 9.2166 46.4007 8.55794 45.2914 8.10727C44.182 7.62194 42.9514 7.37927 41.5993 7.37927C40.282 7.37927 39.2247 7.6566 38.4273 8.21127C37.6647 8.73127 37.2834 9.4766 37.2834 10.4473C37.2834 11.4873 37.7687 12.2673 38.7393 12.7873C39.7447 13.3073 40.9754 13.8619 42.4314 14.4513L46.7994 16.2193C48.8447 17.0513 50.474 18.1953 51.6874 19.6513C52.9007 21.1073 53.5074 23.0313 53.5074 25.4233C53.5074 27.2953 53.0047 29.0286 51.9994 30.6233C50.994 32.2179 49.538 33.5006 47.6314 34.4713C45.7247 35.4419 43.4194 35.9273 40.7154 35.9273Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M11.6583 35.9273C9.09298 35.9273 6.92631 35.4246 5.15831 34.4193C3.39031 33.3793 1.91698 31.8366 0.738312 29.7913L5.93831 25.9433C6.56231 27.0873 7.29031 27.9366 8.12231 28.4913C8.95431 29.046 9.80365 29.3233 10.6703 29.3233C12.057 29.3233 13.097 28.9073 13.7903 28.0753C14.5183 27.2086 14.8823 25.6486 14.8823 23.3953V1.39929H22.5263V24.0193C22.5263 26.2033 22.145 28.1966 21.3823 29.9993C20.6196 31.802 19.4236 33.2406 17.7943 34.3153C16.1996 35.39 14.1543 35.9273 11.6583 35.9273Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M247.108 35.9273C244.058 35.9273 241.371 35.2166 239.048 33.7953C236.76 32.3739 234.958 30.3459 233.64 27.7113C232.358 25.0419 231.716 21.8699 231.716 18.1953C231.716 14.4859 232.358 11.3486 233.64 8.78327C234.958 6.18327 236.76 4.20727 239.048 2.85527C241.371 1.4686 244.058 0.775269 247.108 0.775269C250.159 0.775269 252.828 1.4686 255.116 2.85527C257.439 4.20727 259.242 6.18327 260.524 8.78327C261.842 11.3833 262.5 14.5206 262.5 18.1953C262.5 21.8699 261.842 25.0419 260.524 27.7113C259.242 30.3459 257.439 32.3739 255.116 33.7953C252.828 35.2166 250.159 35.9273 247.108 35.9273ZM247.108 29.3233C249.431 29.3233 251.268 28.3179 252.62 26.3073C253.972 24.2966 254.648 21.5926 254.648 18.1953C254.648 14.7979 253.972 12.1459 252.62 10.2393C251.268 8.3326 249.431 7.37927 247.108 7.37927C244.786 7.37927 242.948 8.3326 241.596 10.2393C240.244 12.1459 239.568 14.7979 239.568 18.1953C239.568 21.5926 240.244 24.2966 241.596 26.3073C242.948 28.3179 244.786 29.3233 247.108 29.3233Z"
|
||||
fill="#BFF164"
|
||||
/>
|
||||
<path
|
||||
d="M201.438 35.3033V1.39929H213.658C216.05 1.39929 218.234 1.72863 220.21 2.38729C222.186 3.01129 223.763 4.08596 224.942 5.61129C226.12 7.13663 226.71 9.25129 226.71 11.9553C226.71 14.4513 226.155 16.514 225.046 18.1433C223.971 19.738 222.515 20.934 220.678 21.7313L228.374 35.3033H219.794L213.294 23.0833H209.082V35.3033H201.438ZM209.082 16.9993H213.034C215.044 16.9993 216.57 16.5833 217.61 15.7513C218.684 14.8846 219.222 13.6193 219.222 11.9553C219.222 10.2913 218.684 9.12996 217.61 8.47129C216.57 7.81263 215.044 7.48329 213.034 7.48329H209.082V16.9993Z"
|
||||
fill="#BFF164"
|
||||
/>
|
||||
<path
|
||||
d="M172.949 35.3033V1.39929H194.165V7.84729H180.593V14.6593H192.137V21.0553H180.593V28.8553H194.685V35.3033H172.949Z"
|
||||
fill="#BFF164"
|
||||
/>
|
||||
<path
|
||||
d="M137.91 35.3033V1.39929H145.554V14.4513H157.254V1.39929H164.95V35.3033H157.254V21.1593H145.554V35.3033H137.91Z"
|
||||
fill="#BFF164"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
70
jsonhero-web/app/components/Icons/LogoTriggerdotdev.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
export function LogoTriggerdotdev({
|
||||
className,
|
||||
width = "100%",
|
||||
}: {
|
||||
className?: string;
|
||||
width?: string;
|
||||
}) {
|
||||
return (
|
||||
<a href="https://trigger.dev/" aria-label="Trigger.dev">
|
||||
<svg
|
||||
className={`${className}`}
|
||||
width={width}
|
||||
height="30"
|
||||
viewBox="0 0 169 30"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M44.0084 4.04088H30.6671H31.1941V7.67807H35.686V23.329H39.489V7.67807H44.0084V4.04088Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M47.646 11.9215V9.55178H44.0911V23.329H47.646V16.7435C47.646 13.8503 49.9884 13.0236 51.8348 13.2441V9.27623C50.0986 9.27623 48.3625 10.0478 47.646 11.9215Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M55.6379 7.89851C56.8505 7.89851 57.8426 6.90655 57.8426 5.7217C57.8426 4.53686 56.8505 3.51733 55.6379 3.51733C54.453 3.51733 53.4609 4.53686 53.4609 5.7217C53.4609 6.90655 54.453 7.89851 55.6379 7.89851ZM53.8743 23.329H57.4292V9.55178H53.8743V23.329Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M70.9327 9.55179V11.2602C69.9681 9.96509 68.48 9.16603 66.5234 9.16603C62.6103 9.16603 59.6616 12.3623 59.6616 16.22C59.6616 20.1052 62.6103 23.2739 66.5234 23.2739C68.48 23.2739 69.9681 22.4749 70.9327 21.1798V22.6677C70.9327 24.8445 69.5548 26.0569 67.3226 26.0569C65.2007 26.0569 64.2913 25.2027 63.7126 24.1281L60.6812 25.864C61.8938 28.096 64.2637 29.2257 67.2124 29.2257C70.85 29.2257 74.4049 27.1867 74.4049 22.6677V9.55179H70.9327ZM67.0746 19.9949C64.8424 19.9949 63.2165 18.4243 63.2165 16.22C63.2165 14.0432 64.8424 12.4726 67.0746 12.4726C69.3068 12.4726 70.9327 14.0432 70.9327 16.22C70.9327 18.4243 69.3068 19.9949 67.0746 19.9949Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M87.8808 9.55179V11.2602C86.9163 9.96509 85.4282 9.16603 83.4716 9.16603C79.5584 9.16603 76.6097 12.3623 76.6097 16.22C76.6097 20.1052 79.5584 23.2739 83.4716 23.2739C85.4282 23.2739 86.9163 22.4749 87.8808 21.1798V22.6677C87.8808 24.8445 86.5029 26.0569 84.2708 26.0569C82.1488 26.0569 81.2394 25.2027 80.6607 24.1281L77.6294 25.864C78.8419 28.096 81.2119 29.2257 84.1605 29.2257C87.7981 29.2257 91.3531 27.1867 91.3531 22.6677V9.55179H87.8808ZM84.0227 19.9949C81.7906 19.9949 80.1647 18.4243 80.1647 16.22C80.1647 14.0432 81.7906 12.4726 84.0227 12.4726C86.2549 12.4726 87.8808 14.0432 87.8808 16.22C87.8808 18.4243 86.2549 19.9949 84.0227 19.9949Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M97.2782 17.9008H107.667C107.75 17.4324 107.805 16.964 107.805 16.4404C107.805 12.3899 104.912 9.16603 100.833 9.16603C96.5066 9.16603 93.5579 12.3348 93.5579 16.4404C93.5579 20.546 96.479 23.7148 101.109 23.7148C103.754 23.7148 105.821 22.6402 107.116 20.7665L104.25 19.1132C103.644 19.9123 102.542 20.4909 101.164 20.4909C99.2899 20.4909 97.7742 19.7194 97.2782 17.9008ZM97.2231 15.1454C97.6364 13.3819 98.9316 12.3623 100.833 12.3623C102.321 12.3623 103.809 13.1614 104.25 15.1454H97.2231Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M113.468 11.9215V9.55178H109.914V23.329H113.468V16.7435C113.468 13.8503 115.811 13.0236 117.657 13.2441V9.27623C115.921 9.27623 114.185 10.0478 113.468 11.9215Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M119.008 23.6874C120.303 23.6874 121.35 22.6403 121.35 21.3452C121.35 20.0502 120.303 19.0031 119.008 19.0031C117.712 19.0031 116.665 20.0502 116.665 21.3452C116.665 22.6403 117.712 23.6874 119.008 23.6874Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M133.944 4.04102V11.1776C132.952 9.91011 131.491 9.16616 129.479 9.16616C125.787 9.16616 122.755 12.3349 122.755 16.4405C122.755 20.5462 125.787 23.7149 129.479 23.7149C131.491 23.7149 132.952 22.9709 133.944 21.7034V23.3292H137.499V4.04102L133.944 4.04102ZM130.141 20.3257C127.936 20.3257 126.31 18.7551 126.31 16.4405C126.31 14.126 127.936 12.5553 130.141 12.5553C132.318 12.5553 133.944 14.126 133.944 16.4405C133.944 18.7551 132.318 20.3257 130.141 20.3257Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M143.203 17.9009H153.592C153.675 17.4325 153.73 16.9641 153.73 16.4406C153.73 12.39 150.837 9.16617 146.758 9.16617C142.432 9.16617 139.483 12.3349 139.483 16.4406C139.483 20.5462 142.404 23.7149 147.034 23.7149C149.679 23.7149 151.746 22.6403 153.041 20.7666L150.175 19.1133C149.569 19.9124 148.467 20.4911 147.089 20.4911C145.215 20.4911 143.699 19.7195 143.203 17.9009ZM143.148 15.1455C143.561 13.382 144.857 12.3625 146.758 12.3625C148.246 12.3625 149.734 13.1616 150.175 15.1455H143.148Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
d="M164.45 9.55192L161.088 19.196L157.754 9.55192H153.84L159.076 23.3292H163.127L168.363 9.55192H164.45Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.32238 9.89169L13.6403 0.682007L26.8195 23.5069H0.461029L5.77893 14.2969L9.54072 16.4686L7.9849 19.1632H19.2957L13.6403 9.3691L12.0845 12.0637L8.32238 9.89169Z"
|
||||
fill="#E2E8F0"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
29
jsonhero-web/app/components/Icons/MoonIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
18
jsonhero-web/app/components/Icons/ObjectIcon.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function ObjectIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
key="object"
|
||||
>
|
||||
<path d="M8.63302 22C7.03731 21.9857 5.84052 21.4581 5.04267 20.4171C4.24481 19.3761 3.84589 17.836 3.84589 15.7968C3.84589 15.1408 3.75799 14.6132 3.58219 14.2139C3.40639 13.8004 3.12241 13.4724 2.73024 13.2299L2.5274 13.123C2.33807 13.0232 2.20284 12.9091 2.12171 12.7807C2.04057 12.6524 2 12.4884 2 12.2888V11.6898C2 11.4902 2.04057 11.3262 2.12171 11.1979C2.20284 11.0695 2.33807 10.9554 2.5274 10.8556L2.75052 10.7273C3.14269 10.4848 3.41991 10.164 3.58219 9.76471C3.75799 9.36542 3.84589 8.83779 3.84589 8.18182C3.84589 6.1426 4.24481 4.60963 5.04267 3.58289C5.84052 2.54189 7.03055 2.01426 8.61273 2H8.73444C8.85615 2 8.95757 2.04278 9.03871 2.12834C9.13337 2.2139 9.1807 2.32799 9.1807 2.47059V3.86096C9.1807 3.98931 9.14013 4.10339 9.05899 4.20321C8.97785 4.28877 8.87643 4.33155 8.75473 4.33155H8.67359C8.06505 4.37433 7.59175 4.53832 7.25368 4.82353C6.9156 5.10873 6.67895 5.5508 6.54372 6.14973C6.40849 6.7344 6.34087 7.53298 6.34087 8.54546C6.34087 9.40107 6.20564 10.1283 5.93518 10.7273C5.67825 11.3119 5.31313 11.7326 4.83982 11.9893C5.31313 12.2317 5.67825 12.6595 5.93518 13.2727C6.20564 13.8717 6.34087 14.5918 6.34087 15.4332C6.34087 16.4456 6.40849 17.2513 6.54372 17.8503C6.67895 18.4349 6.9156 18.8699 7.25368 19.1551C7.59175 19.4545 8.05829 19.6185 8.6533 19.6471H8.75473C8.87643 19.6613 8.97785 19.7112 9.05899 19.7968C9.14013 19.8824 9.1807 19.9893 9.1807 20.1176V21.5294C9.1807 21.6578 9.13337 21.7647 9.03871 21.8503C8.95757 21.9501 8.85615 22 8.73444 22H8.63302Z" fill="currentColor"/>
|
||||
<path d="M15.367 2C16.9627 2.01426 18.1595 2.54189 18.9573 3.58289C19.7552 4.62389 20.1541 6.16399 20.1541 8.20321C20.1541 8.85918 20.242 9.39394 20.4178 9.80749C20.5936 10.2068 20.8776 10.5205 21.2698 10.7487L21.4726 10.8556C21.6619 10.9697 21.7972 11.0909 21.8783 11.2193C21.9594 11.3476 22 11.5045 22 11.6898V12.3102C22 12.4955 21.9594 12.6524 21.8783 12.7807C21.7972 12.9091 21.6619 13.0303 21.4726 13.1444L21.2495 13.2513C20.8708 13.4938 20.5936 13.8217 20.4178 14.2353C20.242 14.6346 20.1541 15.1622 20.1541 15.8182C20.1541 19.8966 18.5652 21.9572 15.3873 22H15.2656C15.1439 22 15.0357 21.9501 14.941 21.8503C14.8599 21.7647 14.8193 21.6578 14.8193 21.5294V20.1176C14.8193 19.9893 14.8599 19.8824 14.941 19.7968C15.0221 19.7112 15.1236 19.6613 15.2453 19.6471H15.3264C15.9349 19.6185 16.4083 19.4617 16.7463 19.1765C17.0844 18.8913 17.3211 18.4563 17.4563 17.8717C17.5915 17.2727 17.6591 16.467 17.6591 15.4545C17.6591 14.5847 17.7876 13.8574 18.0445 13.2727C18.315 12.6881 18.6869 12.2674 19.1602 12.0107C18.6869 11.754 18.315 11.3262 18.0445 10.7273C17.7876 10.1141 17.6591 9.38681 17.6591 8.54546C17.6591 7.53298 17.5915 6.7344 17.4563 6.14973C17.3211 5.5508 17.0844 5.10873 16.7463 4.82353C16.4083 4.53832 15.9417 4.38146 15.3467 4.35294L15.2453 4.33155C15.1371 4.33155 15.0357 4.28877 14.941 4.20321C14.8599 4.10339 14.8193 3.98931 14.8193 3.86096V2.47059C14.8193 2.32799 14.8599 2.2139 14.941 2.12834C15.0357 2.04278 15.1439 2 15.2656 2H15.367Z" fill="currentColor"/>
|
||||
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
14
jsonhero-web/app/components/Icons/ShortcutIcon.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export type ShortcutIconProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ShortcutIcon({ className, children }: ShortcutIconProps) {
|
||||
return (
|
||||
<span
|
||||
className={`flex items-center justify-center rounded ${className ?? ""}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
20
jsonhero-web/app/components/Icons/SquareBracketsIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
17
jsonhero-web/app/components/Icons/StringIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
72
jsonhero-web/app/components/Icons/SunIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
17
jsonhero-web/app/components/Icons/TreeIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
18
jsonhero-web/app/components/Icons/TwitterIcon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
37
jsonhero-web/app/components/IndentPreference.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { usePreferences } from "~/components/PreferencesProvider";
|
||||
|
||||
const MIN_INDENT = 1;
|
||||
const MAX_INDENT = 8;
|
||||
|
||||
export function IndentPreference() {
|
||||
const [preferences, setPreferences] = usePreferences();
|
||||
|
||||
const updatePreferences = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newIdent = Number(e.target.value);
|
||||
if (newIdent < MIN_INDENT) newIdent = MIN_INDENT;
|
||||
if (newIdent > MAX_INDENT) newIdent = MAX_INDENT;
|
||||
e.target.value = newIdent.toString();
|
||||
setPreferences({ ...preferences, indent: newIdent });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center -mt-0.5">
|
||||
<label
|
||||
className="pr-2 text-slate-800 transition dark:text-white"
|
||||
htmlFor="indent"
|
||||
>
|
||||
<Body>Indent</Body>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="py-0 pr-0 pl-1 w-9 rounded-sm text-sm h-[23px] bg-slate-300 transition hover:bg-slate-400 hover:bg-opacity-50 dark:bg-slate-800 dark:text-slate-400 hover:cursor-pointer hover:dark:bg-slate-700 hover:dark:bg-opacity-70"
|
||||
defaultValue={preferences?.indent}
|
||||
min={MIN_INDENT}
|
||||
max={MAX_INDENT}
|
||||
onChange={updatePreferences}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
jsonhero-web/app/components/InfoHeader.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { inferType } from "@jsonhero/json-infer-types";
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useJson } from "~/hooks/useJson";
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "~/hooks/useJsonColumnView";
|
||||
import { concatenated, getHierarchicalTypes } from "~/utilities/dataType";
|
||||
import { formatRawValue } from "~/utilities/formatter";
|
||||
import { isNullable } from "~/utilities/nullable";
|
||||
import { CopyTextButton } from "./CopyTextButton";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { LargeMono } from "./Primitives/LargeMono";
|
||||
import { Title } from "./Primitives/Title";
|
||||
import { ValueIcon, ValueIconSize } from "./ValueIcon";
|
||||
|
||||
export type InfoHeaderProps = {
|
||||
relatedPaths: string[];
|
||||
};
|
||||
|
||||
export function InfoHeader({ relatedPaths }: InfoHeaderProps) {
|
||||
const { selectedNodeId, highlightedNodeId, selectedNodes } =
|
||||
useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
if (!selectedNodeId || !highlightedNodeId || selectedNodes.length === 0) {
|
||||
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 formattedSelectedInfo = formatRawValue(selectedInfo);
|
||||
const selectedName = selectedNode.longTitle ?? selectedNode.title;
|
||||
|
||||
const isSelectedLeafNode =
|
||||
selectedInfo.name !== "object" && selectedInfo.name !== "array";
|
||||
|
||||
const canBeNull = useMemo(() => {
|
||||
return isNullable(relatedPaths, json);
|
||||
}, [relatedPaths, json]);
|
||||
|
||||
const [hovering, setHovering] = useState(false);
|
||||
console.warn(selectedInfo);
|
||||
|
||||
const newPath = formattedSelectedInfo.replace(/^#/, "$").replace(/\//g, ".");
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
goToNodeId(newPath, "pathBar");
|
||||
}, [newPath, goToNodeId]);
|
||||
|
||||
return (
|
||||
<div className="mb-4 pb-4">
|
||||
<div className="flex items-center">
|
||||
<Title className="flex-1 mr-2 overflow-hidden overflow-ellipsis break-words text-slate-700 transition dark:text-slate-200">
|
||||
{ selectedName ?? "nothing" }
|
||||
</Title>
|
||||
<div>
|
||||
<ValueIcon
|
||||
monochrome
|
||||
type={selectedInfo}
|
||||
size={ValueIconSize.Medium}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="relative w-full h-full"
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
{isSelectedLeafNode && (
|
||||
<LargeMono
|
||||
className={`z-10 py-1 mb-1 text-slate-800 overflow-ellipsis break-words transition rounded-sm dark:text-slate-300 ${
|
||||
hovering ? "bg-slate-100 dark:bg-slate-700" : "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
{selectedNode.name === "$ref" && checkPathExists(json, newPath) ? (
|
||||
<button onClick={handleClick}>
|
||||
{formatRawValue(selectedInfo)}
|
||||
</button>
|
||||
) : (
|
||||
formatRawValue(selectedInfo)
|
||||
)}
|
||||
</LargeMono>
|
||||
)}
|
||||
<div
|
||||
className={`absolute top-1 right-0 flex justify-end h-full w-fit transition ${
|
||||
hovering ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CopyTextButton
|
||||
className="bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-600 dark:hover:bg-slate-500"
|
||||
value={formatRawValue(selectedInfo)}
|
||||
></CopyTextButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex text-gray-400">
|
||||
<Body className="flex-1">
|
||||
{concatenated(getHierarchicalTypes(selectedInfo))}
|
||||
</Body>
|
||||
{canBeNull && <Body>Can be null</Body>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function checkPathExists(json: unknown, newPath: string) {
|
||||
const heroPath = new JSONHeroPath(newPath);
|
||||
const node = heroPath.first(json);
|
||||
return Boolean(node);
|
||||
}
|
||||
|
||||
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-slate-800 mb-1 overflow-ellipsis break-words dark:text-slate-300">
|
||||
null
|
||||
</Title>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
jsonhero-web/app/components/InfoPanel.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
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";
|
||||
import { useRelatedPaths } from "~/hooks/useRelatedPaths";
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
|
||||
export function InfoPanel() {
|
||||
const { minimal } = useJsonDoc();
|
||||
const selectedInfo = useSelectedInfo();
|
||||
const relatedPaths = useRelatedPaths();
|
||||
|
||||
if (!selectedInfo) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${
|
||||
minimal ? "h-inspectorHeightMinimal" : "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 relatedPaths={relatedPaths} />
|
||||
|
||||
<div className="mb-4">
|
||||
<PreviewValue />
|
||||
</div>
|
||||
<PropertiesValue />
|
||||
|
||||
<ContainerInfo />
|
||||
|
||||
<RelatedValues relatedPaths={relatedPaths} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
74
jsonhero-web/app/components/JsonColumnView.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "../hooks/useJsonColumnView";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Columns } from "./Columns";
|
||||
import { CopySelectedNodeShortcut } from "./CopySelectedNode";
|
||||
|
||||
export function JsonColumnView() {
|
||||
const { getColumnViewProps, columns } = useJsonColumnViewState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<KeyboardShortcuts />
|
||||
<div {...getColumnViewProps()}>
|
||||
<Columns columns={columns} />
|
||||
</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 <>
|
||||
<CopySelectedNodeShortcut />
|
||||
</>;
|
||||
}
|
||||
91
jsonhero-web/app/components/JsonEditor.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
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";
|
||||
import {usePreferences} from '~/components/PreferencesProvider'
|
||||
|
||||
export function JsonEditor() {
|
||||
const [json] = useJson();
|
||||
const { selectedNodeId } = useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
const [preferences] = usePreferences();
|
||||
|
||||
const jsonMapped = useMemo(() => {
|
||||
return jsonMap.stringify(json, null, preferences?.indent || 2);
|
||||
}, [json, preferences]);
|
||||
|
||||
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(), "editor");
|
||||
},
|
||||
[goToNodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
language="json"
|
||||
content={jsonMapped.json}
|
||||
readOnly={true}
|
||||
onUpdate={onUpdate}
|
||||
selection={selection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
186
jsonhero-web/app/components/JsonPreview.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
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, useState } from "react";
|
||||
import { getPreviewSetup } from "~/utilities/codeMirrorSetup";
|
||||
import { lightTheme, darkTheme } from "~/utilities/codeMirrorTheme";
|
||||
import { CopyTextButton } from "./CopyTextButton";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import {usePreferences} from '~/components/PreferencesProvider';
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
export type JsonPreviewProps = {
|
||||
json: unknown;
|
||||
highlightPath?: string;
|
||||
};
|
||||
|
||||
export function JsonPreview({ json, highlightPath }: JsonPreviewProps) {
|
||||
const editor = useRef(null);
|
||||
const [preferences] = usePreferences();
|
||||
|
||||
const jsonMapped = useMemo(() => {
|
||||
return jsonMap.stringify(json, null, preferences?.indent || 2);
|
||||
}, [json, preferences]);
|
||||
|
||||
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, state } = 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]);
|
||||
|
||||
useHotkeys(
|
||||
"ctrl+a,meta+a,command+a",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
view?.dispatch({ selection: { anchor: 0, head: state?.doc.length } });
|
||||
},
|
||||
[view, state]
|
||||
);
|
||||
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full"
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
<div ref={editor} />
|
||||
<div
|
||||
className={`absolute top-1 right-0 flex justify-end w-full transition ${
|
||||
hovering ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CopyTextButton
|
||||
value={jsonMapped.json}
|
||||
className="bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
></CopyTextButton>
|
||||
</div>
|
||||
</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,
|
||||
}
|
||||
);
|
||||
54
jsonhero-web/app/components/JsonSchemaViewer.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { JSONHeroPath } from "@jsonhero/path";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useJsonSchema } from "~/hooks/useJsonSchema";
|
||||
import { CodeViewer } from "./CodeViewer";
|
||||
import { CopyTextButton } from "./CopyTextButton";
|
||||
import {usePreferences} from '~/components/PreferencesProvider'
|
||||
|
||||
export function JsonSchemaViewer({ path }: { path: string }) {
|
||||
const schema = useJsonSchema();
|
||||
const schemaPath = schemaPathFromPath(path);
|
||||
const schemaJson = schemaPath.first(schema);
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const [preferences] = usePreferences();
|
||||
|
||||
const code = useMemo(() => {
|
||||
return JSON.stringify(schemaJson, null, preferences?.indent || 2);
|
||||
}, [schemaJson, preferences]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full"
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
<CodeViewer code={code} lang="json" />
|
||||
<div
|
||||
className={`absolute top-1 right-0 flex justify-end w-full transition ${
|
||||
hovering ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CopyTextButton
|
||||
value={code}
|
||||
className="bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
></CopyTextButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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("$"));
|
||||
}
|
||||
250
jsonhero-web/app/components/JsonTreeView.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
useJsonColumnViewAPI,
|
||||
useJsonColumnViewState,
|
||||
} from "~/hooks/useJsonColumnView";
|
||||
import { useJsonDoc } from "~/hooks/useJsonDoc";
|
||||
import { JsonTreeViewNode, useJsonTreeViewContext } from "~/hooks/useJsonTree";
|
||||
import { VirtualNode } from "~/hooks/useVirtualTree";
|
||||
import { CopySelectedNodeShortcut } from "./CopySelectedNode";
|
||||
import { Body } from "./Primitives/Body";
|
||||
import { Mono } from "./Primitives/Mono";
|
||||
|
||||
export function JsonTreeView() {
|
||||
const { selectedNodeId, selectedNodeSource } = useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
const { tree, parentRef } = useJsonTreeViewContext();
|
||||
|
||||
// Scroll to the selected node when this component is first rendered.
|
||||
const scrolledToNodeRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrolledToNodeRef.current && selectedNodeId) {
|
||||
tree.scrollToNode(selectedNodeId);
|
||||
scrolledToNodeRef.current = true;
|
||||
}
|
||||
}, [selectedNodeId, scrolledToNodeRef]);
|
||||
|
||||
// Yup, this is hacky.
|
||||
// This is to prevent the selection not changing the first time you try to move to a new node in the tree
|
||||
const focusCount = useRef<number>(0);
|
||||
|
||||
// This focuses and scrolls to the selected node when the selectedNodeId
|
||||
// is set from a source other than this tree (e.g. the search bar, path bar, related values).
|
||||
useEffect(() => {
|
||||
if (
|
||||
tree.focusedNodeId &&
|
||||
selectedNodeId &&
|
||||
tree.focusedNodeId !== selectedNodeId
|
||||
) {
|
||||
if (selectedNodeId === "$") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedNodeSource !== "tree" && focusCount.current > 0) {
|
||||
focusCount.current = focusCount.current + 1;
|
||||
tree.focusNode(selectedNodeId);
|
||||
tree.scrollToNode(selectedNodeId);
|
||||
}
|
||||
}
|
||||
}, [tree.focusedNodeId, goToNodeId, selectedNodeId, selectedNodeSource]);
|
||||
|
||||
// This is what syncs the tree view's focused node to the column view selected node
|
||||
const previousFocusedNodeId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let updated = false;
|
||||
|
||||
if (!previousFocusedNodeId.current) {
|
||||
previousFocusedNodeId.current = tree.focusedNodeId;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (
|
||||
tree.focusedNodeId &&
|
||||
(updated || previousFocusedNodeId.current !== tree.focusedNodeId)
|
||||
) {
|
||||
previousFocusedNodeId.current = tree.focusedNodeId;
|
||||
goToNodeId(tree.focusedNodeId, "tree");
|
||||
}
|
||||
}, [previousFocusedNodeId, tree.focusedNodeId, tree.focusNode, goToNodeId]);
|
||||
|
||||
const treeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (treeRef.current) {
|
||||
treeRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
}, [treeRef.current]);
|
||||
|
||||
const { minimal } = useJsonDoc();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CopySelectedNodeShortcut />
|
||||
<div
|
||||
className="text-white w-full"
|
||||
ref={parentRef}
|
||||
style={{
|
||||
height: `calc(100vh - ${minimal ? "66px" : "106px"})`,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full outline-none"
|
||||
style={{ height: `${tree.totalSize}px` }}
|
||||
{...tree.getTreeProps()}
|
||||
ref={treeRef}
|
||||
>
|
||||
{tree.nodes.map((virtualNode) => (
|
||||
<TreeViewNode
|
||||
virtualNode={virtualNode}
|
||||
key={virtualNode.node.id}
|
||||
onToggle={(node, e) => tree.toggleNode(node.id, e)}
|
||||
selectedNodeId={selectedNodeId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeViewNode({
|
||||
virtualNode,
|
||||
onToggle,
|
||||
selectedNodeId,
|
||||
}: {
|
||||
virtualNode: VirtualNode<JsonTreeViewNode>;
|
||||
selectedNodeId?: string;
|
||||
onToggle?: (node: JsonTreeViewNode, e: MouseEvent) => void;
|
||||
}) {
|
||||
const { node, virtualItem, depth } = virtualNode;
|
||||
|
||||
const indentClassName = computeTreeNodePaddingClass(depth);
|
||||
|
||||
const isSelected = selectedNodeId === node.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${virtualNode.size}px`,
|
||||
transform: `translateY(${virtualNode.start}px)`,
|
||||
}}
|
||||
key={virtualNode.node.id}
|
||||
{...virtualNode.getItemProps()}
|
||||
>
|
||||
<div
|
||||
className={`h-full flex pl-5 rounded-sm select-none ${
|
||||
isSelected
|
||||
? "bg-indigo-700"
|
||||
: virtualItem.index % 2
|
||||
? "dark:bg-slate-900"
|
||||
: "bg-slate-100 bg-opacity-90 dark:bg-slate-800 dark:bg-opacity-30"
|
||||
}`}
|
||||
>
|
||||
<div className={`pl-2 w-2/6 items-center flex`}>
|
||||
{node.children && node.children.length > 0 && (
|
||||
<span
|
||||
onClick={(e) => {
|
||||
if (onToggle) {
|
||||
e.preventDefault();
|
||||
onToggle(node, e.nativeEvent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{virtualNode.isCollapsed ? (
|
||||
<ChevronRightIcon
|
||||
className={`w-4 h-4 mr-1 -ml-5 ${
|
||||
isSelected
|
||||
? "text-slate-100"
|
||||
: "text-slate-600 dark:text-slate-100"
|
||||
}`}
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className={`w-4 h-4 mr-1 -ml-5 ${
|
||||
isSelected
|
||||
? "text-slate-100"
|
||||
: "text-slate-600 dark:text-slate-100"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Body
|
||||
className={`${indentClassName} leading-8 truncate whitespace-nowrap pl-2 pr-2 ${
|
||||
isSelected
|
||||
? "text-slate-100"
|
||||
: "text-slate-700 dark:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
{node.longTitle ?? node.name}
|
||||
</Body>
|
||||
</div>
|
||||
|
||||
<div className="flex w-4/6 items-center">
|
||||
<span className="mr-2">
|
||||
{node.icon && (
|
||||
<node.icon
|
||||
className={`h-5 w-5 ${
|
||||
isSelected
|
||||
? "text-slate-100"
|
||||
: "text-slate-400 dark:text-slate-500"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{node.subtitle && (
|
||||
<Mono
|
||||
className={`truncate pr-1 transition ${
|
||||
isSelected
|
||||
? "text-slate-100"
|
||||
: "text-slate-500 dark:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
{node.subtitle}
|
||||
</Mono>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function computeTreeNodePaddingClass(depth: number) {
|
||||
switch (depth) {
|
||||
case 0:
|
||||
return "ml-[4px] border-l border-slate-400/70";
|
||||
case 1:
|
||||
return "ml-[calc(12px_+_4px)] border-l border-pink-400/70";
|
||||
case 2:
|
||||
return "ml-[calc(12px_*_2_+_4px)] border-l border-blue-400/70";
|
||||
case 3:
|
||||
return "ml-[calc(12px_*_3_+_4px)] border-l border-orange-400/70";
|
||||
case 4:
|
||||
return "ml-[calc(12px_*_4_+_4px)] border-l border-emerald-400/70";
|
||||
case 5:
|
||||
return "ml-[calc(12px_*_5_+_4px)] border-l border-pink-400/70";
|
||||
case 6:
|
||||
return "ml-[calc(12px_*_6_+_4px)] border-l border-blue-400/70";
|
||||
case 7:
|
||||
return "ml-[calc(12px_*_7_+_4px)] border-l border-orange-400/70";
|
||||
case 8:
|
||||
return "ml-[calc(12px_*_8_+_4px)] border-l border-emerald-400/70";
|
||||
case 9:
|
||||
return "ml-[calc(12px_*_9_+_4px)] border-l border-pink-400/70";
|
||||
case 10:
|
||||
return "ml-[calc(12px_*_10_+_4px)] border-l border-orange-400/70";
|
||||
default:
|
||||
return "ml-[calc(12px_*_11_+_4px)] border-l border-slate-400/70";
|
||||
}
|
||||
}
|
||||
21
jsonhero-web/app/components/JsonView.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { PathBar, PathHistoryControls } from "./PathBar";
|
||||
import { SearchBar } from "./SearchBar";
|
||||
|
||||
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="flex justify-between p-1 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>
|
||||
<SearchBar />
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
jsonhero-web/app/components/NewDocument.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DragAndDropForm } from "./DragAndDropForm";
|
||||
import { Title } from "./Primitives/Title";
|
||||
import { SampleUrls } from "./SampleUrls";
|
||||
import { UrlForm } from "./UrlForm";
|
||||
|
||||
export function NewDocument() {
|
||||
return (
|
||||
<div className="bg-indigo-700 text-white rounded-sm shadow-md w-96 max-w-max p-3 transition">
|
||||
<div className="flex flex-col">
|
||||
<UrlForm className="mb-2" />
|
||||
<DragAndDropForm />
|
||||
|
||||
<div className="mt-4">
|
||||
<Title className="mb-2 text-slate-200">没有 JSON?试试这些示例:</Title>
|
||||
<SampleUrls />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
jsonhero-web/app/components/NewFile.tsx
Normal file
@@ -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="mb-2 text-slate-200">No JSON? Try it out:</Title>
|
||||
<SampleUrls />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
jsonhero-web/app/components/OpenInWindow.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
export type OpenInNewWindowProps = {
|
||||
children?: React.ReactNode;
|
||||
url?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function OpenInNewWindow({
|
||||
url,
|
||||
className,
|
||||
children,
|
||||
}: OpenInNewWindowProps) {
|
||||
return (
|
||||
<a href={url} target="_blank" className={`${className} relative`}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
222
jsonhero-web/app/components/PathBar.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
PencilAltIcon,
|
||||
CheckIcon,
|
||||
} 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";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { useJson } from '~/hooks/useJson';
|
||||
import { JSONHeroPath } from '@jsonhero/path';
|
||||
|
||||
export function PathBar() {
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const { selectedNodes, highlightedNodeId } = useJsonColumnViewState();
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
const [json] = useJson();
|
||||
|
||||
if (isEditable) {
|
||||
return (
|
||||
<PathBarText
|
||||
selectedNodes={selectedNodes}
|
||||
onConfirm={(newPath) => {
|
||||
setIsEditable(false);
|
||||
const heroPath = new JSONHeroPath(newPath);
|
||||
const node = heroPath.first(json);
|
||||
if (node) {
|
||||
goToNodeId(newPath, 'pathBar');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PathBarLink
|
||||
selectedNodes={selectedNodes}
|
||||
highlightedNodeId={highlightedNodeId}
|
||||
enableEdit={() => setIsEditable(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function PathBarText({ selectedNodes, onConfirm }: { selectedNodes: ColumnViewNode[], onConfirm: (newPath: string) => void; }) {
|
||||
const [path, setPath] = useState('');
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setPath(selectedNodes.at(-1)?.id || '');
|
||||
}, [selectedNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
onConfirm(path);
|
||||
e.preventDefault();
|
||||
}}
|
||||
// onBlur={() => onConfirm(path)}
|
||||
className="flex overflow-x-hidden items-center bg-slate-300 dark:bg-slate-700 rounded-sm"
|
||||
>
|
||||
<label className="grow">
|
||||
<input
|
||||
ref={ref}
|
||||
className={
|
||||
"w-full border-none outline-none text-ellipsis text-base px-2 py-0 rounded-sm bg-transparent dark:text-slate-200"
|
||||
}
|
||||
style={{ boxShadow: 'none' }}
|
||||
type="text"
|
||||
name="title"
|
||||
spellCheck="false"
|
||||
placeholder="Name your JSON file"
|
||||
value={path}
|
||||
onChange={(e) => setPath(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="flex ml-auto justify-center items-center w-[26px] h-[26px] hover:bg-slate-400 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[10%]">
|
||||
<CheckIcon className='h-5' />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export type PathBarLinkProps = {
|
||||
selectedNodes: ColumnViewNode[];
|
||||
highlightedNodeId?: string;
|
||||
enableEdit: () => void;
|
||||
};
|
||||
|
||||
export function PathBarLink({
|
||||
selectedNodes,
|
||||
highlightedNodeId,
|
||||
enableEdit,
|
||||
}: PathBarLinkProps) {
|
||||
const { goToNodeId } = useJsonColumnViewAPI();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-shrink-0 flex-grow-0 overflow-x-hidden"
|
||||
onClick={(event) => {
|
||||
if (event.detail == 2) {
|
||||
enableEdit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedNodes.map((node, index) => {
|
||||
return (
|
||||
<PathBarItem
|
||||
key={index}
|
||||
node={node}
|
||||
isHighlighted={highlightedNodeId === node.id}
|
||||
onClick={(id) => goToNodeId(id, "pathBar")}
|
||||
isLast={index == selectedNodes.length - 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
className="flex ml-auto justify-center items-center w-[26px] h-[26px] hover:bg-slate-300 dark:text-slate-400 dark:hover:bg-white dark:hover:bg-opacity-[10%]"
|
||||
onClick={enableEdit}>
|
||||
<PencilAltIcon className='h-5' />
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function PathBarElement({
|
||||
node,
|
||||
isHighlighted,
|
||||
onClick,
|
||||
isLast,
|
||||
}: {
|
||||
node: ColumnViewNode;
|
||||
isHighlighted: boolean;
|
||||
onClick?: (id: string) => void;
|
||||
isLast: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center min-w-0"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center hover:cursor-pointer min-w-0 transition ${isHighlighted
|
||||
? "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={() => onClick && onClick(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>
|
||||
|
||||
{isLast ? (
|
||||
<></>
|
||||
) : (
|
||||
<ChevronRightIcon className="flex-grow-0 flex-shrink-[0.5] w-4 h-4 text-slate-400 whitespace-nowrap overflow-x-hidden" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PathBarItem = memo(PathBarElement);
|
||||
132
jsonhero-web/app/components/PathPreview.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
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";
|
||||
|
||||
import eyeIcon from "~/assets/svgs/EyeIcon.svg";
|
||||
|
||||
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('${eyeIcon}')] after:bg-no-repeat`
|
||||
: "disabled"
|
||||
}`}
|
||||
onClick={() =>
|
||||
isEnabled &&
|
||||
goToNodeId(components[components.length - 1].id, "relatedValues")
|
||||
}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
58
jsonhero-web/app/components/PreferencesProvider.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import {createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState} from 'react';
|
||||
|
||||
interface Preferences {
|
||||
indent: number;
|
||||
}
|
||||
|
||||
const PreferencesDefaults: Preferences = {
|
||||
indent: 2,
|
||||
};
|
||||
|
||||
type PreferencesContextType = [
|
||||
Preferences | undefined,
|
||||
Dispatch<SetStateAction<Preferences | undefined>>
|
||||
];
|
||||
|
||||
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
|
||||
|
||||
const loadPreferences = (): Preferences => {
|
||||
const savedPreferences = localStorage.getItem('preferences');
|
||||
const parsedPreferences = JSON.parse(savedPreferences || '{}');
|
||||
for (const [key, value] of Object.entries(PreferencesDefaults)) {
|
||||
if (!parsedPreferences[key]) parsedPreferences[key] = value;
|
||||
}
|
||||
return parsedPreferences;
|
||||
};
|
||||
const savePreferences = (preferences: Preferences) => localStorage.setItem('preferences', JSON.stringify(preferences));
|
||||
|
||||
export function PreferencesProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [preferences, setPreferences] = useState<Preferences>();
|
||||
|
||||
useEffect(() => {
|
||||
const preferences = loadPreferences();
|
||||
setPreferences(preferences);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (preferences === undefined) return;
|
||||
savePreferences(preferences);
|
||||
}, [preferences]);
|
||||
|
||||
return (
|
||||
<PreferencesContext.Provider value={[preferences, setPreferences]}>
|
||||
{children}
|
||||
</PreferencesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePreferences() {
|
||||
const context = useContext(PreferencesContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('usePreferences must be used within a PreferencesProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
138
jsonhero-web/app/components/Preview/CalendarMonth.tsx
Normal file
@@ -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(" ");
|
||||
}
|
||||
29
jsonhero-web/app/components/Preview/PreviewBox.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useCallback } from "react";
|
||||
import { Title } from "../Primitives/Title";
|
||||
|
||||
export type PreviewBoxProps = {
|
||||
link?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PreviewBox({ link, className, children }: PreviewBoxProps) {
|
||||
const onClick = useCallback(() => {
|
||||
if (!link) return;
|
||||
window.open(link, "_blank");
|
||||
}, [link]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Title className="text-slate-700 transition dark:text-slate-400 mb-2">
|
||||
Preview
|
||||
</Title>
|
||||
<div
|
||||
onClick={onClick}
|
||||
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:cursor-pointer"
|
||||
>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
jsonhero-web/app/components/Preview/PreviewProperties.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
17
jsonhero-web/app/components/Preview/PreviewValue.tsx
Normal file
@@ -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,42 @@
|
||||
import { useRef } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Body } from "~/components/Primitives/Body";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
|
||||
export function PreviewAudioUri({
|
||||
src,
|
||||
contentType,
|
||||
}: {
|
||||
src: string;
|
||||
contentType: string;
|
||||
}) {
|
||||
const mediaRef = useRef<HTMLMediaElement>(null);
|
||||
|
||||
useHotkeys(
|
||||
"space",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (mediaRef.current) {
|
||||
if (mediaRef.current.paused) {
|
||||
mediaRef.current.play();
|
||||
} else {
|
||||
mediaRef.current.pause();
|
||||
}
|
||||
}
|
||||
},
|
||||
[mediaRef]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PreviewBox>
|
||||
<Body>
|
||||
<audio controls src={src} ref={mediaRef}>
|
||||
Sorry, your browser doesn't support embedded audio.
|
||||
</audio>
|
||||
</Body>
|
||||
</PreviewBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
jsonhero-web/app/components/Preview/Types/PreviewDate.tsx
Normal file
@@ -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 <></>;
|
||||
}
|
||||
}
|
||||
31
jsonhero-web/app/components/Preview/Types/PreviewHtml.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Body } from "~/components/Primitives/Body";
|
||||
import { Title } from "~/components/Primitives/Title";
|
||||
import { PreviewBox } from "../PreviewBox";
|
||||
import { PreviewHtml } from "./preview.types";
|
||||
|
||||
export type PreviewHtmlProps = {
|
||||
info: PreviewHtml;
|
||||
};
|
||||
|
||||
export function PreviewHtml({ info }: PreviewHtmlProps) {
|
||||
return (
|
||||
<PreviewBox link={info.url}>
|
||||
<div>
|
||||
{info.title && (
|
||||
<Title>
|
||||
{info.icon && (
|
||||
<img src={info.icon.url} className="w-4 h-4 inline mr-1" alt="" />
|
||||
)}
|
||||
<span className="inline">{info.title}</span>
|
||||
</Title>
|
||||
)}
|
||||
{info.description && <Body>{info.description}</Body>}
|
||||
</div>
|
||||
{info.image && (
|
||||
<div>
|
||||
<img className="block" src={info.image?.url} alt={info.image?.alt} />
|
||||
</div>
|
||||
)}
|
||||
</PreviewBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PreviewImageUri } from "./PreviewImageUri";
|
||||
|
||||
export function PreviewIPFSImage({ src }: { src: URL }) {
|
||||
const newSrc = createPreviewIPFSImageURL(src);
|
||||
|
||||
return <PreviewImageUri src={newSrc} />;
|
||||
}
|
||||
|
||||
const createPreviewIPFSImageURL = (src: URL): string => {
|
||||
const url = new URL(src.href);
|
||||
|
||||
url.protocol = "https:";
|
||||
url.pathname = `/ipfs/${src.pathname.substring(1)}`;
|
||||
url.hostname = "ipfs.io";
|
||||
// Remove the leading slash
|
||||
|
||||
return url.href;
|
||||
};
|
||||