diff --git a/src/components/B4ACodeTree/B4ACodeTree.react.js b/src/components/B4ACodeTree/B4ACodeTree.react.js index 881dbe1016..1886b22fae 100644 --- a/src/components/B4ACodeTree/B4ACodeTree.react.js +++ b/src/components/B4ACodeTree/B4ACodeTree.react.js @@ -11,6 +11,8 @@ import B4ATreeActions from 'components/B4ACodeTree/B4ATreeActions'; import Swal from 'sweetalert2'; import folderInfoIcon from './icons/folder-info.png'; import B4aEmptyState from 'components/B4aEmptyState/B4aEmptyState.react'; +import B4aCloudEmpty from 'components/B4aCloudEmpty/B4aCloudEmpty.react'; +import B4aCloudPublicEmpty from 'components/B4aCloudEmpty/B4aCloudPublicEmpty.react'; // import CloudCodeChanges from 'lib/CloudCodeChanges'; import PropTypes from 'lib/PropTypes'; import Icon from 'components/Icon/Icon.react'; @@ -19,6 +21,7 @@ import { amplitudeLogEvent } from 'lib/amplitudeEvents'; import buttonStyles from 'components/Button/Button.scss'; import baseStyles from 'stylesheets/base.scss'; import modalStyles from 'components/B4aModal/B4aModal.scss'; +import CloudCodeSampleModal from './CloudCodeSampleModal.react' import 'jstree/dist/themes/default/style.css' import 'components/B4ACodeTree/B4AJsTree.css' @@ -60,16 +63,30 @@ export default class B4ACodeTree extends React.Component { files: this.props.files, isImage: false, selectedFolder: 0, + currentFolder: null, isFolderSelected: true, selectedNodeData: null, loadingFileId: null, - errorFileData: null, + errorFileData: null } // Used to track the latest file load request this.loadRequestId = 0; } + selectSpecificFile(fileName) { + const tree = $('#tree').jstree(true); + if (!tree) return; + + const node = tree.get_json('#', { flat: true }).find(n => n.text === fileName); + + if (node) { + B4ATreeActions.selectFileOnTree(node.id); + } else { + console.warn('Arquivo não encontrado na árvore.'); + } + } + getFileType(file) { try { return file.split(',')[0].indexOf('image') >= 0 @@ -205,7 +222,16 @@ export default class B4ACodeTree extends React.Component { } } } - this.setState({ source, selectedFile, nodeId, extension, isImage, selectedFolder, isFolderSelected: selected.type == 'folder' || selected.type == 'new-folder' }) + this.setState({ + source, + selectedFile, + nodeId, + extension, + isImage, + selectedFolder, + isFolderSelected: selected.type == 'folder' || selected.type == 'new-folder' , + currentFolder: selected.text + }) } // method to identify the selected tree node @@ -232,8 +258,11 @@ export default class B4ACodeTree extends React.Component { $('#tree').jstree().redraw(true); // set updated files. - this.props.cloudCodeChanges.addFile($('#tree').jstree('get_selected', true).pop().id); - this.props.setUpdatedFile(this.props.cloudCodeChanges.getFiles()); + let cloneUpdatedFiles = [...this.props.updatedFiles]; + if(!cloneUpdatedFiles.includes('j1_mainJS') && !cloneUpdatedFiles.includes('j1_indexHTML')){ + this.props.cloudCodeChanges.addFile($('#tree').jstree('get_selected', true).pop().id); + this.props.setUpdatedFile(this.props.cloudCodeChanges.getFiles()); + } } selectCloudFolder() { @@ -244,7 +273,39 @@ export default class B4ACodeTree extends React.Component { } updateCodeOnNewFile(type, text, id){ + if (type === 'delete-file') { + if (!this.props.hasDeployed) { + let cloneUpdatedFiles = [...this.props.updatedFiles]; + + // Mapping auto created files and specific IDs + const specialFiles = { + 'main.js': 'j1_mainJS', + 'index.html': 'j1_indexHTML' + }; + + // Define which ID to use + const fileIdToRemove = specialFiles[text] && cloneUpdatedFiles.includes(specialFiles[text]) + ? specialFiles[text] + : id; + + // Remove from cloudCodeChanges and cloneArray + this.props.cloudCodeChanges.removeFile(fileIdToRemove); + cloneUpdatedFiles = cloneUpdatedFiles.filter(f => f !== fileIdToRemove); + + // Reselect folder and update UI + if ($('#tree').jstree().get_json().length > 0) { + const cloudFolder = $('#tree').jstree().get_json()[0].id; + $('#tree').jstree('select_node', cloudFolder); + } + + this.props.setUpdatedFile(cloneUpdatedFiles); + + this.selectCloudFolder(); + B4ATreeActions.refreshEmptyFolderIcons(); + return; + } + // this.props.cloudCodeChanges.removeFile(text); this.props.cloudCodeChanges.removeFile(id); if ($('#tree').jstree().get_json().length > 0) { @@ -317,10 +378,29 @@ export default class B4ACodeTree extends React.Component { content = ; } else if (this.state.isFolderSelected === true) { - content = this.state.source && this.state.source !== '' ? :
; + content = + this.state.currentFolder && this.state.currentFolder === 'cloud' ? + this.selectSpecificFile('main.js')} + currentApp={this.props.currentApp} + hasDeployed={this.props.hasDeployed} + /> + : this.state.currentFolder === 'public' ? + this.selectSpecificFile('index.html')} + hasDeployed={this.props.hasDeployed} + /> + : + this.state.source && this.state.source !== '' ? + + : +
; } else if (this.state.selectedFile) { content =
@@ -353,7 +433,7 @@ export default class B4ACodeTree extends React.Component { } return ( -
+
diff --git a/src/components/B4ACodeTree/B4ACodeTree.scss b/src/components/B4ACodeTree/B4ACodeTree.scss index 5a1ec26ce6..fc618226a6 100644 --- a/src/components/B4ACodeTree/B4ACodeTree.scss +++ b/src/components/B4ACodeTree/B4ACodeTree.scss @@ -35,12 +35,123 @@ position: absolute; top: $toolbar-height; // toolbar height width: 100%; - height: calc(100% - $toolbar-height); // toolbar height + min-height: calc(100% - $toolbar-height); // toolbar height display: flex; flex-wrap: wrap; flex: 1; } +.cloudCodeSampleModal{ + @include modalAnimation(); + position: absolute; + top: 50%; + left: 50%; + width: 70vw; + padding: 30px; + max-height: 60vh; + background: #253348; + border-radius: 0.5rem; + overflow: hidden; + transition: width 0.5s cubic-bezier(1, 0, 0, 1); + overflow: auto; + & .cloudCodeSampleModalTitle { + h1{ + user-select: none + } + @include SoraFont(); + padding-bottom: 20px; + display: flex; + justify-content: space-between; + & .closeIcon { + cursor: pointer; + } + } + + & .docsLink { + padding-top: 20px; + a{ + cursor: pointer; + color: $blue; + } + } +} + +.codeBlockCloudSample { + position: relative; + padding-top: 0.5rem; + background: $regal-blue; + border-radius: 4px; + + & .codeBlockHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0rem 1.5rem; + } + + .languageLabel { + user-select: none; + } + + pre { + background: #111214 !important; + border-radius: 4px; + padding: 1.175rem 1.5rem 1.5rem 0!important; + margin: 1rem 0; + overflow-x: auto; + margin: none !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + font-family: 'Roboto Mono', monospace !important; + padding-left: 3.5em !important; + + pre.line-numbers:before { + background: transparent !important; + } + + code { + font-family: 'Roboto Mono', monospace !important; + font-size: 13px; + background: #111214 !important; + } + } + + code { + font-family: 'Roboto Mono', monospace !important; + font-size: 13px; + background: #111214 !important; + } + + .copyButtonCloudSample { + .copyTooltipCloudSample { + position: absolute; + bottom: calc(100% + 8px); + left: calc(100% - 110px); + transform: translateX(50%); + background: $dark-grey; + color: $white; + border-radius: 5px; + padding: .625rem 1rem; + font-size: 12px; + white-space: nowrap; + box-shadow: 0px 6px 16px 0px #0000001A; + animation: fadeIn 0.2s ease-in-out; + + &::after { + content: ''; + position: absolute; + left: 55%; + bottom: -4px; + transform: translateX(-50%) rotate(45deg); + width: 8px; + height: 8px; + background: $dark-grey; + } + } + } + +} + .files-box{ background-color: $dark-blue; padding-top: 1.88rem; diff --git a/src/components/B4ACodeTree/CloudCodeSampleModal.react.js b/src/components/B4ACodeTree/CloudCodeSampleModal.react.js new file mode 100644 index 0000000000..f927441bbe --- /dev/null +++ b/src/components/B4ACodeTree/CloudCodeSampleModal.react.js @@ -0,0 +1,221 @@ +import React, { useEffect, useRef, Suspense, lazy } from 'react'; +import ReactMarkdown from 'react-markdown'; +import Icon from 'components/Icon/Icon.react'; +import styles from 'components/B4ACodeTree/B4ACodeTree.scss'; +import Popover from 'components/Popover/Popover.react'; +import Position from 'lib/Position'; + +const CodeBlock = lazy(() => import('components/CodeBlock/CodeBlock.react')); + +const getCloudCodeSample = (currentApp) => { + return { + 'js-browser': { + icon: 'js-icon', + name: 'JavaScript (Browser)', + iconColor: '#f7df1c', + blocks: [ + { + title: 'Cloud Functions: Are custom functions that allow to execute logic on the backend.', + content: ` +~~~javascript +Parse.Cloud.define("hello", async (request) => { + console.log("Hello from Cloud Code!"); + return "Hello from Cloud Code!"; +}); +~~~` + }, + { + title: 'Here is how you have to call it via REST API.', + content: String.raw` +~~~bash +curl -X POST \ + -H "X-Parse-Application-Id: ${currentApp.applicationId}" \ + -H "X-Parse-REST-API-Key: ${currentApp.restKey}" \ + ${currentApp.serverURL}/functions/hello +~~~` + }, + { + title: 'This example creates an object in your class.', + content: ` +~~~javascript +Parse.Cloud.define("createObject", async (request) => { + const b4aClass = new Parse.Object("B4aSampleClass"); + b4aClass.set("name", request.params.name); + b4aClass.set("value", request.params.value); + await b4aClass.save(null, { useMasterKey: true }); + return "Object created successfully!"; +}); +~~~` + }, + { + title: 'Cloud Triggers: Is a function that automatically runs when certain events happen in your database classes. — such as when an object is saved, updated, deleted, or queried.', + content: ` +~~~javascript +Parse.Cloud.beforeSave("B4aSampleClass", (request) => { + if (request.object.get("value") === undefined) { + request.object.set("value", 0); + } +}); +~~~` + }, + { + title: 'You can use the createObject function created earlier and omit the value property to see the trigger in action.', + content: String.raw` +~~~bash +curl -X POST \ + -H "X-Parse-Application-Id: ${currentApp.applicationId}" \ + -H "X-Parse-REST-API-Key: ${currentApp.restKey}" \ + -H "Content-Type: application/json" \ + -d '{"name":"b4aObject"}' \ + ${currentApp.serverURL}/functions/createObject +~~~` + }, + { + title: 'Now we can retrieve the object created with this function:', + content: ` +~~~javascript +Parse.Cloud.define("getObjects", async (request) => { + const query = new Parse.Query("B4aSampleClass"); + const objects = await query.find({ useMasterKey: true }); + + return objects.map(obj => ({ + id: obj.id, + name: obj.get("name"), + value: obj.get("value"), + })); +}); +~~~` + }, + { + title: 'Here is how you have to call it via REST API.', + content: String.raw` +~~~bash +curl -X POST \ + -H "X-Parse-Application-Id: ${currentApp.applicationId}" \ + -H "X-Parse-REST-API-Key: ${currentApp.restKey}" \ + -H "Content-Type: application/json" \ + ${currentApp.serverURL}/functions/getObjects +~~~` + }, + { + title: `Cloud Jobs: Are background routines that can be scheduled or triggered to run automatically, ideal for long-running or maintenance tasks.`, + content: ` +~~~javascript +Parse.Cloud.job("activeAllObjects", async (request) => { + const query = new Parse.Query("B4aSampleClass"); + const objects = await query.find({ useMasterKey: true }); + + for (const obj of objects) { + obj.set("isActive", true); + await obj.save(null, { useMasterKey: true }); + } +}); +~~~` + }, + { + title: 'Here is how you have to call it. Jobs can be only excute with the Master Key.', + content: String.raw` +~~~bash +curl -X POST \ + -H "X-Parse-Application-Id: ${currentApp.applicationId}" \ + -H "X-Parse-Master-Key: ${currentApp.masterKey}" \ + ${currentApp.serverURL}/jobs/activeAllObjects +~~~` + }, + ] + } + } +} + +const origin = new Position(0, 0) + +const CloudCodeSampleModal = ({ closeModal, currentApp }) => { + const sample = getCloudCodeSample(currentApp)['js-browser']; + + const startRef = useRef(null); + const overlayRef = useRef(null); + + const handlePointerDown = (e) => { + startRef.current = { x: e.clientX, y: e.clientY }; + }; + + const handleClick = (e) => { + if (!overlayRef.current) return; + + const dx = e.clientX - startRef.current.x; + const dy = e.clientY - startRef.current.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 5 && e.target === overlayRef.current) { + closeModal(); + } + }; + + useEffect(() => { + const toolbar = document.querySelector('#toolbar'); + const sidebar = document.querySelector('#sidebar'); + const codeContainer = document.querySelector('#codeContainer'); + const navbar = document.querySelector('nav'); + + if (toolbar) toolbar.style.userSelect = 'none'; + if (sidebar) sidebar.style.userSelect = 'none'; + if (codeContainer) codeContainer.style.userSelect = 'none'; + if (navbar) navbar.style.userSelect = 'none' + + return () => { + if (toolbar) toolbar.style.userSelect = ''; + if (sidebar) sidebar.style.userSelect = ''; + if (codeContainer) codeContainer.style.userSelect = ''; + if (navbar) navbar.style.userSelect = ''; + }; + }, []); + + return ( + +
+
+
+

The examples below show you what Cloud Code looks like.

+
+ +
+
+ Loading...
}> + {sample.blocks.map((block, i) => ( + ( + + ) + }} + /> + ))} + + +
+
+ + ); +}; + +export default CloudCodeSampleModal; diff --git a/src/components/B4aCloudEmpty/B4aCloudEmpty.react.js b/src/components/B4aCloudEmpty/B4aCloudEmpty.react.js new file mode 100644 index 0000000000..44088ebe65 --- /dev/null +++ b/src/components/B4aCloudEmpty/B4aCloudEmpty.react.js @@ -0,0 +1,130 @@ +import React, { useState, Suspense, lazy } from 'react'; +import ghostImg from './ghost.png'; +import styles from 'components/B4aCloudEmpty/B4aCloudEmpty.scss'; + +import Icon from 'components/Icon/Icon.react'; +import ReactMarkdown from 'react-markdown'; +import Button from 'components/Button/Button.react'; + +const LazyCloudCodeSampleModal = lazy(() => import('../B4ACodeTree/CloudCodeSampleModal.react')); +const CodeBlock = lazy(() => import('components/CodeBlock/CodeBlock.react')); + +const B4aCloudEmpty = ({ imgSrc = ghostImg, dark = true, selectMainJs, currentApp, hasDeployed }) => { + const [openCloudCodeSample, setOpenCloudCodeSample] = useState(false); + + const handleCloudCodeSample = () => { + if(!openCloudCodeSample) { + import('../B4ACodeTree/CloudCodeSampleModal.react') + } + setOpenCloudCodeSample(prev => !prev); + } + + return ( + <> +
+ empty state +
+

Cloud Code — Extend Your App Backend with JavaScript

+

Cloud Code lets you run JavaScript functions on the server, together with your app's backend. Use it for backend logic, database triggers, and integrations with external services.

+
+ { !hasDeployed && ( + <> +
+

How it works

+
    +
  • +
    1
    +
    +
    + Write your function — Use the Cloud Code syntax. handleCloudCodeSample()} className={styles.mainJsText}>See examples → +
    +
    + + ( + + ), + }} + > +{`\`\`\`js +Parse.Cloud.define("hello", () => { + return "Hello from Cloud Code!"; +}); +`} + + + + {/*
    Parse.Cloud.define("hello", () = "Hello from Cloud Code!");
    */} +
    +
    +
  • +
  • +
    2
    +
    +
    Add it to selectMainJs()} className={styles.mainJsText}>main.js and Deploy — All Cloud Code must be defined in selectMainJs()} className={styles.mainJsText}>main.js. If you use other files, import them into selectMainJs()} className={styles.mainJsText}>main.js, then click Deploy. +
    +
    +
  • +
  • +
    3
    +
    +
    Call it via API or SDK — After deployment, your function is live and callable:
    +
    + + ( + + ), + }} + > +{`\`\`\`bash +curl -X POST ${currentApp.serverURL}/functions/hello \\ + -H "X-Parse-Application-Id: ${currentApp && currentApp.applicationId ? currentApp.applicationId : 'YOUR_APP_ID'}" \\ + -H "X-Parse-REST-API-Key: ${currentApp && currentApp.restKey ? currentApp.restKey : 'YOUR_REST_KEY'}" +`} + + + +
    +
    +
  • +
+
+
+
+ + )} +
+ handleCloudCodeSample()}>View Cloud Code examples → +
+
+ { + openCloudCodeSample && ( + + handleCloudCodeSample()} + currentApp={currentApp} + /> + + ) + } + + ) +} + +export default B4aCloudEmpty; diff --git a/src/components/B4aCloudEmpty/B4aCloudEmpty.scss b/src/components/B4aCloudEmpty/B4aCloudEmpty.scss new file mode 100644 index 0000000000..686d704c09 --- /dev/null +++ b/src/components/B4aCloudEmpty/B4aCloudEmpty.scss @@ -0,0 +1,157 @@ +@import 'stylesheets/globals.scss'; +@import 'stylesheets/back4app.scss'; + +.content { + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: center; + align-items: center; + margin: 46px 0 0 0; + .titleSection{ + display: flex; + flex-direction: column; + align-items: center; + margin: 15px 0 25px 0; + gap: 30px; + .title { + color: $white; + font-family: 'Sora', sans-serif; + font-size: 1.375rem; + font-weight: 600; + line-height: 140%; + text-align: center; + } + .description { + color: $light-blue; + font-family: 'Inter', sans-serif; + line-height: 140%; + text-align: center; + max-width: 70%; + } + } + .cardSection{ + display: flex; + flex-direction: column; + justify-content: center; + max-width: 70%; + padding: 28px 32px; + border: 1px solid #34506f66; + border-radius: 0.375rem; + h1{ + font-size: 1.375rem; + font-weight: 600; + } + .cardList{ + li{ + display: grid; + grid-template-columns: 32px 1fr; + grid-template-rows: auto 1fr; + column-gap: 1.175rem; + row-gap: 0.5rem; + align-items: start; + margin: 1.175rem 0; + min-width: 0; + .numberList{ + display: flex; + font-weight: 700; + justify-content: center; + align-items: center; + border: 4px solid $blue; + height: 40px; + width: 40px; + border-radius: 100%; + } + .contentList{ + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; + } + .mainJsText{ + color: $blue; + font-weight: 500; + cursor: pointer; + } + } + li:last-child{ + margin-bottom: 0 !important; + } + } + } + + .openMainButton{ + margin: 0.375rem 0; + } + + .viewCloudCodeSamples{ + span{ + padding-bottom: 1.375rem; + color: $blue; + font-weight: 500; + cursor: pointer; + } + } + + .filesPublicButton{ + margin-bottom: 25px; + } + +} + +.filesPublicButton{ + margin: 0.375rem 0; +} + +.mainJsButton{ + display: flex; + align-items: center; + gap: 10px; + height: 2.350rem; + padding: 0 20px; + font-size: 0.875rem; + font-weight: 600; + color: $white; + border: 1px solid $regal-blue; + border-radius: 8px; + .globeIcon{ + stroke: #F9F9F9 !important; + } + span{ + white-space: nowrap; + } +} +.mainJsButton:hover{ + background-color: $dark-blue; + color: $white; +} + +pre.line-numbers:before{ + border-radius: 0.3rem 0 0 0; +} + +@media screen and (max-width: 1280px){ + .content{ + .titleSection{ + .description{ + max-width: 90%; + } + } + .cardSection{ + max-width: 90%; + } + } +} + +@media screen and (max-width: 1514px){ + .content{ + .titleSection{ + .description{ + max-width: 90%; + } + } + .cardSection{ + max-width: 90%; + } + } +} \ No newline at end of file diff --git a/src/components/B4aCloudEmpty/B4aCloudPublicEmpty.react.js b/src/components/B4aCloudEmpty/B4aCloudPublicEmpty.react.js new file mode 100644 index 0000000000..90f0799fa5 --- /dev/null +++ b/src/components/B4aCloudEmpty/B4aCloudPublicEmpty.react.js @@ -0,0 +1,121 @@ +import React, { useEffect, Suspense, lazy } from 'react'; +import { useParams } from 'react-router-dom'; +// import Icon from 'components/Icon/Icon.react'; +import ghostImg from './ghost.png'; +import styles from 'components/B4aCloudEmpty/B4aCloudEmpty.scss'; +import Icon from 'components/Icon/Icon.react'; +import ReactMarkdown from 'react-markdown'; + +// Import Prism Line Numbers plugin +import 'prismjs/plugins/line-numbers/prism-line-numbers'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; + +import 'prismjs/components/prism-markup-templating.js'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-bash'; + +// eslint-disable-next-line no-unused-vars +import customPrisma from 'stylesheets/b4a-prisma.css'; + +const CodeBlock = lazy(() => import('components/CodeBlock/CodeBlock.react')); + +const B4aCloudPublicEmpty = ({ imgSrc = ghostImg, dark = true, selectIndex, hasDeployed }) => { + const { appId } = useParams(); + return ( +
+ empty state +
+

Web Hosting — Deploy Static Sites Instantly

+

Deploy your static websites, HTML pages, JavaScript apps, and assets directly to Back4app. Your files are served globally with automatic HTTPS and custom domain support.

+
+ { !hasDeployed && ( + <> +
+

How it works

+
    +
  • +
    1
    +
    +
    + Upload your files — Drop your HTML, CSS, JavaScript, images, and other static assets into the selectIndex()} className={styles.mainJsText}>public folder. You can organize files in subdirectories as needed. +
    +
    + ( + + ), + }} + > +{`\`\`\`text + public/ + ├── index.html + ├── login.html + └── styles.css`} + +
    +
    +
  • +
  • +
    2
    +
    +
    Enable your hosting URL — After uploading files, click Deploy and enable your web hosting URL. Your site will be available instantly at a unique Back4app subdomain:
    +
    + + ( + + ), + }} + > + {`\`\`\`bash + https://your-app.b4a.app + `} + + +
    +
    + +
    +
    +
  • +
+
+
+ +
+ + )} +
+ ) +} + +export default B4aCloudPublicEmpty; diff --git a/src/components/B4aCloudEmpty/ghost.png b/src/components/B4aCloudEmpty/ghost.png new file mode 100644 index 0000000000..c39874dd47 Binary files /dev/null and b/src/components/B4aCloudEmpty/ghost.png differ diff --git a/src/components/CodeBlock/CodeBlock.react.js b/src/components/CodeBlock/CodeBlock.react.js new file mode 100644 index 0000000000..6948d66eab --- /dev/null +++ b/src/components/CodeBlock/CodeBlock.react.js @@ -0,0 +1,111 @@ +import React, { useEffect, useState, useRef } from 'react'; +import Prism from 'prismjs'; +import Icon from 'components/Icon/Icon.react'; +import styles from './CodeBlock.scss'; + +// Plugins e linguagens suportadas (iguais ao original) +import 'prismjs/plugins/line-numbers/prism-line-numbers'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; + +import 'prismjs/components/prism-markup-templating.js'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-bash'; +import 'prismjs/components/prism-graphql'; +import 'prismjs/components/prism-java'; +import 'prismjs/components/prism-php'; +import 'prismjs/components/prism-dart'; +import 'prismjs/components/prism-kotlin'; +import 'prismjs/components/prism-swift'; + +// Mantém o mesmo CSS global +import 'stylesheets/b4a-prisma.css'; + +const CodeBlock = ({ language, value, title, content, hasTitle = true, hideCopyButton = false, hideLineNumbers = false }) => { + const [copied, setCopied] = useState(false); + const codeRef = useRef(null); + + const codeText = value ?? content ?? ''; + const lang = language || 'javascript'; + const isCloudSample = Boolean(title); + + useEffect(() => { + if (typeof Prism !== 'undefined') { + Prism.highlightAll(); + } + }, [codeText, language]); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(codeText.trim()); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } + }; + + return ( +
+ { hasTitle && ( +
+ {isCloudSample ? ( +
+ ) : ( +
{lang}
+ )} +
+ {copied && ( +
+ Copied! +
+ )} + +
+
+ )} +
+            {codeText.trim()}
+        
+ { !hasTitle && !hideCopyButton && ( +
+ {copied &&
Copied!
} + +
+ )} +
+ ); +}; + +export default CodeBlock; diff --git a/src/components/CodeBlock/CodeBlock.scss b/src/components/CodeBlock/CodeBlock.scss new file mode 100644 index 0000000000..a1542af02d --- /dev/null +++ b/src/components/CodeBlock/CodeBlock.scss @@ -0,0 +1,131 @@ +@import 'stylesheets/globals.scss'; +@import 'stylesheets/back4app.scss'; +.codeBlockHasNoTitle { + border-radius: 0.3rem 0 0 0.3rem !important; +} + +.codeBlockContainer { + position: relative; + padding-top: 0.5rem; + background: $regal-blue; + margin-bottom: 1rem !important; + border-radius: 0.3rem; + + & .codeBlockHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0rem 1.5rem 0.5rem 1.5rem; + } + + .copyButtonWrapper { + position: relative; + display: flex; + align-items: center; + .copyTooltip { + position: absolute; + right: 50%; + bottom: calc(100% + 8px); + transform: translateX(50%); + background: $dark-grey; + color: $white; + border-radius: 5px; + padding: .625rem 1rem; + font-size: 12px; + white-space: nowrap; + box-shadow: 0px 6px 16px 0px #0000001A; + animation: fadeIn 0.2s ease-in-out; + + &::after { + content: ''; + position: absolute; + left: 50%; + bottom: -4px; + transform: translateX(-50%) rotate(45deg); + width: 8px; + height: 8px; + background: $dark-grey; + } + } + } + + + h1, h2, h3 { + margin: .8rem 0; + font-weight: 600; + } + h4 { + font-weight: 500; + margin-bottom: .5rem; + } + + p { + margin-bottom: .5rem; + font-size: 14px; + } + + .hideLineNumbers { + padding-left: 1rem !important; + border-radius: 0 0.3rem 0.3rem 0.3rem !important; + } + + pre { + background: #111214 !important; + // padding: 0rem 1.5rem 1.5rem 0!important; + margin: 0 !important; + overflow-x: auto; + margin: none !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + font-family: 'Roboto Mono', monospace !important; + + pre.line-numbers:before { + background: transparent !important; + } + + code { + font-family: 'Roboto Mono', monospace !important; + font-size: 13px; + background: #111214 !important; + } + } + + code { + font-family: 'Roboto Mono', monospace !important; + font-size: 13px; + background: #111214 !important; + } +} + +.copyButtonCloudEmpty { + display: flex; + align-items: center; + padding: 0 10px; + background: #111214 !important; + border-radius: 0 0.3rem 0.3rem 0; + .copyTooltipCloudEmpty { + position: absolute; + bottom: calc(100% + 8px); + left: calc(100% - 100px); + transform: translateX(50%); + background: $dark-grey; + color: $white; + border-radius: 5px; + padding: .625rem 1rem; + font-size: 12px; + white-space: nowrap; + box-shadow: 0px 6px 16px 0px #0000001A; + animation: fadeIn 0.2s ease-in-out; + + &::after { + content: ''; + position: absolute; + left: 55%; + bottom: -4px; + transform: translateX(-50%) rotate(45deg); + width: 8px; + height: 8px; + background: $dark-grey; + } + } +} \ No newline at end of file diff --git a/src/components/CodeSnippet/CodeSnippet.css b/src/components/CodeSnippet/CodeSnippet.css index 43665d7e58..60f0f588e9 100644 --- a/src/components/CodeSnippet/CodeSnippet.css +++ b/src/components/CodeSnippet/CodeSnippet.css @@ -168,7 +168,7 @@ pre.line-numbers:before { } pre.line-numbers > code { - position: relative; + position: sticky !important; top: 10px; } diff --git a/src/dashboard/Data/AppOverview/AppOverview.scss b/src/dashboard/Data/AppOverview/AppOverview.scss index c5e26054d5..e100330fdc 100644 --- a/src/dashboard/Data/AppOverview/AppOverview.scss +++ b/src/dashboard/Data/AppOverview/AppOverview.scss @@ -1139,7 +1139,7 @@ pre { background: #111214 !important; border-radius: 4px; - padding: 0rem 1.5rem 1.5rem 0!important; + // padding: 0rem 1.5rem 1.5rem 0!important; margin: 1rem 0; overflow-x: auto; margin: none !important; diff --git a/src/dashboard/Data/AppOverview/ConnectAppModal.react.js b/src/dashboard/Data/AppOverview/ConnectAppModal.react.js index 7aac13eddc..5a3c7c298b 100644 --- a/src/dashboard/Data/AppOverview/ConnectAppModal.react.js +++ b/src/dashboard/Data/AppOverview/ConnectAppModal.react.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, Suspense, lazy } from 'react'; import Popover from 'components/Popover/Popover.react'; import Position from 'lib/Position'; import styles from 'dashboard/Data/AppOverview/AppOverview.scss'; @@ -25,6 +25,7 @@ import 'prismjs/plugins/line-numbers/prism-line-numbers.css' // eslint-disable-next-line no-unused-vars import customPrisma from 'stylesheets/b4a-prisma.css'; +const CodeBlock = lazy(() => import('components/CodeBlock/CodeBlock.react')); const LanguageDocMap = { rest: { @@ -873,52 +874,6 @@ function deleteObject($objectId) { }; const origin = new Position(0, 0); -const CodeBlock = ({ language, value }) => { - const [copied, setCopied] = useState(false); - - useEffect(() => { - if (typeof Prism !== 'undefined') { - Prism.highlightAll(); - } - }, [value, language]); - - const copyToClipboard = async () => { - try { - await navigator.clipboard.writeText(value); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error('Failed to copy text: ', err); - } - }; - - return ( -
-
-
{language}
-
- {copied && ( -
- Copied! -
- )} - -
-
-
-        {value}
-      
-
- ); -}; - const ConnectAppModal = ({ closeModal }) => { const [selectedLanguage, setSelectedLanguage] = useState(LanguageDocMap['js-browser']); const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -970,12 +925,16 @@ const ConnectAppModal = ({ closeModal }) => {
- + Loading...
}> + ( + + ), + }} + children={selectedLanguage.content} + /> +
diff --git a/src/dashboard/Data/CloudCode/B4ACloudCode.react.js b/src/dashboard/Data/CloudCode/B4ACloudCode.react.js index 7468c7c1c1..7c7d26a711 100644 --- a/src/dashboard/Data/CloudCode/B4ACloudCode.react.js +++ b/src/dashboard/Data/CloudCode/B4ACloudCode.react.js @@ -45,7 +45,10 @@ class B4ACloudCode extends CloudCode { // Parameters used to on/off alerts showTips: localStorage.getItem(this.alertTips) !== 'false', - showWhatIs: localStorage.getItem(this.alertWhatIs) !== 'false' + showWhatIs: localStorage.getItem(this.alertWhatIs) !== 'false', + + hideBlocker: false, + hasDeployed: true, }; this.onLogClick = this.onLogClick.bind(this); @@ -88,7 +91,7 @@ class B4ACloudCode extends CloudCode { await this.fetchSource(); // define the parameters to show unsaved changes warning modal this.unblock = this.props.navigator.block(tx => { - if (this.state.unsavedChanges || this.state.updatedFiles.length > 0) { + if ((this.state.unsavedChanges || this.state.updatedFiles.length > 0) && this.state.hideBlocker == false) { const unblock = this.unblock.bind(this); const autoUnblockingTx = { ...tx, @@ -219,7 +222,7 @@ class B4ACloudCode extends CloudCode { confirmText='Ok, got it' onConfirm={() => this.setState({ modal: null })} />; - this.setState({updatedFiles: [], unsavedChanges: false, modal: successModal }); + this.setState({updatedFiles: [], unsavedChanges: false, modal: successModal, hideBlocker: false }); this.cloudCodeChanges.clearChanges(); $('#tree').jstree(true).redraw(true); this.fetchSource(); @@ -250,7 +253,44 @@ class B4ACloudCode extends CloudCode { try { const response = await axios.get(this.getPath(), { withCredentials: true }) if (response.data && response.data.tree) { - this.setState({ files: response.data.tree, loading: false }) + const tree = response.data.tree; + + + const cloudFolder = tree.find(folder => folder.text === 'cloud'); + const publicFolder = tree.find(folder => folder.text === 'public'); + + const mainJsNotExists = cloudFolder?.mainJsNotExists ?? false; + const indexHtmlNotExists = publicFolder?.indexHtmlNotExists ?? false; + + if (mainJsNotExists && cloudFolder?.children?.length) { + cloudFolder.children.forEach(file => { + if (file.text === 'main.js') { + file.type = 'yellow-file'; + this.cloudCodeChanges.addFile('j1_mainJS') + this.setState({ updatedFiles: this.cloudCodeChanges.getFiles() }) + } + }); + } + + if (indexHtmlNotExists && publicFolder?.children?.length) { + publicFolder.children.forEach(file => { + if (file.text === 'index.html') { + file.type = 'yellow-file'; + this.cloudCodeChanges.addFile('j1_indexHTML') + this.setState({ updatedFiles: this.cloudCodeChanges.getFiles() }) + } + }); + } + + if(mainJsNotExists || indexHtmlNotExists) { + this.setState({ hideBlocker: true, hasDeployed: false }) + } + + this.setState({ + files: tree, + loading: false + }); + $('#tree').jstree().refresh(true); } } catch(err) { @@ -272,19 +312,21 @@ class B4ACloudCode extends CloudCode { let content = null; let title = null; const footer = null; - // Show loading page before fetch data if (this.state.loading) { content =
} else { // render cloud code page - + title = { - this.state.updatedFiles.length > 0 && + (this.state.updatedFiles.length > 0) &&
- Files pending deploy ({this.state.updatedFiles.length}) + {' '} + + Files pending deploy ({this.state.updatedFiles.length}) +
}