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 + ? ( +
+
+ avatar + {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

+ +