diff --git a/src/app/index.tsx b/src/app/index.tsx
index dc7a8a44..266e62df 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -22,6 +22,7 @@ import {
useStoreWallet,
} from '@stores';
import { analyticsApp } from 'firebaseConfig';
+import { useStoreCustomExamples } from 'src/stores/examples';
import { routes } from './routes';
@@ -54,32 +55,43 @@ export const App = () => {
resetStore: resetStoreWallet,
} = useStoreWallet.use.actions();
+ const initStoreExamples = useStoreCustomExamples.use.init?.();
+ const { resetStore: resetStoreExamples } = useStoreCustomExamples.use.actions();
+
useEffect(() => {
- initStoreAuth();
- initStoreUI();
- initStoreChainClient();
- initStoreWallet();
-
- window.customPackages = {};
- Object.assign(window.customPackages, {
- createClient,
- ...createClient,
- papiDescriptors,
- ...papiDescriptors,
- getPolkadotSigner,
- connectInjectedExtension,
- getInjectedExtensions,
- });
+ const initializeStores = async () => {
+ await initStoreAuth();
+ initStoreUI();
+ initStoreChainClient();
+ initStoreWallet();
+ initStoreExamples();
+
+ window.customPackages = {};
+ Object.assign(window.customPackages, {
+ createClient,
+ ...createClient,
+ papiDescriptors,
+ ...papiDescriptors,
+ getPolkadotSigner,
+ connectInjectedExtension,
+ getInjectedExtensions,
+ });
- refTimeout.current = setTimeout(() => {
- document.body.removeAttribute('style');
- }, 400);
+ refTimeout.current = setTimeout(() => {
+ document.body.removeAttribute('style');
+ }, 400);
+ };
+
+ initializeStores().catch((error) => {
+ console.error('Error initializing stores', error);
+ });
return () => {
resetStoreAuth();
resetStoreUI();
void resetStoreChain();
resetStoreWallet();
+ resetStoreExamples();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/src/assets/svgs/icon/expand.svg b/src/assets/svgs/icon/expand.svg
new file mode 100644
index 00000000..5fad45b8
--- /dev/null
+++ b/src/assets/svgs/icon/expand.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/svgs/icon/loader.svg b/src/assets/svgs/icon/loader.svg
new file mode 100644
index 00000000..36b06104
--- /dev/null
+++ b/src/assets/svgs/icon/loader.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/svgs/icon/logout.svg b/src/assets/svgs/icon/logout.svg
new file mode 100644
index 00000000..d8a77115
--- /dev/null
+++ b/src/assets/svgs/icon/logout.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/svgs/icon/pen.svg b/src/assets/svgs/icon/pen.svg
new file mode 100644
index 00000000..41118060
--- /dev/null
+++ b/src/assets/svgs/icon/pen.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svgs/icon/trash.svg b/src/assets/svgs/icon/trash.svg
new file mode 100644
index 00000000..c13cc4cc
--- /dev/null
+++ b/src/assets/svgs/icon/trash.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/components/copyToClipboard/index.tsx b/src/components/copyToClipboard/index.tsx
index 26acd623..3126af57 100644
--- a/src/components/copyToClipboard/index.tsx
+++ b/src/components/copyToClipboard/index.tsx
@@ -19,7 +19,7 @@ import type {
interface ICopyToClipboardProps {
text: string;
toastMessage?: string;
- children: (props: { ClipboardIcon: ReactNode; onClick: (e: React.MouseEvent) => void }) => ReactElement;
+ children: (props: { ClipboardIcon: ReactNode; onClick: (e: React.MouseEvent) => void; text: string }) => ReactElement;
onCopy?: (success: boolean, text: string) => void;
showToast?: boolean;
className?: string;
@@ -122,8 +122,13 @@ export const CopyToClipboard = memo((props: ICopyToClipboardProps) => {
isCopied,
]);
- return children({
- ClipboardIcon,
- onClick: copyToClipboard,
- });
+ return (
+ <>
+ {children({
+ ClipboardIcon,
+ onClick: copyToClipboard,
+ text,
+ })}
+ >
+ );
});
diff --git a/src/views/onboarding/components/notFound/index.tsx b/src/components/exampleNotFound/index.tsx
similarity index 50%
rename from src/views/onboarding/components/notFound/index.tsx
rename to src/components/exampleNotFound/index.tsx
index b0423e37..86d7ba0a 100644
--- a/src/views/onboarding/components/notFound/index.tsx
+++ b/src/components/exampleNotFound/index.tsx
@@ -1,8 +1,17 @@
import { Icon } from '@components/icon';
+import { PDLink } from '@components/pdLink';
import { useStoreAuth } from '@stores';
import { cn } from '@utils/helpers';
-export const NotFound = () => {
+interface ExampleNotFoundProps {
+ classes?: string;
+ iconClasses?: string;
+ textClasses?: string;
+ onClick?: () => void;
+}
+
+export const ExampleNotFound = (props: ExampleNotFoundProps) => {
+ const { classes, iconClasses, textClasses, onClick } = props;
const isAuthenticated = useStoreAuth.use.jwtToken?.();
const { authorize } = useStoreAuth.use.actions();
@@ -16,9 +25,14 @@ export const NotFound = () => {
'mb-8',
'self-center text-dev-white-1000',
'dark:text-dev-purple-50',
+ iconClasses,
)}
/>
-
+
Please Log in
To access your custom examples, please log in using your GitHub account.
@@ -40,7 +54,11 @@ export const NotFound = () => {
}
return (
-
+
{
'mb-8',
'self-center text-dev-white-1000',
'dark:text-dev-purple-50',
+ iconClasses,
)}
/>
-
+
Nothing here
Currently, you don't have any custom examples created. Ready to create one?
-
+
+ {
+ onClick
+ ? (
+
+ )
+ : (
+
+
+
+ )
+ }
+
);
diff --git a/src/components/githubButton/index.tsx b/src/components/githubButton/index.tsx
index 1cb455ae..3b672d0d 100644
--- a/src/components/githubButton/index.tsx
+++ b/src/components/githubButton/index.tsx
@@ -1,9 +1,11 @@
import { useToggleVisibility } from '@pivanov/use-toggle-visibility';
+import { useCallback } from 'react';
import { Icon } from '@components/icon';
import { ModalGithubLogin } from '@components/modals/modalGithubLogin';
import { useStoreAuth } from '@stores';
import { cn } from '@utils/helpers';
+import { useStoreCustomExamples } from 'src/stores/examples';
export const GithubButton = () => {
const [
@@ -12,33 +14,69 @@ export const GithubButton = () => {
] = useToggleVisibility(ModalGithubLogin);
const { logout } = useStoreAuth.use.actions();
- const authIsLoading = useStoreAuth.use.jwtTokenIsLoading?.();
+ const { name, avatar } = useStoreAuth.use.user();
const isAuthenticated = useStoreAuth.use.jwtToken?.();
+ const { resetStore } = useStoreCustomExamples.use.actions();
+
+ const handleLogout = useCallback(async () => {
+ resetStore();
+ await logout();
+ }, [
+ logout,
+ resetStore,
+ ]);
return (
-
+ {
+ isAuthenticated
+ ? (
+
+
+
+ {name.length > 20 ? `${name.slice(0, 20)}..` : name}
+
+
+
+ )
+ : (
+
+ )
+ }
+
);
diff --git a/src/components/modals/modalDeleteExample/index.tsx b/src/components/modals/modalDeleteExample/index.tsx
new file mode 100644
index 00000000..46660a9e
--- /dev/null
+++ b/src/components/modals/modalDeleteExample/index.tsx
@@ -0,0 +1,101 @@
+import { useEventBus } from '@pivanov/event-bus';
+import { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { Icon } from '@components/icon';
+import {
+ cn,
+ sleep,
+} from '@utils/helpers';
+import { useStoreCustomExamples } from 'src/stores/examples';
+
+import {
+ type IModal,
+ Modal,
+} from '../modal';
+
+import type { IDeleteExampleModalClose } from '@custom-types/eventBus';
+
+interface IModalDeleteExample extends Pick {
+ id: string;
+ onClose: () => void;
+}
+
+export const ModalDeleteExample = (props: IModalDeleteExample) => {
+ const { id, onClose } = props;
+ const { deleteExample, loadExampleContent } = useStoreCustomExamples.use.actions();
+ const selectedExample = useStoreCustomExamples.use.selectedExample();
+ const { isDeleting } = useStoreCustomExamples.use.loading();
+ const navigate = useNavigate();
+
+ const handleDelete = useCallback(async () => {
+ deleteExample(id);
+ }, [
+ deleteExample,
+ id,
+ ]);
+
+ useEventBus('@@-close-delete-example-modal', () => {
+ if (id === selectedExample?.id) {
+ loadExampleContent('1', 'default');
+ navigate(`/code?d=1`);
+ void sleep(400);
+ }
+
+ onClose();
+ });
+ return (
+
+
+
+
+
Delete Example
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/modals/modalEditExampleInfo/index.tsx b/src/components/modals/modalEditExampleInfo/index.tsx
new file mode 100644
index 00000000..40f275f0
--- /dev/null
+++ b/src/components/modals/modalEditExampleInfo/index.tsx
@@ -0,0 +1,122 @@
+import { useEventBus } from '@pivanov/event-bus';
+import {
+ useCallback,
+ useRef,
+} from 'react';
+import { toast } from 'react-hot-toast';
+
+import { Icon } from '@components/icon';
+import { cn } from '@utils/helpers';
+import { useStoreCustomExamples } from 'src/stores/examples';
+
+import {
+ type IModal,
+ Modal,
+} from '../modal';
+
+import type { IEditExampleInfoModalClose } from '@custom-types/eventBus';
+
+interface IModalEditExampleInfo extends Pick {
+ id: string;
+ onClose: () => void;
+}
+
+export const ModalEditExampleInfo = (props: IModalEditExampleInfo) => {
+ const { id, onClose } = props;
+
+ const examples = useStoreCustomExamples.use.examples();
+ const { updateExampleInfo } = useStoreCustomExamples.use.actions();
+ const { isSavingInfo } = useStoreCustomExamples.use.loading();
+
+ const example = examples.find((example) => example.id === id);
+
+ const exampleNameRef = useRef(null);
+ const descriptionRef = useRef(null);
+
+ const handleSubmit = useCallback(() => {
+ const exampleName = exampleNameRef.current?.value;
+ const description = descriptionRef.current?.value;
+
+ if (!exampleName || !description) {
+ toast.error('Please fill all fields');
+ return;
+ }
+
+ updateExampleInfo(id, exampleName, description);
+ }, [
+ id,
+ updateExampleInfo,
+ ]);
+
+ useEventBus('@@-close-edit-example-modal', () => {
+ onClose();
+ });
+
+ return (
+
+
+
Edit Example
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/modals/modalSaveExample/createExample/index.tsx b/src/components/modals/modalSaveExample/createExample/index.tsx
new file mode 100644
index 00000000..7094d60d
--- /dev/null
+++ b/src/components/modals/modalSaveExample/createExample/index.tsx
@@ -0,0 +1,99 @@
+import {
+ useCallback,
+ useRef,
+} from 'react';
+import { toast } from 'react-hot-toast';
+
+import { Icon } from '@components/icon';
+import { cn } from '@utils/helpers';
+import { useStoreCustomExamples } from 'src/stores/examples';
+
+interface CreateExampleProps {
+ code: string;
+ handleClose: () => void;
+}
+
+export const CreateExample = (props: CreateExampleProps) => {
+ const { code, handleClose } = props;
+ const { createExample } = useStoreCustomExamples.use.actions();
+ const { isCreatingExample } = useStoreCustomExamples.use.loading();
+
+ const exampleNameRef = useRef(null);
+ const descriptionRef = useRef(null);
+
+ const handleSubmit = useCallback(() => {
+ const exampleName = exampleNameRef.current?.value;
+ const description = descriptionRef.current?.value;
+
+ if (!exampleName || !description) {
+ toast.error('Please fill all fields');
+ return;
+ }
+
+ createExample({
+ exampleName,
+ description,
+ code,
+ });
+ }, [
+ code,
+ createExample,
+ ]);
+
+ return (
+
+
Create Example
+
+
+
+
+
+ );
+};
diff --git a/src/components/modals/modalSaveExample/index.tsx b/src/components/modals/modalSaveExample/index.tsx
new file mode 100644
index 00000000..129eff20
--- /dev/null
+++ b/src/components/modals/modalSaveExample/index.tsx
@@ -0,0 +1,157 @@
+import { useEventBus } from '@pivanov/event-bus';
+import {
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+import { toast } from 'react-hot-toast';
+import { useNavigate } from 'react-router-dom';
+
+import { Icon } from '@components/icon';
+import { CreateExample } from '@components/modals/modalSaveExample/createExample';
+import { cn } from '@utils/helpers';
+import { useStoreCustomExamples } from 'src/stores/examples';
+
+import {
+ type IModal,
+ Modal,
+} from '../modal';
+
+import type { IUploadExampleModalClose } from '@custom-types/eventBus';
+
+interface IModalGithubLogin extends Pick {
+ code?: string;
+ onClose: () => void;
+}
+
+export const ModalSaveExample = (props: IModalGithubLogin) => {
+ const { code, onClose } = props;
+
+ const selectedExample = useStoreCustomExamples.use.selectedExample();
+ const examples = useStoreCustomExamples.use.examples();
+
+ const { updateExampleContent, loadExampleContent } = useStoreCustomExamples.use.actions();
+ const { isSavingContent, isCreatingExample } = useStoreCustomExamples.use.loading();
+ const navigate = useNavigate();
+
+ const [
+ createNewExample,
+ setCreateNewExample,
+ ] = useState(false);
+
+ const [
+ isDefaultExample,
+ setIsDefaultExample,
+ ] = useState(false);
+
+ const handleUpdateCurrentExample = useCallback(() => {
+
+ if (!code) {
+ toast.error('No code to save');
+ return;
+ }
+
+ updateExampleContent(code);
+ }, [
+ code,
+ updateExampleContent,
+ ]);
+
+ const handleChangeIsUpload = useCallback(() => {
+ setCreateNewExample(!createNewExample);
+ }, [createNewExample]);
+
+ useEffect(() => {
+ //Check who is author in order to other users to be able to save as new example directly
+ const isAuthor = examples.some((example) => example.id === selectedExample.id);
+
+ if (!isAuthor) {
+ setCreateNewExample(true);
+ setIsDefaultExample(true);
+ }
+ }, [
+ examples,
+ selectedExample.id,
+ ]);
+
+ useEventBus('@@-close-upload-example-modal', ({ data: id }) => {
+ navigate(`/code?c=${id}`);
+ loadExampleContent(id, 'custom');
+ onClose();
+ });
+
+ return (
+
+ {
+ createNewExample
+ ? (
+
+ )
+ : (
+ <>
+
+
+
+
Save Changes
+
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/constants/navigation.ts b/src/constants/navigation.ts
index 687bd172..8db32cc0 100644
--- a/src/constants/navigation.ts
+++ b/src/constants/navigation.ts
@@ -11,7 +11,7 @@ export const NAVIGATION_ITEMS: TNavItem[] = [
{
linkProps: {
title: 'Console',
- to: '/code',
+ to: '/code?d=1',
},
type: 'link',
},
diff --git a/src/constants/snippets/index.ts b/src/constants/snippets/index.ts
index f30293cf..0ac51c02 100644
--- a/src/constants/snippets/index.ts
+++ b/src/constants/snippets/index.ts
@@ -5,29 +5,34 @@ import ChainSubscriptionSnippet from './chainSubscription.txt?raw';
import GetChainSpecificDataSnippet from './getChainSpecificData.txt?raw';
/* eslint-enable */
-import type { ICodeSnippet } from '@custom-types/codeSnippet';
-const snippet1: ICodeSnippet = {
- id: 1,
- name: 'Balance transfer from a test account',
- code: TestAccountTransferSnippet,
+import type { ICodeExample } from '@custom-types/codeSnippet';
+
+const snippet1: ICodeExample = {
+ id: '1',
+ name: 'Get chain specific data',
+ description: 'This snippet demonstrates how to get chain specific data',
+ code: GetChainSpecificDataSnippet,
};
-const snippet2: ICodeSnippet = {
- id: 2,
+const snippet2: ICodeExample = {
+ id: '2',
name: 'Balance transfer from an injected account',
+ description: 'This snippet demonstrates how to transfer balance from an injected account',
code: InjectedAccountTransferSnippet,
};
-const snippet3: ICodeSnippet = {
- id: 3,
+const snippet3: ICodeExample = {
+ id: '3',
name: 'Subscribe to chain data',
+ description: 'This snippet demonstrates how to subscribe to chain data',
code: ChainSubscriptionSnippet,
};
-const snippet4: ICodeSnippet = {
- id: 4,
- name: 'Get chain specific data',
- code: GetChainSpecificDataSnippet,
+const snippet4: ICodeExample = {
+ id: '4',
+ name: 'Balance transfer from a test account',
+ description: 'This snippet demonstrates how to transfer balance from a test account',
+ code: TestAccountTransferSnippet,
};
export const snippets = [
diff --git a/src/services/authService.ts b/src/services/authService.ts
index 8329d82e..71e6c86d 100644
--- a/src/services/authService.ts
+++ b/src/services/authService.ts
@@ -16,11 +16,12 @@ import type { IAuthResponse } from '@custom-types/auth';
const AUTH_URL = `${SERVER_URL}/auth`;
-const authoriseGitHubApp = () => {
+const authorizeGitHubApp = () => {
const githubClientId = import.meta.env.VITE_GITHUB_CLIENT_ID;
const githubApiUrl = import.meta.env.VITE_GITHUB_API_URL;
const { pathname, search } = location;
+
window.localStorage.setItem(
STORAGE_AUTH_SUCCESSFUL_REDIRECT_TO,
`${pathname}${search}`,
@@ -32,56 +33,112 @@ const authoriseGitHubApp = () => {
};
const login = async (code: string): Promise => {
- const response = await axios.post(
- `${AUTH_URL}/login`,
- { code },
- { withCredentials: true },
- );
-
- await storageSetItem(
- STORAGE_AUTH_CACHE_NAME,
- STORAGE_AUTH_JWT_TOKEN,
- response?.data?.jwtToken,
- );
-
- return response.data;
+ try {
+ const response = await axios.post(
+ `${AUTH_URL}/login`,
+ { code },
+ { withCredentials: true },
+ );
+
+ await storageSetItem(
+ STORAGE_AUTH_CACHE_NAME,
+ STORAGE_AUTH_JWT_TOKEN,
+ response?.data?.jwtToken,
+ );
+
+ await storageSetItem(
+ STORAGE_AUTH_CACHE_NAME,
+ 'user',
+ JSON.stringify({ name: response?.data?.userName, avatar: response?.data?.userAvatar }),
+ );
+
+ return response.data;
+ } catch (error) {
+ console.error('Login failed', error);
+ throw error;
+ }
};
const logout = async (): Promise => {
- await storageRemoveItem(
- STORAGE_AUTH_CACHE_NAME,
- STORAGE_AUTH_JWT_TOKEN,
- );
+ try {
+ await storageRemoveItem(
+ STORAGE_AUTH_CACHE_NAME,
+ STORAGE_AUTH_JWT_TOKEN,
+ );
+
+ await storageRemoveItem(
+ STORAGE_AUTH_CACHE_NAME,
+ 'user',
+ );
+
+ await axios.post(
+ `${AUTH_URL}/logout`,
+ {},
+ { withCredentials: true },
+ );
+
+ } catch (error) {
+ console.error('Logout failed', error);
+ throw error;
+ }
};
const refreshJwtToken = async (): Promise => {
- const response = await axios.post(
- `${AUTH_URL}/refresh`,
- {},
- { withCredentials: true });
-
- await storageSetItem(
- STORAGE_AUTH_CACHE_NAME,
- STORAGE_AUTH_JWT_TOKEN,
- response?.data?.jwtToken,
- );
-
- return response.data;
+ try {
+ const response = await axios.post(
+ `${AUTH_URL}/refresh`,
+ {},
+ { withCredentials: true });
+
+ await storageSetItem(
+ STORAGE_AUTH_CACHE_NAME,
+ STORAGE_AUTH_JWT_TOKEN,
+ response?.data?.jwtToken,
+ );
+
+ return response.data;
+ } catch (error) {
+ console.error('Token refresh failed', error);
+ throw error;
+ }
};
const getJwtToken = async (): Promise => {
- const jwtToken: string | null = await storageGetItem(
- STORAGE_AUTH_CACHE_NAME,
- STORAGE_AUTH_JWT_TOKEN,
- );
+ try {
+ const jwtToken: string | null = await storageGetItem(
+ STORAGE_AUTH_CACHE_NAME,
+ STORAGE_AUTH_JWT_TOKEN,
+ );
+
+ return jwtToken;
+ } catch (error) {
+ return null;
+ }
+};
- return jwtToken;
+const getUserData = async () => {
+ try {
+ const userData: string | null = await storageGetItem(
+ STORAGE_AUTH_CACHE_NAME,
+ 'user',
+ );
+
+ if (userData === null) {
+ throw new Error('User data not found');
+ }
+
+ return JSON.parse(userData);
+ } catch (error) {
+ console.error('Failed to get user data', error);
+ throw error;
+ }
};
export default {
login,
refreshJwtToken,
- authoriseGitHubApp,
+ authorizeGitHubApp,
getJwtToken,
+ getUserData,
logout,
};
diff --git a/src/services/axiosSetup.ts b/src/services/axiosSetup.ts
index 5e7e265b..e8968ef2 100644
--- a/src/services/axiosSetup.ts
+++ b/src/services/axiosSetup.ts
@@ -2,11 +2,17 @@ import axios from 'axios';
import authService from './authService';
+const MAX_RETRY_COUNT = 3;
+
axios.interceptors.request.use(
async (config) => {
- const token = await authService.getJwtToken();
- if (token) {
- config.headers['Authorization'] = `Bearer ${token}`;
+ try {
+ const token = await authService.getJwtToken();
+ if (token) {
+ config.headers['Authorization'] = `Bearer ${token}`;
+ }
+ } catch (error) {
+ console.error('Error fetching JWT token', error);
}
return config;
},
@@ -22,27 +28,32 @@ axios.interceptors.response.use(
async (error) => {
const originalRequest = error.config;
- if (error.response.status === 401 && !originalRequest._retry) {
- originalRequest._retry = true;
+ if (!originalRequest._retryCount) {
+ originalRequest._retryCount = 0;
+ }
+
+ if (originalRequest._retryCount < MAX_RETRY_COUNT && error.response.status === 401) {
+ const jwtToken = await authService.getJwtToken();
+ if (!jwtToken) {
+ return Promise.reject(error);
+ }
+
+ originalRequest._retryCount++;
try {
await authService.refreshJwtToken();
- const token = await authService.getJwtToken();
- if (token) {
- axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
- return axios(originalRequest);
- }
- } catch (e) {
- return Promise.reject(e);
+ console.log('Token refreshed');
+ return axios(originalRequest);
+ } catch (error) {
+ console.log('Error refreshing token', error);
+ return Promise.reject(error);
}
}
- if (error.response.status === 403 && !originalRequest._retry) {
- originalRequest._retry = true;
- try {
- return authService.authoriseGitHubApp();
- } catch (e) {
- return Promise.reject(e);
- }
+
+ if (error.response.status === 403) {
+ await authService.logout();
+ authService.authorizeGitHubApp();
}
+
return Promise.reject(error);
},
);
diff --git a/src/services/examplesService.ts b/src/services/examplesService.ts
new file mode 100644
index 00000000..05e5493a
--- /dev/null
+++ b/src/services/examplesService.ts
@@ -0,0 +1,95 @@
+import axios from 'axios';
+
+import { SERVER_URL } from '@constants/auth';
+
+interface GistFile {
+ filename?: string;
+ content: string;
+}
+
+interface UploadCustomExampleData {
+ code: string;
+ description: string;
+ exampleName: string;
+}
+
+const createExample = async (data: UploadCustomExampleData) => {
+ const files: { [key: string]: GistFile } = {
+ 'example': {
+ filename: data.exampleName,
+ content: data.code,
+ },
+ };
+
+ const body = {
+ name: data.exampleName,
+ description: data.description,
+ files,
+ publicGist: true,
+ };
+
+ const response = await axios.post(`${SERVER_URL}/gists`, body);
+
+ return response.data;
+};
+
+const getUserExamples = async () => {
+ const response = await axios.get(`${SERVER_URL}/gists`);
+
+ return response.data;
+};
+
+const getExampleContent = async (id: string) => {
+ try {
+ const { data } = await axios.get(`${SERVER_URL}/gists/${id}`, { withCredentials: true });
+
+ return data;
+ } catch (error) {
+ console.log('error', error);
+ }
+};
+
+const updateExampleInfo = async (id: string, name: string, description: string) => {
+ const body = {
+ name,
+ description,
+ };
+
+ const { data } = await axios.patch(`${SERVER_URL}/gists/edit-info/${id}`, body);
+
+ return data;
+};
+
+const updateExampleContent = async (id: string, name: string, data: string) => {
+ const files: { [key: string]: GistFile } = {
+ [name]: {
+ content: data,
+ },
+ };
+
+ const body = {
+ files,
+ };
+
+ try {
+ const gists = await axios.patch(`${SERVER_URL}/gists/edit-content/${id}`, body);
+ return gists;
+ } catch (error) {
+ console.log('error', error);
+ }
+
+};
+
+const deleteExample = async (id: string) => {
+ const { data } = await axios.delete(`${SERVER_URL}/gists/${id}`, { withCredentials: true });
+ return data;
+};
+
+export default {
+ createExample,
+ getUserExamples,
+ getExampleContent,
+ updateExampleContent,
+ updateExampleInfo,
+ deleteExample,
+};
diff --git a/src/services/gistService.ts b/src/services/gistService.ts
deleted file mode 100644
index 3f1750de..00000000
--- a/src/services/gistService.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import axios from 'axios';
-
-import { SERVER_URL } from '@constants/auth';
-import { snippets } from '@constants/snippets';
-
-import authService from './authService';
-
-export const uploadSnippet = async () => {
-
- const jwtToken = authService.getJwtToken();
-
- if (!jwtToken) {
- return;
- }
-
- const snippet = snippets[0].code;
- const files = {
- 'snippet.tsx': {
- content: snippet,
- },
- 'package.json': {
- content: 'snippet2 content',
- },
- 'readme.txt': {
- content: 'readme content',
- },
- };
-
- const body = {
- description: 'Snippet description',
- files,
- publicGist: true,
- };
- const response = await axios.post(`${SERVER_URL}/gists`, body, { withCredentials: true });
-
- console.log('response', response);
-};
diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts
index 94f800a2..339496f2 100644
--- a/src/stores/auth/index.ts
+++ b/src/stores/auth/index.ts
@@ -4,55 +4,89 @@ import authService from '@services/authService';
import { createSelectors } from '../createSelectors';
-import type { IAuthResponse } from '@custom-types/auth';
-
interface StoreInterface {
- jwtToken: IAuthResponse['jwtToken'];
+ user: {
+ name: string;
+ avatar: string;
+ };
+ jwtToken: string | null;
jwtTokenIsLoading: boolean;
actions: {
resetStore: () => void;
login: (gitHubCode: string) => Promise;
authorize: () => void;
- refreshJwtToken: () => void;
- logout: () => void;
+ refreshJwtToken: () => Promise;
+ logout: () => Promise;
};
- init: () => void;
+ init: () => Promise;
}
-const initialState = {
+const initialState: Omit = {
+ user: {
+ name: '',
+ avatar: '',
+ },
jwtToken: '',
- jwtTokenIsLoading: true,
+ jwtTokenIsLoading: false,
};
const baseStore = create()((set) => ({
...initialState,
actions: {
- resetStore: () => {
- set(initialState);
- },
- login: async (gitHubCode: string) => {
- const { jwtToken } = await authService.login(gitHubCode);
- set({ jwtToken });
+ resetStore: () => set({ ...initialState }),
- return jwtToken;
+ login: async (gitHubCode: string) => {
+ try {
+ const { userName, userAvatar, jwtToken } = await authService.login(gitHubCode);
+ set({ user: { name: userName, avatar: userAvatar }, jwtToken });
+ return jwtToken;
+ } catch (error) {
+ console.error('Login failed', error);
+ throw error;
+ }
},
+
authorize: () => {
- authService.authoriseGitHubApp();
+ authService.authorizeGitHubApp();
},
refreshJwtToken: async () => {
- const { jwtToken } = await authService.refreshJwtToken();
- set({ jwtToken });
+ try {
+ const { jwtToken } = await authService.refreshJwtToken();
+ set((state) => (state.jwtToken !== jwtToken ? { jwtToken } : state));
+ } catch (error) {
+ console.error('Token refresh failed', error);
+ throw error;
+ }
},
logout: async () => {
- await authService.logout();
- set({ jwtToken: '' });
+ try {
+ await authService.logout();
+ set({ ...initialState });
+ } catch (error) {
+ console.error('Logout failed', error);
+ throw error;
+ }
},
},
+
init: async () => {
- const token = await authService.getJwtToken();
- set({ jwtToken: token || '', jwtTokenIsLoading: false });
+ try {
+ const token = await authService.getJwtToken();
+
+ if (!token) {
+ set({ jwtTokenIsLoading: false });
+ return;
+ }
+
+ const user = await authService.getUserData();
+
+ set({ jwtToken: token, user, jwtTokenIsLoading: false });
+ } catch (error) {
+ console.error('Initialization failed', error);
+ set({ jwtTokenIsLoading: false });
+ }
},
}));
diff --git a/src/stores/examples/index.ts b/src/stores/examples/index.ts
new file mode 100644
index 00000000..97674c41
--- /dev/null
+++ b/src/stores/examples/index.ts
@@ -0,0 +1,255 @@
+import { busDispatch } from '@pivanov/event-bus';
+import { toast } from 'react-hot-toast';
+import { create } from 'zustand';
+
+import { snippets } from '@constants/snippets';
+import authService from '@services/authService';
+import gistService from '@services/examplesService';
+import { createSelectors } from 'src/stores/createSelectors';
+
+import type { ICodeExample } from '@custom-types/codeSnippet';
+import type {
+ IDeleteExampleModalClose,
+ IEditExampleInfoModalClose,
+ IUploadExampleModalClose,
+} from '@custom-types/eventBus';
+
+interface UploadCustomExampleData {
+ code: string;
+ description: string;
+ exampleName: string;
+}
+
+interface StoreInterface {
+ examples: ICodeExample[];
+ selectedExample: ICodeExample;
+ loading: {
+ isGettingExamples: boolean;
+ isCreatingExample: boolean;
+ isSavingInfo: boolean;
+ isSavingContent: boolean;
+ isDeleting: boolean;
+ };
+ actions: {
+ resetStore: () => void;
+ createExample: (data: UploadCustomExampleData) => void;
+ loadExampleContent: (id: string, type: string) => void;
+ updateExampleInfo: (id: string, exampleName: string, description: string) => void;
+ updateExampleContent: (data: string) => void;
+ getExamples: () => void;
+ deleteExample: (id: string) => void;
+ };
+ init: () => void;
+}
+
+const initialState: Omit = {
+ examples: [],
+ selectedExample: {
+ id: '',
+ name: '',
+ description: '',
+ code: '',
+ },
+ loading: {
+ isGettingExamples: false,
+ isCreatingExample: false,
+ isSavingInfo: false,
+ isSavingContent: false,
+ isDeleting: false,
+ },
+};
+
+const handleLoading = (set: (fn: (state: StoreInterface) => StoreInterface) => void, key: keyof StoreInterface['loading'], value: boolean) => {
+ set((state: StoreInterface) => ({ ...state, loading: { ...state.loading, [key]: value } }));
+};
+
+const baseStore = create()((set, get) => ({
+ ...initialState,
+ actions: {
+ createExample: async (data: UploadCustomExampleData) => {
+ const token = await authService.getJwtToken();
+ if (!token) return;
+
+ handleLoading(set, 'isCreatingExample', true);
+
+ if (!data.code || !data.description || !data.exampleName) {
+ console.error('Invalid input data: Missing code, description, or exampleName');
+ handleLoading(set, 'isCreatingExample', false);
+ return;
+ }
+
+ if (get().examples.some((example) => example.name === data.exampleName)) {
+ toast.error('Example with this name already exists');
+ handleLoading(set, 'isCreatingExample', false);
+ return;
+ }
+
+ try {
+ const { name, id, description } = await gistService.createExample(data);
+ set((state) => ({
+ examples: [
+ ...state.examples,
+ { name, id, description },
+ ],
+ }));
+
+ if (id) {
+ busDispatch({
+ type: '@@-close-upload-example-modal',
+ data: id,
+ });
+ }
+
+ toast.success('Snippet uploaded');
+ } catch (error) {
+ console.error('Error uploading snippet', error);
+ } finally {
+ handleLoading(set, 'isCreatingExample', false);
+ }
+ },
+
+ getExamples: async () => {
+ const token = await authService.getJwtToken();
+ if (!token) return;
+
+ handleLoading(set, 'isGettingExamples', true);
+
+ try {
+ const data = await gistService.getUserExamples();
+ set({ examples: data });
+ } catch (error) {
+ console.error('Error getting examples', error);
+ } finally {
+ handleLoading(set, 'isGettingExamples', false);
+ }
+ },
+
+ loadExampleContent: async (id: string, type: string) => {
+ busDispatch({ type: '@@-monaco-editor-show-loading' });
+ busDispatch({ type: '@@-problems-message', data: [] });
+ busDispatch({ type: '@@-console-message-reset' });
+ busDispatch({ type: '@@-monaco-editor-types-progress', data: 0 });
+
+ try {
+ let selectedExample;
+
+ if (type === 'default') {
+ selectedExample = snippets.find((snippet) => snippet.id === id);
+ } else if (type === 'custom') {
+ const exampleData = await gistService.getExampleContent(id);
+ if (exampleData) {
+ selectedExample = { ...exampleData, id };
+ }
+ }
+
+ set({
+ selectedExample: selectedExample || { id: '', name: 'Example Not Found', description: '' },
+ });
+ } catch (error) {
+ console.error('Error loading example content', error);
+ throw error;
+ } finally {
+ busDispatch({ type: '@@-monaco-editor-hide-loading' });
+ }
+ },
+
+ updateExampleInfo: async (id: string, exampleName: string, description: string) => {
+ const token = await authService.getJwtToken();
+ if (!token) return;
+
+ if (!id || !exampleName || !description) {
+ toast.error('Invalid input data: Missing id, exampleName, or description');
+ return;
+ }
+
+ handleLoading(set, 'isSavingInfo', true);
+
+ try {
+ const editedGist = await gistService.updateExampleInfo(id, exampleName, description);
+
+ const updatedExamples = get().examples.map((example) =>
+ example.id === id ? { ...example, ...editedGist } : example,
+ );
+
+ if (get().selectedExample.id === id) {
+ set({ selectedExample: editedGist });
+ }
+
+ set({ examples: updatedExamples });
+ busDispatch('@@-close-edit-example-modal');
+ toast.success('Example Information Updated!');
+ } catch (error) {
+ console.error('Error updating snippet', error);
+ toast.error('Failed to update example information');
+ } finally {
+ handleLoading(set, 'isSavingInfo', false);
+ }
+ },
+
+ updateExampleContent: async (data: string) => {
+ const token = await authService.getJwtToken();
+ if (!token) return;
+
+ handleLoading(set, 'isSavingContent', true);
+ const { name, id } = get().selectedExample;
+
+ if (!id || !data || !name) {
+ toast.error('No selected example id or data');
+ return;
+ }
+
+ try {
+ await gistService.updateExampleContent(id, name, data);
+ busDispatch({
+ type: '@@-close-upload-example-modal',
+ data: id,
+ });
+ toast.success('Example Content Updated!');
+ } catch (error) {
+ console.error('Error updating snippet', error);
+ } finally {
+ handleLoading(set, 'isSavingContent', false);
+ }
+ },
+
+ deleteExample: async (id: string) => {
+ const token = await authService.getJwtToken();
+ if (!token) return;
+
+ if (!id) {
+ toast.error('No selected example id');
+ return;
+ }
+
+ handleLoading(set, 'isDeleting', true);
+
+ try {
+ const updatedExampleList = get().examples.filter((example) => example.id !== id);
+ set({ examples: updatedExampleList });
+
+ busDispatch({
+ type: '@@-close-delete-example-modal',
+ });
+
+ await gistService.deleteExample(id);
+ toast.success('Snippet deleted');
+ } catch (error) {
+ console.error('Error deleting snippet', error);
+ } finally {
+ handleLoading(set, 'isDeleting', false);
+ }
+ },
+
+ resetStore: () => set({ ...initialState }),
+ },
+
+ init: async () => {
+ const token = await authService.getJwtToken();
+ if (!token) return;
+
+ get().actions.getExamples();
+ },
+}));
+
+export const baseStoreCustomExamples = baseStore;
+export const useStoreCustomExamples = createSelectors(baseStore);
diff --git a/src/types/auth.ts b/src/types/auth.ts
index 8e42a9c0..87147d5a 100644
--- a/src/types/auth.ts
+++ b/src/types/auth.ts
@@ -1,3 +1,5 @@
export interface IAuthResponse {
+ userName: string;
+ userAvatar: string;
jwtToken: string;
}
diff --git a/src/types/codeSnippet.ts b/src/types/codeSnippet.ts
index e3c965fb..78568143 100644
--- a/src/types/codeSnippet.ts
+++ b/src/types/codeSnippet.ts
@@ -1,5 +1,6 @@
-export interface ICodeSnippet {
- id: number;
+export interface ICodeExample {
+ id: string;
name: string;
- code: string;
+ description?: string;
+ code?: string;
}
diff --git a/src/types/eventBus.ts b/src/types/eventBus.ts
index 770cb1eb..64bd03e2 100644
--- a/src/types/eventBus.ts
+++ b/src/types/eventBus.ts
@@ -11,7 +11,10 @@ export interface IEventBusStoreSize {
export interface IEventBusMonacoEditorLoadSnippet {
type: '@@-monaco-editor-load-snippet';
- data: number;
+ data: {
+ code: string;
+ type: string;
+ };
}
export interface IEventBusMonacoEditorShowPreview {
@@ -21,7 +24,7 @@ export interface IEventBusMonacoEditorShowPreview {
export interface IEventBusMonacoEditorUpdateCode {
type: '@@-monaco-editor-update-code';
- data: string;
+ data: string | null;
}
export interface IEventBusMonacoEditorUpdateCursorPosition {
@@ -78,3 +81,15 @@ export interface IEventBusForksReceiveUpdate {
export interface IEventBusNavLinkClick {
type: '@@-navlink-click';
}
+
+export interface IUploadExampleModalClose {
+ type: '@@-close-upload-example-modal';
+ data: string;
+}
+
+export interface IEditExampleInfoModalClose {
+ type: '@@-close-edit-example-modal';
+}
+export interface IDeleteExampleModalClose {
+ type: '@@-close-delete-example-modal';
+}
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index c9ac02b3..b83fcbb5 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -122,6 +122,11 @@ export const truncateAddress = (address: string = '', chars: number = 3) => {
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
};
+export const truncateString = (str: string, length: number) => {
+
+ return str?.length > length ? `${str.slice(0, length)}...` : str;
+};
+
export const formatTokenValue = (
{ value = 0,
tokenDecimals = 10,
diff --git a/src/views/blockDetails/blockBody/index.tsx b/src/views/blockDetails/blockBody/index.tsx
index ac881717..5300c7b3 100644
--- a/src/views/blockDetails/blockBody/index.tsx
+++ b/src/views/blockDetails/blockBody/index.tsx
@@ -9,6 +9,7 @@ import {
import { Icon } from '@components/icon';
import { ModalJSONViewer } from '@components/modals/modalJSONViewer';
import { Tabs } from '@components/tabs';
+import { ToolTip } from '@components/tooltTip';
import { cn } from '@utils/helpers';
import type {
@@ -157,11 +158,15 @@ export const BlockBody = (props: BlockBodyProps) => {
)
-
+
+
+
+
+
|
);
@@ -232,11 +237,15 @@ export const BlockBody = (props: BlockBodyProps) => {
{event.phase.type} |
-
+
+
+
+
+
|
))
diff --git a/src/views/blockDetails/blockHeader/index.tsx b/src/views/blockDetails/blockHeader/index.tsx
index c2e4a0e2..7f47b826 100644
--- a/src/views/blockDetails/blockHeader/index.tsx
+++ b/src/views/blockDetails/blockHeader/index.tsx
@@ -9,6 +9,7 @@ import { CopyToClipboard } from '@components/copyToClipboard';
import { Icon } from '@components/icon';
import { ToggleButton } from '@components/toggleButton';
import { useStoreChain } from '@stores';
+import { cn } from '@utils/helpers';
import styles from '../styles.module.css';
@@ -22,22 +23,43 @@ interface DetailRowProps {
label: string;
value: string | undefined;
isCopyable?: boolean;
+ iconComponent?: React.ReactNode;
}
const DetailRow = (props: DetailRowProps) => {
- const { label, value } = props;
+ const { label, value, iconComponent } = props;
return (
-
+
{label}
-
{value}
+ {iconComponent &&
{iconComponent}}
+
{value && (
- {({ ClipboardIcon }) => <>{ClipboardIcon}>}
+ {({ ClipboardIcon, text, onClick }) => (
+
+
+ {text}
+
+
+ {ClipboardIcon}
+
+
+ )}
)}
@@ -67,12 +89,22 @@ export const BlockHeader = (props: BlockHeaderProps) => {
Time stamp
- {formattedTimestamp}
+
- UTC
+
@@ -103,28 +135,17 @@ export const BlockHeader = (props: BlockHeaderProps) => {
value={headerData.extrinsicRoot}
/>
{headerData.identity && (
-
-
Validator
-
+
-
- {headerData.identity.name}
-
- {headerData.identity.address}
-
- {({ ClipboardIcon }) => <>{ClipboardIcon}>}
-
-
-
-
-
+ )}
+ />
)}
void;
classes?: string;
fill?: string;
toolTip?: string;
+ isLoading?: boolean;
+ disabled?: boolean;
+ onClick?: () => void;
}
export const ActionButton = (props: ActionButtonProps) => {
- const { iconName, onClick, classes, toolTip } = props;
+ const { iconName, classes, toolTip, isLoading = false, disabled, onClick } = props;
const button = (
);
diff --git a/src/views/codeEditor/components/debugPanel/console/consoleActions.tsx b/src/views/codeEditor/components/debugPanel/console/consoleActions.tsx
index 6f6b715e..42fac67f 100644
--- a/src/views/codeEditor/components/debugPanel/console/consoleActions.tsx
+++ b/src/views/codeEditor/components/debugPanel/console/consoleActions.tsx
@@ -9,6 +9,7 @@ import {
import { CopyToClipboard } from '@components/copyToClipboard';
import { Icon } from '@components/icon';
+import { ToolTip } from '@components/tooltTip';
import { cn } from '@utils/helpers';
import type {
@@ -46,7 +47,7 @@ export const ConsoleActions = () => {
return (
{
},
)}
>
-
+
+
+
message).join('\n')}
toastMessage="console output"
diff --git a/src/views/codeEditor/components/debugPanel/console/index.tsx b/src/views/codeEditor/components/debugPanel/console/index.tsx
index 8e4610ba..e0503618 100644
--- a/src/views/codeEditor/components/debugPanel/console/index.tsx
+++ b/src/views/codeEditor/components/debugPanel/console/index.tsx
@@ -111,9 +111,8 @@ export const Console = () => {
return (
{
return (
{
- const refCode = useRef('');
+ const { code } = useStoreCustomExamples.use.selectedExample();
+ const isAuthenticated = useStoreAuth.use.jwtToken?.();
+
+ const [
+ isDownloading,
+ setIsDownloading,
+ ] = useState(false);
+
+ const [
+ SaveExampleModal,
+ toggleVisibility,
+ ] = useToggleVisibility(ModalSaveExample);
+
+ const refCode = useRef(code);
+
const [
isRunning,
setIsRunning,
@@ -48,6 +65,7 @@ export const EditorActions = () => {
const handleDownload = useCallback(async () => {
const getPackageVersion = async (packageName: string) => {
try {
+ setIsDownloading(true);
const response = await fetch(packageName);
if (response.ok) {
const html = await response.text();
@@ -58,12 +76,14 @@ export const EditorActions = () => {
}
} catch (error) {
console.error(`Failed to fetch version for package ${packageName}:`, error);
+ } finally {
+ setIsDownloading(false);
}
return 'latest';
};
const generatePackageJson = async () => {
- const importMap = mergeImportMap(defaultImportMap, getImportMap(refCode.current));
+ const importMap = mergeImportMap(defaultImportMap, getImportMap(refCode.current || ''));
const dependencies: { [key: string]: string } = {};
for (const url of Object.values(importMap.imports || {})) {
@@ -94,7 +114,7 @@ export const EditorActions = () => {
const files = [
{
name: 'index.tsx',
- content: refCode.current,
+ content: refCode.current || '',
},
{
name: 'package.json',
@@ -105,53 +125,80 @@ export const EditorActions = () => {
await downloadFiles(files);
}, []);
+ const handleShare = useCallback(() => {
+ const url = window.location.href;
+ navigator.clipboard.writeText(url).then(() => {
+ toast.success('URL copied to clipboard');
+ }).catch((err) => {
+ console.error('Failed to copy URL to clipboard:', err);
+ toast.error('Failed to copy URL to clipboard');
+ });
+ }, []);
+
useEventBus('@@-monaco-editor-update-code', ({ data }) => {
- refCode.current = data;
- });
+ if (data !== null) {
+ refCode.current = data;
+ }
- useEventBus('@@-monaco-editor-load-snippet', () => {
handleStop();
});
return (
- {isRunning
- ? (
-
- )
- : (
-
- )}
+ {
+ isRunning
+ ? (
+
+ )
+ : (
+
+ )
+ }
+
);
};
diff --git a/src/views/codeEditor/components/monacoEditor/index.tsx b/src/views/codeEditor/components/monacoEditor/index.tsx
index 1d74242b..6440e3c1 100644
--- a/src/views/codeEditor/components/monacoEditor/index.tsx
+++ b/src/views/codeEditor/components/monacoEditor/index.tsx
@@ -2,9 +2,9 @@ import {
busDispatch,
useEventBus,
} from '@pivanov/event-bus';
+import { useToggleVisibility } from '@pivanov/use-toggle-visibility';
import * as PAPI_SIGNER from '@polkadot-api/signer';
import * as PAPI_WS_PROVIDER_WEB from '@polkadot-api/ws-provider/web';
-import { shikiToMonaco } from '@shikijs/monaco/index.mjs';
import * as monaco from 'monaco-editor';
import {
useCallback,
@@ -12,14 +12,15 @@ import {
useRef,
useState,
} from 'react';
-import { getSingletonHighlighter } from 'shiki/index.mjs';
-import { snippets } from '@constants/snippets';
-import { useStoreUI } from '@stores';
+import { ModalGithubLogin } from '@components/modals/modalGithubLogin';
+import {
+ useStoreAuth,
+ useStoreUI,
+} from '@stores';
import {
cn,
getSearchParam,
- setSearchParam,
} from '@utils/helpers';
import { storageSetItem } from '@utils/storage';
import {
@@ -32,11 +33,11 @@ import {
} from '@views/codeEditor/helpers';
import { monacoEditorConfig } from '@views/codeEditor/monaco-editor-config';
import { Progress } from '@views/codeEditor/progress';
+import { useStoreCustomExamples } from 'src/stores/examples';
import type {
IEventBusIframeDestroy,
IEventBusMonacoEditorExecuteSnippet,
- IEventBusMonacoEditorLoadSnippet,
IEventBusMonacoEditorUpdateCursorPosition,
} from '@custom-types/eventBus';
@@ -95,29 +96,31 @@ const compilerOptions: monaco.languages.typescript.CompilerOptions = {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions);
const checkTheme = async (theme: string) => {
- const currentTheme = theme === 'dark' ? 'github-dark' : 'github-light';
- const highlighter = await getSingletonHighlighter({
- themes: [
- 'github-dark',
- 'github-light',
- ],
- langs: [
- 'tsx',
- 'typescript',
- 'json',
- ],
- });
-
- shikiToMonaco(highlighter, monaco);
+ const currentTheme = theme === 'dark' ? 'dark-theme' : 'light-theme';
monaco.editor.setTheme(currentTheme);
};
+// Utility to handle showing/hiding preview based on code content
+const triggerPreview = (code: string) => {
+ busDispatch({
+ type: '@@-monaco-editor-show-preview',
+ data: code.includes('createRoot'),
+ });
+};
interface IMonacoEditorProps {
classNames?: string;
}
export const MonacoEditor = (props: IMonacoEditorProps) => {
const { classNames } = props;
+ const { loadExampleContent } = useStoreCustomExamples.use.actions();
+ const { code } = useStoreCustomExamples.use.selectedExample();
+ const isAuthenticated = useStoreAuth.use.jwtToken?.();
+
+ const [
+ GithubModal,
+ toggleVisibility,
+ ] = useToggleVisibility(ModalGithubLogin);
const refTimeout = useRef();
const refSnippet = useRef('');
@@ -194,7 +197,7 @@ export const MonacoEditor = (props: IMonacoEditorProps) => {
void checkTheme(theme);
void triggerValidation();
},
- () => {},
+ () => { },
(userFacingMessage, error) => {
console.error('Custom error handling:', userFacingMessage, error);
},
@@ -223,57 +226,21 @@ export const MonacoEditor = (props: IMonacoEditorProps) => {
void fetchType(refSnippet.current);
};
- const loadSnippet = useCallback(async (snippetIndex: number | null) => {
+ const loadExample = useCallback(async (code: string) => {
clearTimeout(refTimeout.current);
- busDispatch({
- type: '@@-problems-message',
- data: [],
- });
-
- busDispatch({
- type: '@@-console-message-reset',
- });
-
- busDispatch({
- type: '@@-monaco-editor-types-progress',
- data: 0,
- });
-
- let code = 'console.log("Hello, World!");';
- if (!!snippetIndex) {
- const selectedCodeSnippet = snippets.find((f) => f.id === snippetIndex) || snippets[0];
- refSnippetIndex.current = String(selectedCodeSnippet.id);
-
- // const isTempVersionExist = await storageExists(STORAGE_CACHE_NAME, `${STORAGE_PREFIX}-${snippetIndex}`);
- code = selectedCodeSnippet.code;
-
- // if (isTempVersionExist) {
- // const existingCode = await storageGetItem(STORAGE_CACHE_NAME, `${STORAGE_PREFIX}-${snippetIndex}`);
- // code = existingCode || code;
- // }
-
- setSearchParam('s', snippetIndex);
- }
-
+ refSnippetIndex.current = String('id');
refSnippet.current = await formatCode(code);
createNewModel(refSnippet.current);
+ triggerPreview(refSnippet.current);
- busDispatch({
- type: '@@-monaco-editor-show-preview',
- data: refSnippet.current.includes('createRoot'),
- });
-
- refTimeout.current = setTimeout(async () => {
- busDispatch({
- type: '@@-monaco-editor-hide-loading',
- });
+ refTimeout.current = setTimeout(() => {
+ busDispatch({ type: '@@-monaco-editor-hide-loading' });
}, 400);
-
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const updateMonacoCursorPositon = useCallback((currentPosition: monaco.Position) => {
+ const updateMonacoCursorPosition = useCallback((currentPosition: monaco.Position) => {
if (currentPosition) {
refMonacoEditor.current?.setPosition(currentPosition);
refMonacoEditor.current?.revealPositionInCenter(currentPosition);
@@ -282,9 +249,26 @@ export const MonacoEditor = (props: IMonacoEditorProps) => {
}, []);
useEffect(() => {
- const snippetIndex = getSearchParam('s');
- void loadSnippet(!!snippetIndex ? Number(snippetIndex) : null);
+ if (code) {
+ void loadExample(code);
+ }
+ }, [
+ code,
+ loadExample,
+ ]);
+ useEffect(() => {
+ const defaultId = getSearchParam('d');
+ const customId = getSearchParam('c');
+
+ if (defaultId) {
+ void loadExampleContent(defaultId, 'default');
+ } else if (customId) {
+ if (!isAuthenticated) {
+ toggleVisibility();
+ }
+ void loadExampleContent(customId, 'custom');
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -311,7 +295,7 @@ export const MonacoEditor = (props: IMonacoEditorProps) => {
const code = refMonacoEditor.current.getValue() || '';
refSnippet.current = await formatCode(code);
- updateMonacoCursorPositon(currentPosition!);
+ updateMonacoCursorPosition(currentPosition!);
await fetchType(refSnippet.current);
@@ -362,7 +346,7 @@ export const MonacoEditor = (props: IMonacoEditorProps) => {
}, []);
useEventBus('@@-monaco-editor-update-cursor-position', ({ data }) => {
- updateMonacoCursorPositon(data);
+ updateMonacoCursorPosition(data);
});
useEventBus('@@-monaco-editor-execute-snippet', () => {
@@ -380,10 +364,6 @@ export const MonacoEditor = (props: IMonacoEditorProps) => {
refMonacoEditor.current?.focus();
});
- useEventBus('@@-monaco-editor-load-snippet', ({ data }) => {
- void loadSnippet(data);
- });
-
return (
{
classNames="absolute top-2 right-6 z-100"
size={18}
/>
+
);
};
diff --git a/src/views/codeEditor/components/selectExample/examplesList/customExamples.tsx b/src/views/codeEditor/components/selectExample/examplesList/customExamples.tsx
new file mode 100644
index 00000000..6d1243fb
--- /dev/null
+++ b/src/views/codeEditor/components/selectExample/examplesList/customExamples.tsx
@@ -0,0 +1,179 @@
+import { useToggleVisibility } from '@pivanov/use-toggle-visibility';
+import {
+ useCallback,
+ useRef,
+} from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { ExampleNotFound } from '@components/exampleNotFound';
+import { Icon } from '@components/icon';
+import { Loader } from '@components/loader';
+import { ModalDeleteExample } from '@components/modals/modalDeleteExample';
+import { ModalEditExampleInfo } from '@components/modals/modalEditExampleInfo';
+import { PDScrollArea } from '@components/pdScrollArea';
+import {
+ cn,
+ sleep,
+ truncateString,
+} from '@utils/helpers';
+import { useStoreCustomExamples } from 'src/stores/examples';
+
+interface ExamplesListProps {
+ handleClose: () => void;
+}
+export const CustomExampleList = (props: ExamplesListProps) => {
+ const { handleClose } = props;
+ const customExamples = useStoreCustomExamples.use.examples();
+ const { isGettingExamples } = useStoreCustomExamples.use.loading();
+ const { name: selectedExample } = useStoreCustomExamples.use.selectedExample();
+ const { loadExampleContent } = useStoreCustomExamples.use.actions();
+ const navigate = useNavigate();
+
+ const exampleId = useRef('');
+
+ const [
+ EditExampleInfo,
+ toggleEditExampleInfoModal,
+ ] = useToggleVisibility(ModalEditExampleInfo);
+
+ const [
+ DeleteExampleModal,
+ toggleDeleteModal,
+ ] = useToggleVisibility(ModalDeleteExample);
+
+ const handleDeleteExample = useCallback((e: React.MouseEvent) => {
+ const id = e.currentTarget.getAttribute('data-example-index');
+ exampleId.current = id as string;
+ toggleDeleteModal();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const handleEditExample = useCallback((e: React.MouseEvent) => {
+ const id = e.currentTarget.getAttribute('data-example-index');
+ exampleId.current = id as string;
+ toggleEditExampleInfoModal();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const handleChangeExample = useCallback(async (e: React.MouseEvent): Promise => {
+ const id = e.currentTarget.getAttribute('data-example-index') ?? '';
+
+ loadExampleContent(id, 'custom');
+ navigate(`/code?c=${id}`);
+ handleClose();
+ void sleep(400);
+ }, [
+ handleClose,
+ loadExampleContent,
+ navigate,
+ ]);
+
+ const handleNavigateToCreateExample = useCallback(() => {
+ navigate('/code?d=1');
+ loadExampleContent('1', 'default');
+ handleClose();
+ }, [
+ handleClose,
+ loadExampleContent,
+ navigate,
+ ]);
+
+ if (isGettingExamples) {
+ return (
+
+
+
+ );
+ }
+
+ if (!customExamples.length) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {
+ customExamples?.map((example) => (
+ -
+
+
+
+
+
+
+
+
+
+
+ ))
+ }
+
+
+
+
+ >
+ );
+};
diff --git a/src/views/codeEditor/components/selectExample/examplesList/defaultExamples.tsx b/src/views/codeEditor/components/selectExample/examplesList/defaultExamples.tsx
new file mode 100644
index 00000000..50e82078
--- /dev/null
+++ b/src/views/codeEditor/components/selectExample/examplesList/defaultExamples.tsx
@@ -0,0 +1,77 @@
+// import { ModalSaveExample } from '@components/modals/modalSaveExample';
+import { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { PDScrollArea } from '@components/pdScrollArea';
+import { snippets } from '@constants/snippets';
+import {
+ cn,
+ sleep,
+ truncateString,
+} from '@utils/helpers';
+import { useStoreCustomExamples } from 'src/stores/examples';
+
+interface DefaultExamplesListProps {
+ handleClose: () => void;
+}
+
+export const DefaultExamplesList = (props: DefaultExamplesListProps) => {
+ const { handleClose } = props;
+
+ const { loadExampleContent } = useStoreCustomExamples.use.actions();
+ const { name: selectedExample } = useStoreCustomExamples.use.selectedExample();
+ const navigate = useNavigate();
+
+ const handleChangeExample = useCallback(async (e: React.MouseEvent): Promise => {
+ const id = e.currentTarget.getAttribute('data-example-index') ?? '';
+
+ loadExampleContent(id, 'default');
+ navigate(`/code?d=${id}`);
+ handleClose();
+ void sleep(400);
+ }, [
+ handleClose,
+ loadExampleContent,
+ navigate,
+ ]);
+
+ return (
+
+
+ {
+ snippets?.map((example) => (
+ -
+
+
+
+ ))
+ }
+
+
+ );
+};
diff --git a/src/views/codeEditor/components/selectExample/index.tsx b/src/views/codeEditor/components/selectExample/index.tsx
index 4c0300a6..7fe5fd4e 100644
--- a/src/views/codeEditor/components/selectExample/index.tsx
+++ b/src/views/codeEditor/components/selectExample/index.tsx
@@ -1,64 +1,42 @@
-import { busDispatch } from '@pivanov/event-bus';
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react';
-import {
- useNavigate,
- useSearchParams,
-} from 'react-router-dom';
import { Icon } from '@components/icon';
-import { PDScrollArea } from '@components/pdScrollArea';
import { Tabs } from '@components/tabs';
-import { snippets } from '@constants/snippets';
import {
cn,
- sleep,
+ truncateString,
} from '@utils/helpers';
-
-import type { IEventBusMonacoEditorLoadSnippet } from '@custom-types/eventBus';
+import { CustomExampleList } from '@views/codeEditor/components/selectExample/examplesList/customExamples';
+import { DefaultExamplesList } from '@views/codeEditor/components/selectExample/examplesList/defaultExamples';
+import { useStoreCustomExamples } from 'src/stores/examples';
export const SelectExample = () => {
- const navigate = useNavigate();
+ const { name: selectedExample } = useStoreCustomExamples.use.selectedExample();
+ const { getExamples } = useStoreCustomExamples.use.actions();
+
+ const refContainer = useRef(null);
const [
isOpened,
setIsOpened,
] = useState(false);
- const [searchParams] = useSearchParams();
- const selectedSnippet = searchParams.get('s');
- const selectedSnippetName = snippets.find((snippet) => snippet.id === Number(selectedSnippet))?.name;
-
- const refContainer = useRef(null);
+ const [
+ initialTab,
+ setInitialTab,
+ ] = useState(0);
const handleSetOpen = useCallback(() => {
setIsOpened((prev) => !prev);
}, []);
- const handleChangeExample = useCallback(async (e: React.MouseEvent) => {
- const snippetIndex = Number(e.currentTarget.getAttribute('data-snippet-index'));
- navigate(`/code?s=${snippetIndex}`);
+ const handleClose = useCallback(() => {
setIsOpened(false);
-
- busDispatch({
- type: '@@-monaco-editor-show-loading',
- });
-
- await sleep(400);
-
- busDispatch({
- type: '@@-monaco-editor-load-snippet',
- data: snippetIndex,
- });
-
- busDispatch({
- type: '@@-monaco-editor-hide-loading',
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
@@ -74,6 +52,11 @@ export const SelectExample = () => {
};
}, []);
+ useEffect(() => {
+ getExamples();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
return (
{
},
)}
>
- {selectedSnippetName || 'Select Example'}
+ {truncateString(selectedExample, 60) || 'Select Example'}
{
)}
>
-
-
- No examples found
-
-
-
-
- {
- snippets.map((snippet) => (
- -
-
-
- ))
- }
-
-
+
+
+
+
);
};
+
diff --git a/src/views/codeEditor/index.tsx b/src/views/codeEditor/index.tsx
index 9ee81901..e99f6d42 100644
--- a/src/views/codeEditor/index.tsx
+++ b/src/views/codeEditor/index.tsx
@@ -1,10 +1,8 @@
import { useEventBus } from '@pivanov/event-bus';
import {
- useCallback,
useRef,
useState,
} from 'react';
-import { toast } from 'react-hot-toast';
import {
Panel,
PanelGroup,
@@ -18,26 +16,23 @@ import { Tabs } from '@components/tabs';
import { useStoreUI } from '@stores';
import { cn } from '@utils/helpers';
import { useResizeObserver } from '@utils/hooks/useResizeObserver';
-import { encodeCodeToBase64 } from '@utils/iframe';
import { SelectExample } from '@views/codeEditor/components/selectExample';
+import { useStoreCustomExamples } from 'src/stores/examples';
-import { ActionButton } from './components/actionButton';
import { DebugPanel } from './components/debugPanel';
import { Iframe } from './components/iframe';
import { MonacoEditor } from './components/monacoEditor';
import { EditorActions } from './components/monacoEditor/actions';
-import type {
- IEventBusMonacoEditorShowPreview,
- IEventBusMonacoEditorUpdateCode,
-} from '@custom-types/eventBus';
+import type { IEventBusMonacoEditorShowPreview } from '@custom-types/eventBus';
const TypeScriptEditor = () => {
const refContainer = useRef(null);
- const refCode = useRef('');
const refTimeout = useRef();
const refCanPreview = useRef(false);
+ const selectedExample = useStoreCustomExamples?.use?.selectedExample() || {};
+ const exampleDescription = selectedExample?.description;
const [refContainerDimensions] = useResizeObserver(refContainer);
const containerWidth = refContainerDimensions?.width;
@@ -48,8 +43,6 @@ const TypeScriptEditor = () => {
const isDesktop = useStoreUI?.use?.isDesktop?.();
- const theme = useStoreUI?.use?.theme?.();
-
useEventBus('@@-monaco-editor-show-preview', ({ data }) => {
refCanPreview.current = data;
});
@@ -66,33 +59,6 @@ const TypeScriptEditor = () => {
setIsLoaded(false);
});
- useEventBus('@@-monaco-editor-update-code', ({ data }) => {
- refCode.current = data;
- });
-
- const shareCode = useCallback(async () => {
- const toastId = 'copy-to-clipboard';
- toast.dismiss(toastId);
-
- const encodedCode = encodeCodeToBase64(refCode.current);
- const sharedUrl = `${window.location.origin}${window.location.pathname}/${encodedCode}`;
-
- try {
- await navigator.clipboard.writeText(sharedUrl);
- toast.success((
-
- Copied
- {' '}
- URL
- {' '}
- to clipboard
-
- ), { id: toastId });
- } catch (err) {
- toast.error('Oops. Something went wrong', { id: toastId });
- }
- }, []);
-
if (!isDesktop) {
return ;
}
@@ -125,24 +91,18 @@ const TypeScriptEditor = () => {
)}
>
-
-
-
@@ -153,12 +113,13 @@ const TypeScriptEditor = () => {
- Readme.md
+ {exampleDescription || 'No description'}
+
@@ -187,22 +148,8 @@ const TypeScriptEditor = () => {
{
refCanPreview.current && (
<>
-
{
if (!client) {
return;
}
- console.log('test log');
resetState();
refSubscription.current = client.bestBlocks$.subscribe((bestBlocks) => {
diff --git a/src/views/forks/virtualizedList.tsx b/src/views/forks/virtualizedList.tsx
index f3ffe3d2..8978b5c2 100644
--- a/src/views/forks/virtualizedList.tsx
+++ b/src/views/forks/virtualizedList.tsx
@@ -214,17 +214,19 @@ export const VirtualizedList = (props: IVirtualizedListProps) => {
/>
)
}
-
- {item.blockHash}
-
-
- {
- ({ ClipboardIcon }) => (
+ {({ ClipboardIcon, text, onClick }) => (
+
+
+ {text}
+
{
>
{ClipboardIcon}
- )
- }
+
+ )}
diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx
index 305a3e92..5b36c464 100644
--- a/src/views/home/index.tsx
+++ b/src/views/home/index.tsx
@@ -34,7 +34,7 @@ const Home = () => {
description={'With devGround’s custom IDE, safely execute selected JavaScript snippets within the ecosystem.'}
iconName="icon-brackets"
title="Developer Console"
- to={hasVisited ? '/code' : '/onboarding'}
+ to={hasVisited ? '/code?d=1' : '/onboarding'}
/>
{HOME_LINKS.map((card) => (
{
{filteredSnippets.map((snippet, index) => (
{
return (
{/* TO DO LIST GITHUB EXAMPLES */}
-
+
);
};