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 error boundary #62

Merged
merged 13 commits into from
Jan 5, 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
10 changes: 8 additions & 2 deletions src/components/DocsComp/DocsComp.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { lazy, Suspense } from 'react';

import useSchemaExplorer from './lib/hooks/useSchemaExplorer';
import DocsModal from './ui/DocsModal';
import DocsOverlay from './ui/DocsOverlay';
import SuspenseFallback from './ui/SuspenseFallback';

type PropsType = {
setIsDocsShown: React.Dispatch<React.SetStateAction<boolean>>;
isShown: boolean;
};

const DocsModal = lazy(() => import('./ui/DocsModal'));

const DocsComp = ({ isShown, setIsDocsShown }: PropsType) => {
const schemaExplorer = useSchemaExplorer();
return (
<DocsOverlay isShown={isShown} setIsDocsShown={setIsDocsShown} explorer={schemaExplorer}>
<DocsModal setIsDocsShown={setIsDocsShown} explorer={schemaExplorer} />
<Suspense fallback={<SuspenseFallback />}>
<DocsModal setIsDocsShown={setIsDocsShown} explorer={schemaExplorer} />
</Suspense>
</DocsOverlay>
);
};
Expand Down
31 changes: 31 additions & 0 deletions src/components/DocsComp/lib/helpers/getEndpointSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Dispatch, SetStateAction } from 'react';

import introspectionQuery from '@/shared/constants/introspectionQuery';
import { DocsSchemaType } from '@/shared/types';

export default async function getEndpointSchema(
endpoint: string,
schemaSetter: (value: SetStateAction<DocsSchemaType>) => void,
loadingSetter: Dispatch<SetStateAction<boolean>>,
) {
try {
loadingSetter(true);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(introspectionQuery),
});
if (response.ok && response.status === 200) {
const res = await response.json();
loadingSetter(false);
return schemaSetter(res.data.__schema);
}
loadingSetter(false);
return schemaSetter(null);
} catch (e) {
loadingSetter(false);
return schemaSetter(null);
}
}
15 changes: 15 additions & 0 deletions src/components/DocsComp/ui/DocsLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import DocsModalLayout from '@/layouts/DocsModalLayout';
import Spinner from '@/shared/ui/Spinner';

const DocsLoader = () => {
return (
<DocsModalLayout>
<div className="flex h-full w-full flex-col items-center justify-center">
<Spinner indeterminate />
<p className="mt-10 text-on-surface">We are loading your docs...</p>
</div>
</DocsModalLayout>
);
};

export default DocsLoader;
29 changes: 21 additions & 8 deletions src/components/DocsComp/ui/DocsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
// AFTER LOGIC FOR SAVING AND FETCHING ENDPOINT SHEMA WILL BE ADDED - MUST REMOVE SCHEMA IMPORT AND REPLACE IT FOR DOWNLOADED SCHEMA IN FURTHER CODE
import { Dispatch, SetStateAction, useEffect, useState } from 'react';

import { Dispatch, SetStateAction } from 'react';

import { swapiSchema } from '@/shared/constants/schemaData';
import DocsModalLayout from '@/layouts/DocsModalLayout';
import { useAppContext } from '@/shared/Context/hooks';
import { DocsExplorerType, SchemaTypeObj } from '@/shared/types';
import CloseDocsBtn from '@components/DocsComp/ui/CloseDocsBtn';

import DocsLoader from './DocsLoader';
import DocsRootComp from './DocsRootComp';
import DocsTypeComp from './DocsTypeComp';
import SchemaFallbackUi from './SchemaFallbackUi';
import getEndpointSchema from '../lib/helpers/getEndpointSchema';

type PropsType = {
setIsDocsShown: Dispatch<SetStateAction<boolean>>;
explorer: DocsExplorerType;
};

const DocsModal = ({ setIsDocsShown, explorer }: PropsType) => {
const { currEndpoint, setEndpointSchema, endpointSchema } = useAppContext();
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
getEndpointSchema(currEndpoint, setEndpointSchema, setIsLoading);
}, [currEndpoint, setEndpointSchema]);

if (isLoading) return <DocsLoader />;
if (!endpointSchema) return <SchemaFallbackUi closeModal={setIsDocsShown} />;

const content = explorer.isDocs() ? (
<DocsRootComp types={swapiSchema.data.__schema.types as SchemaTypeObj[]} explorer={explorer} />
<DocsRootComp types={endpointSchema.types as SchemaTypeObj[]} explorer={explorer} />
) : (
<DocsTypeComp
explorer={explorer}
currType={swapiSchema.data.__schema.types.find((elem) => elem.name === explorer.current()) as SchemaTypeObj}
currType={endpointSchema.types.find((elem) => elem.name === explorer.current()) as SchemaTypeObj}
/>
);

return (
<section className="relative z-20 h-[100dvh] w-[270px] cursor-auto rounded-r-[28px] bg-surface p-3 sm:w-[420px]">
<DocsModalLayout>
<CloseDocsBtn
onClick={() => {
setIsDocsShown((prev) => !prev);
Expand All @@ -33,7 +46,7 @@ const DocsModal = ({ setIsDocsShown, explorer }: PropsType) => {
className="absolute right-[20px] top-[20px] z-20"
/>
{content}
</section>
</DocsModalLayout>
);
};

Expand Down
27 changes: 27 additions & 0 deletions src/components/DocsComp/ui/SchemaFallbackUi.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FC, SetStateAction } from 'react';

import DocsModalLayout from '@/layouts/DocsModalLayout';

import CloseDocsBtn from './CloseDocsBtn';

type PropsType = {
closeModal: (value: SetStateAction<boolean>) => void;
};

const SchemaFallbackUi: FC<PropsType> = ({ closeModal }) => {
return (
<DocsModalLayout>
<CloseDocsBtn
onClick={() => {
closeModal((prev) => !prev);
}}
className="absolute right-[20px] top-[20px] z-20"
/>
<div className="flex h-full w-full items-center p-6">
<p className="w-full text-center text-on-surface">There is no schema at provided endpoint :(</p>
</div>
</DocsModalLayout>
);
};

export default SchemaFallbackUi;
15 changes: 15 additions & 0 deletions src/components/DocsComp/ui/SuspenseFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import DocsModalLayout from '@/layouts/DocsModalLayout';
import Spinner from '@/shared/ui/Spinner';

const SuspenseFallback = () => {
return (
<DocsModalLayout>
<div className="flex h-full w-full flex-col items-center justify-center">
<Spinner indeterminate />
<p className="mt-10 text-on-surface">Soon here will be docs section...</p>
</div>
</DocsModalLayout>
);
};

export default SuspenseFallback;
37 changes: 37 additions & 0 deletions src/components/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable no-console */
import { Component } from 'react';

type MyProps = {
fallback: JSX.Element;
children: JSX.Element;
};

type MyState = {
hasError: boolean;
};

class ErrorBoundary extends Component<MyProps, MyState> {
constructor(props: MyProps) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError() {
return { hasError: true };
}

componentDidCatch(error: unknown, info: React.ErrorInfo) {
console.log(error, info.componentStack);
}

render() {
const { hasError } = this.state;
const { fallback, children } = this.props;
if (hasError) {
return fallback;
}
return children;
}
}

export default ErrorBoundary;
13 changes: 13 additions & 0 deletions src/components/ErrorFallback/ErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const ErrorFallback = () => {
return (
<div className="flex h-[100dvh] w-full items-center justify-center bg-surface">
<div className="mx-6 max-w-[500px] rounded-md bg-surface-container p-6 text-center text-lg text-on-surface">
Somehow something managed to crash our app...
<br />
<br /> We suggest you to reload the page to continue using the app.
</div>
</div>
);
};

export default ErrorFallback;
18 changes: 6 additions & 12 deletions src/components/SettingsPageComp/EndpointComp.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { FC } from 'react';

import { useLanguage } from '@/shared/Context/hooks';
import { useAppContext, useLanguage } from '@/shared/Context/hooks';
import FilledTextField from '@/shared/ui/FilledTextField';
import Icon from '@/shared/ui/Icon';
import IconButton from '@/shared/ui/IconButton';

type PropsType = {
endpoint: string;
saveEndpoint: (value: string) => void;
};

const EndpointComp: FC<PropsType> = ({ endpoint, saveEndpoint }) => {
const EndpointComp = () => {
const { translation } = useLanguage();
const { currEndpoint, setCurrEndpoint } = useAppContext();
return (
<div className="mt-6 flex flex-col items-center justify-between border-b-[1px] border-outline-variant pb-6 sm:flex-row">
<div className="flex w-full flex-col items-start justify-between">
Expand All @@ -21,11 +15,11 @@ const EndpointComp: FC<PropsType> = ({ endpoint, saveEndpoint }) => {
<FilledTextField
className="relative ml-auto mt-4 h-[68px] w-full max-w-[360px] text-base sm:mt-0"
label="API endpoint"
value={endpoint}
value={currEndpoint}
name="endpoint"
onChange={(e) => saveEndpoint((e?.target as HTMLInputElement).value)}
onChange={(e) => setCurrEndpoint((e?.target as HTMLInputElement).value)}
>
<IconButton className="absolute right-0 top-[10px]" slot="trailing-icon" onClick={() => saveEndpoint('')}>
<IconButton className="absolute right-0 top-[10px]" slot="trailing-icon" onClick={() => setCurrEndpoint('')}>
<Icon>cancel</Icon>
</IconButton>
</FilledTextField>
Expand Down
11 changes: 11 additions & 0 deletions src/layouts/DocsModalLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type PropsType = { children: JSX.Element | JSX.Element[] };

const DocsModalLayout = ({ children }: PropsType) => {
return (
<section className="relative z-20 h-[100dvh] w-[270px] cursor-auto rounded-r-[28px] bg-surface p-3 sm:w-[420px]">
{children}
</section>
);
};

export default DocsModalLayout;
6 changes: 5 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import ReactDOM from 'react-dom/client';

import App from '@/app/App';

import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
import ErrorFallback from './components/ErrorFallback/ErrorFallback';
import initFirebaseApp from './firebase';

import '@/styles/index.css';
Expand All @@ -14,6 +16,8 @@ initFirebaseApp();

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<ErrorBoundary fallback={<ErrorFallback />}>
<App />
</ErrorBoundary>
</React.StrictMode>,
);
6 changes: 1 addition & 5 deletions src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import useScrollbar from '@shared/lib/hooks/useScrollbar';
const SettignsPage = () => {
const [settingsState, setSettingsState] = useState({
headers: true,
endpoint: 'goods',
});
const root = useScrollbar();

Expand All @@ -25,10 +24,7 @@ const SettignsPage = () => {
switcher={() => setSettingsState((prev) => ({ ...prev, headers: !prev.headers }))}
/>
<DarkModeComp />
<EndpointComp
endpoint={settingsState.endpoint}
saveEndpoint={(value: string) => setSettingsState((prev) => ({ ...prev, endpoint: value }))}
/>
<EndpointComp />
<LangSelectorComp />
<ClearStorageComp />
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/router/router.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHashRouter } from 'react-router-dom';

import ErrorFallback from '@/components/ErrorFallback/ErrorFallback';
import MainLayout from '@/layouts/MainLayout';
import LoginPage from '@/pages/LoginPage';
import SettignsPage from '@/pages/SettingsPage';
Expand All @@ -14,6 +15,7 @@ export const routes = [
{
path: '/',
element: <MainLayout />,
errorElement: <ErrorFallback />,
children: [
{
path: ROUTES.WELCOME_PAGE,
Expand Down
32 changes: 29 additions & 3 deletions src/shared/Context/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { createContext, ReactNode, useCallback, useMemo, useState } from 'react';
import { createContext, Dispatch, ReactNode, SetStateAction, useCallback, useMemo, useState } from 'react';

import jsonFormater from '@/shared/lib/helpers/jsonFormatter';

import { DocsSchemaType } from '../types';

type AppContextType = {
currentResponse: string;
updateCurrentResponse: (response: string) => void;
prettifyEditors: (isPrettified: boolean) => void;
prettify: boolean;
currEndpoint: string;
setCurrEndpoint: Dispatch<SetStateAction<string>>;
setEndpointSchema: Dispatch<SetStateAction<DocsSchemaType>>;
endpointSchema: DocsSchemaType;
};

export const AppContext = createContext<AppContextType>({} as AppContextType);

export default function AppContextProvider({ children }: { children: ReactNode }) {
const [currEndpoint, setCurrEndpoint] = useState('https://rickandmortyapi.com/graphql');
const [endpointSchema, setEndpointSchema] = useState(null as DocsSchemaType);
const [currentResponse, setCurrentResponse] = useState<string>('');
const [prettify, setPrettify] = useState<boolean>(false);

Expand All @@ -25,8 +33,26 @@ export default function AppContextProvider({ children }: { children: ReactNode }
}, []);

const contextValue = useMemo(
() => ({ currentResponse, updateCurrentResponse, prettifyEditors, prettify }),
[currentResponse, updateCurrentResponse, prettifyEditors, prettify],
() => ({
currentResponse,
updateCurrentResponse,
prettifyEditors,
prettify,
currEndpoint,
setCurrEndpoint,
endpointSchema,
setEndpointSchema,
}),
[
currentResponse,
updateCurrentResponse,
prettifyEditors,
prettify,
currEndpoint,
setCurrEndpoint,
endpointSchema,
setEndpointSchema,
],
);

return <AppContext.Provider value={contextValue}>{children}</AppContext.Provider>;
Expand Down
7 changes: 7 additions & 0 deletions src/shared/constants/introspectionQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const introspectionQuery = {
query:
'query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n types {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n args {\n name\n description\n type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n defaultValue\n }\n type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n name\n type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n defaultValue\n }\n interfaces {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n enumValues(includeDeprecated: true) {\n name\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n directives {\n name\n locations\n args {\n name\n type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n defaultValue\n }\n }\n }\n}',
operationName: 'IntrospectionQuery',
};

export default introspectionQuery;
Loading