Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add submit request functionality #49

Merged
merged 22 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
904 changes: 688 additions & 216 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.3.2",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/state": "^6.3.3",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.22.3",
"@hookform/resolvers": "^3.3.2",
"@lit/react": "^1.0.2",
"@material/web": "^1.0.1",
Expand Down
17 changes: 12 additions & 5 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { RouterProvider } from 'react-router-dom';

import router from '@/router/router';
import localStorageKeys from '@/shared/constants/localStorageKeys';
import AppContextProvider from '@/shared/Context/AppContext';
import colorThemeSwitcher from '@/shared/helpers/colorThemeSwitcher';
import EditorProvider from '@components/Editor/context/EditorProvider';
import AuthProvider from '@shared/Context/AuthContext';
import LanguageProvider from '@shared/Context/LanguageContext';

Expand All @@ -15,12 +17,17 @@ const App = () => {
colorThemeSwitcher.setLight();
}
}, []);

return (
<AuthProvider>
<LanguageProvider>
<RouterProvider router={router} />
</LanguageProvider>
</AuthProvider>
<EditorProvider>
<AppContextProvider>
<AuthProvider>
<LanguageProvider>
<RouterProvider router={router} />
</LanguageProvider>
</AuthProvider>
</AppContextProvider>
</EditorProvider>
);
};

Expand Down
12 changes: 5 additions & 7 deletions src/components/Editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { Dispatch, FC, SetStateAction } from 'react';

import EditorField from '@components/Editor/ui/EditorField';
import LineNumbers from '@components/Editor/ui/LineNumbers';
import useEditorSize from '@components/RequestEditor/lib/hooks/useEditorSize';
import cn from '@shared/lib/helpers/cn';
import useScrollbar from '@shared/lib/hooks/useScrollbar';

type EditorProps = {
editorState: string;
onChange: Dispatch<SetStateAction<string>> | ((value: string) => void);
className?: string;
isJson: boolean;
isReadOnly: boolean;
};

const Editor: FC<EditorProps> = ({ onChange, editorState, className }) => {
const { editorRef, editorNumbersNum, editorNumRef } = useEditorSize();
const Editor: FC<EditorProps> = ({ onChange, editorState, className, isJson, isReadOnly }) => {
const rootRef = useScrollbar<HTMLElement>();

return (
<article ref={rootRef} className={cn('h-full w-full font-jetbrains_mono', className)}>
<div className="flex w-full gap-4 py-4 pr-4 sm:py-7">
<LineNumbers ref={editorNumRef} size={editorNumbersNum} />
<EditorField ref={editorRef} onChange={onChange} value={editorState} />
<div className="flex w-full gap-4 py-7 pr-4">
<EditorField onChange={onChange} value={editorState} isJson={isJson} isReadOnly={isReadOnly} />
</div>
</article>
);
Expand Down
60 changes: 60 additions & 0 deletions src/components/Editor/context/EditorProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createContext, PropsWithChildren, useCallback, useMemo, useState } from 'react';

type TEditorContext = {
readonly query: [number, () => void];
readonly headers: [number, () => void];
readonly variables: [number, () => void];
invalidateKeys: () => void;
};

export const EditorContext = createContext<TEditorContext>({} as TEditorContext);

function generateRandomKey() {
return Math.random() * Math.random();
}

function EditorProvider({ children }: PropsWithChildren) {
const [queryKey, setQueryKey] = useState(() => generateRandomKey());
const [variablesKey, setVariablesKey] = useState(() => generateRandomKey());
const [headersKey, setHeadersKey] = useState(() => generateRandomKey());

const invalidateQueryKey = useCallback(() => {
setQueryKey(generateRandomKey());
}, []);

const invalidateVariablesKey = useCallback(() => {
setVariablesKey(generateRandomKey());
}, []);

const invalidateHeadersKey = useCallback(() => {
setHeadersKey(generateRandomKey());
}, []);

const invalidateKeys = useCallback(() => {
invalidateQueryKey();
invalidateVariablesKey();
invalidateHeadersKey();
}, [invalidateHeadersKey, invalidateQueryKey, invalidateVariablesKey]);

const value = useMemo<TEditorContext>(
() => ({
query: [queryKey, invalidateQueryKey] as const,
headers: [headersKey, invalidateHeadersKey] as const,
variables: [variablesKey, invalidateVariablesKey] as const,
invalidateKeys,
}),
[
headersKey,
invalidateHeadersKey,
invalidateKeys,
invalidateQueryKey,
invalidateVariablesKey,
queryKey,
variablesKey,
],
);

return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;
}

export default EditorProvider;
15 changes: 15 additions & 0 deletions src/components/Editor/lib/hooks/useEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useContext } from 'react';

import { EditorContext } from '@components/Editor/context/EditorProvider';

function useEditor() {
const context = useContext(EditorContext);

if (context === undefined) {
throw new Error('useEditor must be used within a EditorProvider');
}

return context;
}

export default useEditor;
11 changes: 4 additions & 7 deletions src/components/Editor/lib/hooks/useEditorUrlState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useEffect } from 'react';

import useUrl from '@shared/lib/hooks/useUrl';
import { UrlParams } from '@shared/lib/types/types';
Expand All @@ -19,12 +19,9 @@ function useEditorUrlState(urlParam: UrlParams, initialState = '') {
if (urlState === initialState) setUrl(urlParam, initialState);
}, [initialState, setUrl, urlParam, urlState]);

const handleChange = useCallback(
function handleChange(value: string) {
setUrl(urlParam, value);
},
[setUrl, urlParam],
);
const handleChange = (value: string) => {
setUrl(urlParam, value);
};

return [urlState, handleChange] as const;
}
Expand Down
32 changes: 32 additions & 0 deletions src/components/Editor/lib/submitRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* eslint-disable no-console */
import isValidJson from '@/shared/lib/helpers/isJsonValid';

async function sumbitRequest(query: string, variables: string, headers: string) {
const DEFAULT_HEADERS = '{"Content-Type": "application/json"}';
const isHeadersEmpty = typeof headers !== 'string' || headers.trim().length === 0;
const headersToParse = isHeadersEmpty ? DEFAULT_HEADERS : headers;

if (!isHeadersEmpty && isValidJson(headersToParse)) {
const msg = 'Invalid headers';
return msg; // TODO maybe show toaster instead
}

try {
const response = await fetch('https://swapi-graphql.netlify.app/.netlify/functions/index', {
method: 'POST',
headers: JSON.parse(headersToParse),
body: JSON.stringify({
query,
variables,
}),
});

const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}

export default sumbitRequest;
72 changes: 51 additions & 21 deletions src/components/Editor/ui/EditorField.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
/* eslint-disable react/no-danger */
import { Dispatch, forwardRef, SetStateAction, SyntheticEvent } from 'react';
/* eslint-disable react-hooks/exhaustive-deps */
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';

import { defaultKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { EditorState } from '@codemirror/state';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, gutter, keymap, lineNumbers } from '@codemirror/view';

import { useAppContext } from '@/shared/Context/hooks';

type EditorFieldProps = {
value: string;
onChange: Dispatch<SetStateAction<string>> | ((value: string) => void);
isJson: boolean;
isReadOnly: boolean;
};

const EditorField = forwardRef<HTMLDivElement, EditorFieldProps>(({ onChange, value = '' }, ref) => {
function handleInput(e: SyntheticEvent) {
onChange?.((e.target as HTMLElement).innerHTML);
}
const EditorField = ({ onChange, value = '', isJson, isReadOnly }: EditorFieldProps) => {
const editor = useRef<HTMLPreElement>(null);
const [code, setCode] = useState(value);
const { prettifyEditors } = useAppContext();

return (
<div
data-testid="editor-field"
ref={ref}
tabIndex={0}
aria-label="The text editor"
role="textbox"
contentEditable
onInput={handleInput}
className="h-fit w-full whitespace-pre-wrap outline-none"
dangerouslySetInnerHTML={{ __html: value }}
/>
);
});
useEffect(() => {
const codemirrorLanguage = isJson ? json() : javascript();
const onUpdate = EditorView.updateListener.of((v) => {
const newValue = v.state.doc.toString();
setCode(newValue);
onChange(newValue);
prettifyEditors(false);
});
const extensions = [
keymap.of(defaultKeymap),
codemirrorLanguage,
oneDark,
EditorView.lineWrapping,
onUpdate,
EditorState.readOnly.of(isReadOnly),
];
if (!isReadOnly) {
extensions.push(lineNumbers());
extensions.push(gutter({}));
}
const startState = EditorState.create({
doc: code,
extensions,
});
const view = new EditorView({
state: startState,
parent: editor.current ?? undefined,
});
return () => {
view.destroy();
};
}, []);

EditorField.displayName = 'EditorField';
return <pre data-testid="editor-field" ref={editor} />;
};

export default EditorField;
29 changes: 28 additions & 1 deletion src/components/EditorTools/ui/HeadersEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
import { useEffect } from 'react';

import { useAppContext } from '@/shared/Context/hooks';
import isValidJson from '@/shared/lib/helpers/isJsonValid';
import jsonFormatter from '@/shared/lib/helpers/jsonFormatter';
import Editor from '@components/Editor/Editor';
import useEditor from '@components/Editor/lib/hooks/useEditor';
import useEditorUrlState from '@components/Editor/lib/hooks/useEditorUrlState';
import urlParams from '@shared/constants/urlParams';

const HeadersEditor = () => {
const [editorState, setEditorState] = useEditorUrlState(urlParams.HEADERS);
const { prettify } = useAppContext();
const {
headers: [headersKey, invalidateKey],
} = useEditor();

useEffect(() => {
if (prettify && isValidJson(editorState)) {
setEditorState(jsonFormatter(editorState));
invalidateKey();
}
}, [editorState, invalidateKey, prettify, setEditorState]);

return <Editor key="headers" editorState={editorState} onChange={setEditorState} />;
return (
<Editor
key={headersKey}
editorState={editorState}
onChange={(val: string) => {
if (!prettify) setEditorState(val);
}}
isJson
isReadOnly={false}
/>
);
};

export default HeadersEditor;
29 changes: 28 additions & 1 deletion src/components/EditorTools/ui/VariablesEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
import { useEffect } from 'react';

import { useAppContext } from '@/shared/Context/hooks';
import isValidJson from '@/shared/lib/helpers/isJsonValid';
import jsonFormatter from '@/shared/lib/helpers/jsonFormatter';
import Editor from '@components/Editor/Editor';
import useEditor from '@components/Editor/lib/hooks/useEditor';
import useEditorUrlState from '@components/Editor/lib/hooks/useEditorUrlState';
import urlParams from '@shared/constants/urlParams';

const VariablesEditor = () => {
const [editorState, setEditorState] = useEditorUrlState(urlParams.VARIABLES);
const { prettify } = useAppContext();
const {
variables: [variablesKey, invalidateKey],
} = useEditor();

useEffect(() => {
if (prettify && isValidJson(editorState)) {
setEditorState(jsonFormatter(editorState));
invalidateKey();
}
}, [editorState, invalidateKey, prettify, setEditorState]);

return <Editor key="variables" editorState={editorState} onChange={setEditorState} />;
return (
<Editor
key={variablesKey}
editorState={editorState}
onChange={(val: string) => {
if (!prettify) setEditorState(val);
}}
isJson
isReadOnly={false}
/>
);
};

export default VariablesEditor;
2 changes: 1 addition & 1 deletion src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';

import useLanguage from '@/shared/Context/hooks';
import { useLanguage } from '@/shared/Context/hooks';
import Icon from '@/shared/ui/Icon';
import IconButton from '@/shared/ui/IconButton';
import DocsComp from '@components/DocsComp/DocsComp';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Nav/ui/NavigationDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Details from '@components/ViewList/ui/Details';
import ViewItem from '@components/ViewList/ui/ViewItem';
import ViewList from '@components/ViewList/ViewList';
import ROUTES from '@shared/constants/routes';
import useLanguage from '@shared/Context/hooks';
import { useLanguage } from '@shared/Context/hooks';
import Icon from '@shared/ui/Icon';

const NavigationDrawer = () => {
Expand Down
Loading