feat: add real-time collaboration features with STOMP and SockJS
- Added @stomp/stompjs and sockjs-client dependencies for WebSocket communication. - Updated routing for pastes to include new endpoint structure. - Implemented real-time editing in PastePanel using STOMP for collaborative editing. - Introduced NotificationModal for experimental mode warnings. - Enhanced NavBar to display connection status. - Refactored Home and PastePanel components to support new features and improve user experience. - Updated error handling in DataContext to utilize ErrorContext for better error management. - Added CSS animations for connection status indication.
This commit is contained in:
11
index.html
11
index.html
@@ -1,13 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title style="font-family: Fira Code;">mpaste</title>
|
<title style="font-family: Fira Code;">mpaste</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
<body>
|
||||||
<div id="root" class="p-0 m-0"></div>
|
<div id="root" class="p-0 m-0"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
137
package-lock.json
generated
137
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
"@fortawesome/react-fontawesome": "^0.2.3",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -25,7 +26,8 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^7.7.1",
|
"react-router-dom": "^7.7.1",
|
||||||
"react-slick": "^0.30.3",
|
"react-slick": "^0.30.3",
|
||||||
"slick-carousel": "^1.8.1"
|
"slick-carousel": "^1.8.1",
|
||||||
|
"sockjs-client": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.30.1",
|
"@eslint/js": "^9.30.1",
|
||||||
@@ -1606,6 +1608,12 @@
|
|||||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@stomp/stompjs": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.19",
|
"version": "0.5.19",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||||
@@ -2696,6 +2704,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventsource": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extract-zip": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -2738,6 +2755,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/faye-websocket": {
|
||||||
|
"version": "0.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
||||||
|
"integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"websocket-driver": ">=0.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fd-slicer": {
|
"node_modules/fd-slicer": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
@@ -3174,6 +3203,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/http-parser-js": {
|
||||||
|
"version": "0.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
|
||||||
|
"integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/http2-wrapper": {
|
"node_modules/http2-wrapper": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||||
@@ -3225,6 +3260,12 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/invariant": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
@@ -3544,7 +3585,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
@@ -3843,6 +3883,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/querystringify": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/quick-lru": {
|
"node_modules/quick-lru": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||||
@@ -4001,6 +4047,12 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resize-observer-polyfill": {
|
"node_modules/resize-observer-polyfill": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
@@ -4101,6 +4153,26 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -4180,6 +4252,34 @@
|
|||||||
"jquery": ">=1.8.0"
|
"jquery": ">=1.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sockjs-client": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^3.2.7",
|
||||||
|
"eventsource": "^2.0.2",
|
||||||
|
"faye-websocket": "^0.11.4",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"url-parse": "^1.5.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/sockjs-client"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sockjs-client/node_modules/debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -4372,6 +4472,16 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url-parse": {
|
||||||
|
"version": "1.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"querystringify": "^2.1.1",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
@@ -4456,6 +4566,29 @@
|
|||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/websocket-driver": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"http-parser-js": ">=0.5.1",
|
||||||
|
"safe-buffer": ">=5.1.0",
|
||||||
|
"websocket-extensions": ">=0.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket-extensions": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
"@fortawesome/react-fontawesome": "^0.2.3",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^7.7.1",
|
"react-router-dom": "^7.7.1",
|
||||||
"react-slick": "^0.30.3",
|
"react-slick": "^0.30.3",
|
||||||
"slick-carousel": "^1.8.1"
|
"slick-carousel": "^1.8.1",
|
||||||
|
"sockjs-client": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.30.1",
|
"@eslint/js": "^9.30.1",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"endpoints": {
|
"endpoints": {
|
||||||
"pastes": {
|
"pastes": {
|
||||||
"all": "/pastes",
|
"all": "/pastes",
|
||||||
"byId": "/:pasteId",
|
"byId": "/by-id/:pasteId",
|
||||||
"byKey": "/:pasteKey"
|
"byKey": "/s/:pasteKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"endpoints": {
|
"endpoints": {
|
||||||
"pastes": {
|
"pastes": {
|
||||||
"all": "/pastes",
|
"all": "/pastes",
|
||||||
"byId": "/:pasteId",
|
"byId": "/by-id/:pasteId",
|
||||||
"byKey": "/:pasteKey"
|
"byKey": "/s/:pasteKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/App.jsx
43
src/App.jsx
@@ -1,17 +1,54 @@
|
|||||||
import NavBar from '@/components/NavBar.jsx';
|
import NavBar from '@/components/NavBar.jsx';
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom'
|
import { Route, Routes, useLocation } from 'react-router-dom'
|
||||||
import Home from '@/pages/Home.jsx'
|
import Home from '@/pages/Home.jsx'
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import NotificationModal from './components/NotificationModal';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [showExperimentalModal, setShowExperimentalModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hasSeenWarning = localStorage.getItem('experimental_rt_warned');
|
||||||
|
|
||||||
|
if (!hasSeenWarning) {
|
||||||
|
setShowExperimentalModal(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCloseWarning = () => {
|
||||||
|
localStorage.setItem('experimental_rt_warned', 'true');
|
||||||
|
setShowExperimentalModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar />
|
<NavBar connected={connected} />
|
||||||
<div className="fill d-flex flex-column">
|
<div className="fill d-flex flex-column">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home mode="create" onConnectChange={setConnected} />} />
|
||||||
<Route path="/:pasteKey" element={<Home />} />
|
<Route path="/s/:pasteKey" element={<Home mode="static" onConnectChange={setConnected} />} />
|
||||||
|
<Route path="/:rtKey" element={<Home mode="rt" onConnectChange={setConnected} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
<NotificationModal
|
||||||
|
show={showExperimentalModal}
|
||||||
|
onClose={handleCloseWarning}
|
||||||
|
title="Modo Experimental"
|
||||||
|
message={
|
||||||
|
<span>
|
||||||
|
He añadido un modo tiempo real pero de momento es <strong>EXPERIMENTAL</strong>. Cualquier fallo por favor mandadlo a jose [arroba] miarma.net.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
variant="warning"
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
label: "Vale",
|
||||||
|
variant: "warning",
|
||||||
|
onClick: handleCloseWarning
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import '@/css/PasswordInput.css';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
|
import { faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
const PasswordInput = ({ value, onChange, name = "password" }) => {
|
const PasswordInput = ({ disabled, value, onChange, name = "password" }) => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
const toggleShow = () => setShow(prev => !prev);
|
const toggleShow = () => setShow(prev => !prev);
|
||||||
@@ -28,6 +28,7 @@ const PasswordInput = ({ value, onChange, name = "password" }) => {
|
|||||||
placeholder=""
|
placeholder=""
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className="rounded-4 pe-5"
|
className="rounded-4 pe-5"
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</FloatingLabel>
|
</FloatingLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { Navbar, Nav, Container } from 'react-bootstrap';
|
|||||||
import SearchToolbar from './SearchToolbar';
|
import SearchToolbar from './SearchToolbar';
|
||||||
import { useSearch } from "@/context/SearchContext";
|
import { useSearch } from "@/context/SearchContext";
|
||||||
import NotificationModal from './NotificationModal';
|
import NotificationModal from './NotificationModal';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
const NavBar = () => {
|
const NavBar = ({ connected }) => {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [isLg, setIsLg] = useState(window.innerWidth >= 992);
|
const [isLg, setIsLg] = useState(window.innerWidth >= 992);
|
||||||
const [isXs, setIsXs] = useState(window.innerWidth < 576);
|
const [isXs, setIsXs] = useState(window.innerWidth < 576);
|
||||||
@@ -47,7 +49,6 @@ const NavBar = () => {
|
|||||||
className='shadow-none custom-border-bottom'
|
className='shadow-none custom-border-bottom'
|
||||||
>
|
>
|
||||||
<Container fluid>
|
<Container fluid>
|
||||||
{/* brand */}
|
|
||||||
<Nav.Item
|
<Nav.Item
|
||||||
title="mpaste"
|
title="mpaste"
|
||||||
className={`navbar-brand`}
|
className={`navbar-brand`}
|
||||||
@@ -58,12 +59,10 @@ const NavBar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
|
|
||||||
{/* ThemeButton SIEMPRE fijo */}
|
|
||||||
<div className="order-lg-2 ms-auto me-2">
|
<div className="order-lg-2 ms-auto me-2">
|
||||||
<ThemeButton onlyIcon={isXs} />
|
<ThemeButton onlyIcon={isXs} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* burger */}
|
|
||||||
<Navbar.Toggle
|
<Navbar.Toggle
|
||||||
aria-controls="main-navbar"
|
aria-controls="main-navbar"
|
||||||
className="custom-toggler border-0 order-lg-3"
|
className="custom-toggler border-0 order-lg-3"
|
||||||
@@ -79,7 +78,6 @@ const NavBar = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</Navbar.Toggle>
|
</Navbar.Toggle>
|
||||||
|
|
||||||
{/* links y search que colapsan */}
|
|
||||||
<Navbar.Collapse id="main-navbar" className="order-lg-1">
|
<Navbar.Collapse id="main-navbar" className="order-lg-1">
|
||||||
<Nav
|
<Nav
|
||||||
className={`me-auto gap-3 w-100 ${expanded ? "flex-column align-items-start mt-3 mb-2" : "d-flex align-items-center"}`}
|
className={`me-auto gap-3 w-100 ${expanded ? "flex-column align-items-start mt-3 mb-2" : "d-flex align-items-center"}`}
|
||||||
@@ -92,11 +90,24 @@ const NavBar = () => {
|
|||||||
onSearchChange={setSearchTerm}
|
onSearchChange={setSearchTerm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="d-flex align-items-center gap-2 ms-2 px-2 py-1 " style={{ fontSize: '0.85rem' }}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCircle}
|
||||||
|
className={connected ? "pulse-animation" : ""}
|
||||||
|
style={{
|
||||||
|
color: connected ? '#28a745' : '#dc3545',
|
||||||
|
fontSize: '10px',
|
||||||
|
filter: connected ? 'drop-shadow(0 0 4px #28a745)' : 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-secondary fw-medium">
|
||||||
|
{connected ? 'conectado' : 'desconectado'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar.Collapse>
|
</Navbar.Collapse>
|
||||||
</Container>
|
</Container>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
{/* Contact Modal */}
|
|
||||||
<NotificationModal
|
<NotificationModal
|
||||||
show={showContactModal}
|
show={showContactModal}
|
||||||
onClose={() => setShowContactModal(false)}
|
onClose={() => setShowContactModal(false)}
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Form, Button, Row, Col, FloatingLabel, Alert } from "react-bootstrap";
|
import { Form, Button, Row, Col, FloatingLabel, Alert } from "react-bootstrap";
|
||||||
import '@/css/PastePanel.css';
|
import '@/css/PastePanel.css';
|
||||||
import PasswordInput from "@/components/Auth/PasswordInput";
|
import PasswordInput from "@/components/Auth/PasswordInput";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faCode, faHeader } from "@fortawesome/free-solid-svg-icons";
|
import { faCircle, faCode, faHeader } from "@fortawesome/free-solid-svg-icons";
|
||||||
import CodeEditor from "./CodeEditor";
|
import CodeEditor from "./CodeEditor";
|
||||||
import PublicPasteItem from "./PublicPasteItem";
|
import PublicPasteItem from "./PublicPasteItem";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { useDataContext } from "@/hooks/useDataContext";
|
import { useDataContext } from "@/hooks/useDataContext";
|
||||||
import PasswordModal from "@/components/Auth/PasswordModal.jsx";
|
import PasswordModal from "@/components/Auth/PasswordModal.jsx";
|
||||||
|
import { Client } from "@stomp/stompjs";
|
||||||
|
import SockJS from 'sockjs-client';
|
||||||
|
|
||||||
const PastePanel = ({ onSubmit, publicPastes }) => {
|
const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnectChange }) => {
|
||||||
const { pasteKey } = useParams();
|
const { pasteKey: urlPasteKey, rtKey } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { getData } = useDataContext();
|
const { getData } = useDataContext();
|
||||||
|
|
||||||
|
const activeKey = propKey || urlPasteKey || rtKey;
|
||||||
|
|
||||||
|
const [selectedPaste, setSelectedPaste] = useState(null);
|
||||||
|
const [editorErrors, setEditorErrors] = useState([]);
|
||||||
|
const [fieldErrors, setFieldErrors] = useState({});
|
||||||
|
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||||
|
const [stompClient, setStompClient] = useState(null);
|
||||||
|
const [connected, setConnected] = useState(null);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: "",
|
title: "",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -24,10 +35,113 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
password: ""
|
password: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedPaste, setSelectedPaste] = useState(null);
|
const lastSavedContent = useRef(formData.content);
|
||||||
const [editorErrors, setEditorErrors] = useState([]);
|
|
||||||
const [fieldErrors, setFieldErrors] = useState({});
|
const isReadOnly = !!selectedPaste || mode === 'rt';
|
||||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
const isRemoteChange = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'static' && activeKey) {
|
||||||
|
fetchPaste(activeKey);
|
||||||
|
} else if (mode === 'create') {
|
||||||
|
setSelectedPaste(null);
|
||||||
|
setFormData({ title: "", content: "", syntax: "", burnAfter: false, isPrivate: false, password: "" });
|
||||||
|
setFieldErrors({});
|
||||||
|
setEditorErrors([]);
|
||||||
|
}
|
||||||
|
}, [activeKey, mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'rt' && activeKey) {
|
||||||
|
const socketUrl = import.meta.env.MODE === 'production'
|
||||||
|
? `https://api.miarma.net/v2/mpaste/ws`
|
||||||
|
: `http://localhost:8081/v2/mpaste/ws`;
|
||||||
|
|
||||||
|
const socket = new SockJS(socketUrl);
|
||||||
|
const client = new Client({
|
||||||
|
webSocketFactory: () => socket,
|
||||||
|
onConnect: () => {
|
||||||
|
setConnected(true);
|
||||||
|
onConnectChange(true);
|
||||||
|
client.subscribe(`/topic/session/${activeKey}`, (message) => {
|
||||||
|
try {
|
||||||
|
const remoteState = JSON.parse(message.body);
|
||||||
|
|
||||||
|
setFormData(prev => {
|
||||||
|
if (prev.content === remoteState.content && prev.syntax === remoteState.syntax) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
isRemoteChange.current = true;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
...remoteState
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parseando el mensaje del socket", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
client.publish({ destination: `/app/join/${activeKey}` });
|
||||||
|
},
|
||||||
|
onDisconnect: () => {
|
||||||
|
setConnected(false);
|
||||||
|
onConnectChange(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.activate();
|
||||||
|
setStompClient(client);
|
||||||
|
return () => client.deactivate();
|
||||||
|
} else {
|
||||||
|
setConnected(false);
|
||||||
|
}
|
||||||
|
}, [mode, activeKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'rt' && connected && formData.content) {
|
||||||
|
|
||||||
|
if (isRemoteChange.current) {
|
||||||
|
lastSavedContent.current = formData.content;
|
||||||
|
isRemoteChange.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.content !== lastSavedContent.current) {
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const dataToSave = {
|
||||||
|
...formData,
|
||||||
|
pasteKey: activeKey,
|
||||||
|
title: mode === 'rt' ? `Sesión: ${activeKey?.substring(0, 8)}` : formData.title
|
||||||
|
};
|
||||||
|
await onSubmit(dataToSave, true);
|
||||||
|
lastSavedContent.current = formData.content;
|
||||||
|
console.log("Autosave");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error autosaving:", err);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [formData.content, mode, connected, activeKey]);
|
||||||
|
|
||||||
|
const handleChange = (key, value) => {
|
||||||
|
const updatedData = { ...formData, [key]: value };
|
||||||
|
|
||||||
|
setFormData(updatedData);
|
||||||
|
|
||||||
|
if (connected && stompClient && activeKey) {
|
||||||
|
stompClient.publish({
|
||||||
|
destination: `/app/edit/${activeKey}`,
|
||||||
|
body: JSON.stringify(updatedData)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -53,17 +167,17 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectPaste = async (key) => navigate(`/${key}`);
|
const handleSelectPaste = (key) => navigate(`/s/${key}`);
|
||||||
|
|
||||||
const fetchPaste = async (key, pwd = "") => {
|
const fetchPaste = async (key, pwd = "") => {
|
||||||
const url = import.meta.env.MODE === 'production'
|
const url = import.meta.env.MODE === 'production'
|
||||||
? `https://api.miarma.net/v2/mpaste/pastes/${key}`
|
? `https://api.miarma.net/v2/mpaste/pastes/s/${key}`
|
||||||
: `http://localhost:8081/v2/mpaste/pastes/${key}`;
|
: `http://localhost:8081/v2/mpaste/pastes/s/${key}`;
|
||||||
|
|
||||||
const headers = pwd ? { "X-Paste-Password": pwd } : {};
|
const headers = pwd ? { "X-Paste-Password": pwd } : {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getData(url, null, false, headers);
|
const response = await getData(url, null, false, headers, true);
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
setSelectedPaste(response);
|
setSelectedPaste(response);
|
||||||
@@ -91,12 +205,6 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { if (pasteKey) fetchPaste(pasteKey); }, [pasteKey]);
|
|
||||||
|
|
||||||
const handleChange = (key, value) => {
|
|
||||||
setFormData(prev => ({ ...prev, [key]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="paste-panel border-0 flex-fill d-flex flex-column min-h-0 p-3">
|
<div className="paste-panel border-0 flex-fill d-flex flex-column min-h-0 p-3">
|
||||||
@@ -134,20 +242,20 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={12} lg={3} className="d-flex flex-column flex-fill min-h-0 overflow-hidden">
|
<Col xs={12} lg={3} className="d-flex flex-column flex-fill min-h-0 overflow-hidden">
|
||||||
<div className="d-flex flex-column flex-fill gap-3 overflow-auto">
|
<div className="d-flex flex-column flex-fill gap-3 overflow-auto p-1">
|
||||||
<FloatingLabel
|
<FloatingLabel
|
||||||
controlId="titleInput"
|
controlId="titleInput"
|
||||||
label={
|
label={
|
||||||
<span className={selectedPaste ? "text-white" : ""}>
|
<span className={isReadOnly ? "text-white" : ""}>
|
||||||
<FontAwesomeIcon icon={faHeader} className="me-2" />
|
<FontAwesomeIcon icon={faHeader} className="me-2" />
|
||||||
Título
|
Título
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
disabled={!!selectedPaste}
|
disabled={isReadOnly}
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.title}
|
value={mode === 'rt' ? `Sesión: ${activeKey?.substring(0, 8)}` : formData.title}
|
||||||
onChange={(e) => handleChange("title", e.target.value)}
|
onChange={(e) => handleChange("title", e.target.value)}
|
||||||
isInvalid={!!fieldErrors.title}
|
isInvalid={!!fieldErrors.title}
|
||||||
/>
|
/>
|
||||||
@@ -202,7 +310,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
|
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type="switch"
|
type="switch"
|
||||||
disabled={!!selectedPaste}
|
disabled={isReadOnly}
|
||||||
id="burnAfter"
|
id="burnAfter"
|
||||||
label="volátil"
|
label="volátil"
|
||||||
checked={formData.burnAfter}
|
checked={formData.burnAfter}
|
||||||
@@ -212,7 +320,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
|
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type="switch"
|
type="switch"
|
||||||
disabled={!!selectedPaste}
|
disabled={isReadOnly}
|
||||||
id="isPrivate"
|
id="isPrivate"
|
||||||
label="privado"
|
label="privado"
|
||||||
checked={formData.isPrivate}
|
checked={formData.isPrivate}
|
||||||
@@ -221,14 +329,14 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{formData.isPrivate && (
|
{formData.isPrivate && (
|
||||||
<PasswordInput onChange={(e) => handleChange("password", e.target.value)} />
|
<PasswordInput disabled={isReadOnly} onChange={(e) => handleChange("password", e.target.value)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="d-flex justify-content-end">
|
<div className="d-flex justify-content-end">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!!selectedPaste}
|
disabled={isReadOnly}
|
||||||
>
|
>
|
||||||
Crear paste
|
Crear paste
|
||||||
</Button>
|
</Button>
|
||||||
@@ -243,7 +351,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
onClose={() => setShowPasswordModal(false)}
|
onClose={() => setShowPasswordModal(false)}
|
||||||
onSubmit={(pwd) => {
|
onSubmit={(pwd) => {
|
||||||
setShowPasswordModal(false);
|
setShowPasswordModal(false);
|
||||||
fetchPaste(pasteKey, pwd);
|
fetchPaste(activeKey, pwd);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useData } from "../hooks/useData";
|
import { useData } from "../hooks/useData";
|
||||||
|
import { useError } from "./ErrorContext";
|
||||||
|
|
||||||
export const DataContext = createContext();
|
export const DataContext = createContext();
|
||||||
|
|
||||||
export const DataProvider = ({ config, onError, children }) => {
|
export const DataProvider = ({ config, children }) => {
|
||||||
const data = useData(config, onError);
|
const { showError } = useError();
|
||||||
|
const data = useData(config, showError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataContext.Provider value={data}>
|
<DataContext.Provider value={data}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useState, useContext } from 'react';
|
import { createContext, useState, useContext, useCallback } from 'react';
|
||||||
import NotificationModal from '../components/NotificationModal';
|
import NotificationModal from '../components/NotificationModal';
|
||||||
|
|
||||||
const ErrorContext = createContext();
|
const ErrorContext = createContext();
|
||||||
@@ -6,29 +6,28 @@ const ErrorContext = createContext();
|
|||||||
export const ErrorProvider = ({ children }) => {
|
export const ErrorProvider = ({ children }) => {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const showError = (err) => {
|
const showError = useCallback((err) => {
|
||||||
|
if (err.status === 422) return;
|
||||||
|
|
||||||
setError({
|
setError({
|
||||||
title: err.status ? `Error ${err.status}` : "Error",
|
title: err.status ? `Error ${err.status}` : "Ups!",
|
||||||
message: err.message,
|
message: err.message || "Algo ha salido mal miarma",
|
||||||
variant: 'danger'
|
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const closeError = () => setError(null);
|
const closeError = () => setError(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorContext.Provider value={{ showError }}>
|
<ErrorContext.Provider value={{ showError }}>
|
||||||
{children}
|
{children}
|
||||||
{error && (
|
|
||||||
<NotificationModal
|
<NotificationModal
|
||||||
show={true}
|
show={error !== null}
|
||||||
onClose={closeError}
|
onClose={closeError}
|
||||||
title={error.title}
|
title={error?.title || "Error"}
|
||||||
message={error.message}
|
message={error?.message || ""}
|
||||||
variant='danger'
|
variant='danger'
|
||||||
buttons={[{ label: "Aceptar", variant: "danger", onClick: closeError }]}
|
buttons={[{ label: "Entendido", variant: "danger", onClick: closeError }]}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</ErrorContext.Provider>
|
</ErrorContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,3 +63,21 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-green {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
filter: drop-shadow(0 0 0px rgba(40, 167, 69, 0.7));
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: drop-shadow(0 0 6px rgba(40, 167, 69, 0.4));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
filter: drop-shadow(0 0 0px rgba(40, 167, 69, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-animation {
|
||||||
|
animation: pulse-green 2s infinite;
|
||||||
|
}
|
||||||
@@ -5,141 +5,102 @@ export const useData = (config, onError) => {
|
|||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [dataLoading, setLoading] = useState(true);
|
const [dataLoading, setLoading] = useState(true);
|
||||||
const [dataError, setError] = useState(null);
|
const [dataError, setError] = useState(null);
|
||||||
const configRef = useRef();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const configString = JSON.stringify(config);
|
||||||
if (config?.baseUrl) {
|
|
||||||
configRef.current = config;
|
|
||||||
}
|
|
||||||
}, [config]);
|
|
||||||
|
|
||||||
const getAuthHeaders = (isFormData = false) => {
|
const getAuthHeaders = (isFormData = false) => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
const headers = {};
|
const headers = {};
|
||||||
if (token) headers.Authorization = `Bearer ${token}`;
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
if (!isFormData) headers["Content-Type"] = "application/json";
|
||||||
if (!isFormData) {
|
|
||||||
headers["Content-Type"] = "application/json";
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAxiosError = (err) => {
|
const handleAxiosError = (err) => {
|
||||||
if (err.response && err.response.data) {
|
const errorData = {
|
||||||
const data = err.response.data;
|
status: err.response?.status || (err.request ? "Network Error" : "Client Error"),
|
||||||
|
message: err.response?.data?.message || err.message || "Error desconocido",
|
||||||
if (data.status === 422 && data.errors) {
|
errors: err.response?.data?.errors || null
|
||||||
return {
|
|
||||||
status: 422,
|
|
||||||
errors: data.errors,
|
|
||||||
path: data.path ?? null,
|
|
||||||
timestamp: data.timestamp ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: data.status ?? err.response.status,
|
|
||||||
error: data.error ?? null,
|
|
||||||
message: data.message ?? err.response.statusText ?? "Error desconocido",
|
|
||||||
path: data.path ?? null,
|
|
||||||
timestamp: data.timestamp ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.request) {
|
|
||||||
return {
|
|
||||||
status: null,
|
|
||||||
error: "Network Error",
|
|
||||||
message: "No se pudo conectar al servidor",
|
|
||||||
path: null,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: null,
|
|
||||||
error: "Client Error",
|
|
||||||
message: err.message || "Error desconocido",
|
|
||||||
path: null,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
};
|
||||||
|
return errorData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
const current = configRef.current;
|
if (!config?.baseUrl) return;
|
||||||
if (!current?.baseUrl) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(current.baseUrl, {
|
const response = await axios.get(config.baseUrl, {
|
||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
params: current.params,
|
params: config.params,
|
||||||
});
|
});
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = handleAxiosError(err);
|
const error = handleAxiosError(err);
|
||||||
|
const isPasteLookup = config.baseUrl.includes('/pastes/');
|
||||||
|
|
||||||
|
if (isPasteLookup && (error.status === 403 || error.status === 404 || error.status === 500)) {
|
||||||
|
console.log("Not in DB, assuming real-time...");
|
||||||
setError(error);
|
setError(error);
|
||||||
|
} else {
|
||||||
|
if (onError) onError(error);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [configString, onError]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.baseUrl) fetchData();
|
fetchData();
|
||||||
}, [config, fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
const requestWrapper = async (method, endpoint, payload = null, refresh = false, extraHeaders = {}) => {
|
const requestWrapper = async (method, endpoint, payload = null, refresh = false, extraHeaders = {}, silent = false) => {
|
||||||
try {
|
try {
|
||||||
const isFormData = payload instanceof FormData;
|
const isFormData = payload instanceof FormData;
|
||||||
|
|
||||||
const headers = {
|
const combinedHeaders = {
|
||||||
...getAuthHeaders(isFormData),
|
...getAuthHeaders(isFormData),
|
||||||
...extraHeaders
|
...extraHeaders
|
||||||
};
|
};
|
||||||
|
|
||||||
const cfg = { headers };
|
const axiosConfig = {
|
||||||
let response;
|
headers: combinedHeaders
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
if (method === "get") {
|
if (method === "get") {
|
||||||
if (payload) cfg.params = payload;
|
response = await axios.get(endpoint, {
|
||||||
response = await axios.get(endpoint, cfg);
|
...axiosConfig,
|
||||||
|
params: payload
|
||||||
|
});
|
||||||
} else if (method === "delete") {
|
} else if (method === "delete") {
|
||||||
if (payload) cfg.data = payload;
|
response = await axios.delete(endpoint, {
|
||||||
response = await axios.delete(endpoint, cfg);
|
...axiosConfig,
|
||||||
|
data: payload
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
response = await axios[method](endpoint, payload, cfg);
|
response = await axios[method](endpoint, payload, axiosConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refresh) await fetchData();
|
if (refresh) await fetchData();
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = handleAxiosError(err);
|
const error = handleAxiosError(err);
|
||||||
|
if (!silent && error.status !== 422 && onError) {
|
||||||
if (error.status !== 403 && error.status !== 422) {
|
onError(error);
|
||||||
if (onError) onError(error);
|
|
||||||
setError(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearError = () => setError(null);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data, dataLoading, dataError,
|
||||||
dataLoading,
|
getData: (url, params, refresh = true, h = {}, silent = false) => requestWrapper("get", url, params, refresh, h, silent),
|
||||||
dataError,
|
postData: (url, body, refresh = true, silent = false) => requestWrapper("post", url, body, refresh, silent),
|
||||||
clearError,
|
putData: (url, body, refresh = true, silent = false) => requestWrapper("put", url, body, refresh, silent),
|
||||||
getData: (url, params, refresh = true, headers = {}) => requestWrapper("get", url, params, refresh, headers),
|
deleteData: (url, refresh = true, silent = false) => requestWrapper("delete", url, null, refresh, silent),
|
||||||
postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh),
|
|
||||||
putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh),
|
|
||||||
deleteData: (url, refresh = true) => requestWrapper("delete", url, null, refresh),
|
|
||||||
deleteDataWithBody: (url, body, refresh = true) => requestWrapper("delete", url, body, refresh)
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -3,72 +3,108 @@ import PastePanel from '@/components/Pastes/PastePanel';
|
|||||||
import { useConfig } from '@/hooks/useConfig';
|
import { useConfig } from '@/hooks/useConfig';
|
||||||
import LoadingIcon from '@/components/LoadingIcon';
|
import LoadingIcon from '@/components/LoadingIcon';
|
||||||
import { useDataContext } from '@/hooks/useDataContext';
|
import { useDataContext } from '@/hooks/useDataContext';
|
||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { DataProvider } from '@/context/DataContext';
|
import { DataProvider } from '@/context/DataContext';
|
||||||
import NotificationModal from '@/components/NotificationModal';
|
import NotificationModal from '@/components/NotificationModal';
|
||||||
import { useSearch } from "@/context/SearchContext";
|
import { useSearch } from "@/context/SearchContext";
|
||||||
import { useError } from '@/context/ErrorContext';
|
import { useError } from '@/context/ErrorContext';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = ({ mode, onConnectChange }) => {
|
||||||
|
const { pasteKey, rtKey } = useParams();
|
||||||
const { config, configLoading } = useConfig();
|
const { config, configLoading } = useConfig();
|
||||||
const { showError } = useError();
|
const { showError } = useError();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
if (configLoading) return <p><LoadingIcon /></p>;
|
const currentKey = mode === 'static' ? pasteKey : rtKey;
|
||||||
|
|
||||||
const reqConfig = {
|
const reqConfig = useMemo(() => {
|
||||||
baseUrl: `${config.apiConfig.baseUrl}${config.apiConfig.endpoints.pastes.all}`,
|
if (!config?.apiConfig?.baseUrl) return null;
|
||||||
|
|
||||||
|
const baseApi = `${config.apiConfig.baseUrl}${config.apiConfig.endpoints.pastes.all}`;
|
||||||
|
|
||||||
|
if (mode === 'static' && currentKey) {
|
||||||
|
return {
|
||||||
|
baseUrl: `${baseApi}/s/${currentKey}`,
|
||||||
params: {}
|
params: {}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseUrl: baseApi,
|
||||||
|
params: {}
|
||||||
|
};
|
||||||
|
}, [config, mode, currentKey]);
|
||||||
|
|
||||||
|
if (configLoading) return <p className="text-center mt-5"><LoadingIcon /></p>;
|
||||||
|
|
||||||
|
if (mode === 'static' && !reqConfig?.baseUrl?.includes('/s/')) {
|
||||||
|
return <div className="text-center mt-5"><LoadingIcon /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataProvider config={reqConfig} onError={showError}>
|
<DataProvider key={location.key} config={reqConfig} onError={showError}>
|
||||||
<HomeContent reqConfig={reqConfig} />
|
<HomeContent reqConfig={reqConfig} mode={mode} pasteKey={currentKey} onConnectChange={onConnectChange} />
|
||||||
</DataProvider>
|
</DataProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HomeContent = ({ reqConfig }) => {
|
const HomeContent = ({ reqConfig, mode, pasteKey, onConnectChange }) => {
|
||||||
const { data, dataLoading, dataError, postData } = useDataContext();
|
const { data, dataLoading, postData } = useDataContext();
|
||||||
const [error, setError] = useState(null);
|
const [createdKey, setCreatedKey] = useState(null);
|
||||||
const [key, setKey] = useState(null);
|
|
||||||
const { searchTerm } = useSearch();
|
const { searchTerm } = useSearch();
|
||||||
|
|
||||||
if (dataLoading) return <p><LoadingIcon /></p>;
|
if (mode === 'static' && dataLoading) return <p className="text-center mt-5"><LoadingIcon /></p>;
|
||||||
|
|
||||||
const filtered = (data && Array.isArray(data)) ? data.filter(paste =>
|
const filtered = (data && Array.isArray(data)) ? data.filter(paste =>
|
||||||
paste.title.toLowerCase().includes((searchTerm ?? "").toLowerCase())
|
paste.title.toLowerCase().includes((searchTerm ?? "").toLowerCase())
|
||||||
) : [];
|
) : [];
|
||||||
|
|
||||||
const handleSubmit = async (paste) => {
|
const handleSubmit = async (paste, isAutosave = false) => {
|
||||||
try {
|
try {
|
||||||
const createdPaste = await postData(reqConfig.baseUrl, paste);
|
const createdPaste = await postData(reqConfig.baseUrl, paste);
|
||||||
if (createdPaste && createdPaste.isPrivate) {
|
if (!isAutosave && createdPaste && !paste.pasteKey) {
|
||||||
setKey(createdPaste.pasteKey);
|
setCreatedKey(createdPaste.pasteKey);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
console.error("Error:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PastePanel onSubmit={handleSubmit} publicPastes={filtered} />
|
<PastePanel
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
publicPastes={filtered}
|
||||||
|
mode={mode}
|
||||||
|
pasteKey={pasteKey}
|
||||||
|
onConnectChange={onConnectChange}
|
||||||
|
/>
|
||||||
|
|
||||||
<NotificationModal
|
<NotificationModal
|
||||||
show={key !== null}
|
show={createdKey !== null}
|
||||||
onClose={() => setKey(null)}
|
onClose={() => setCreatedKey(null)}
|
||||||
title="Link a tu paste privado"
|
title="¡Bomba! Paste creado"
|
||||||
message={
|
message={
|
||||||
<span>
|
<span>
|
||||||
Tu paste privado ha sido creado. Puedes acceder a él mediante el siguiente enlace:
|
Tu paste se ha guardado correctamente. Puedes compartirlo con este enlace:
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<a href={`https://paste.miarma.net/${key}`}>https://paste.miarma.net/{key}</a>
|
<a
|
||||||
|
href={`https://paste.miarma.net/s/${createdKey}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-primary font-weight-bold"
|
||||||
|
>
|
||||||
|
https://paste.miarma.net/s/{createdKey}
|
||||||
|
</a>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
Recuerda que este enlace es único y no se puede recuperar si se pierde.
|
{mode === 'rt' && "Nota: Al guardarlo, se ha creado una copia estática permanente."}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
variant=""
|
variant="success"
|
||||||
buttons={[
|
buttons={[
|
||||||
{ label: "Cerrar", variant: "secondary", onClick: () => setKey(null) }
|
{ label: "Cerrar", variant: "secondary", onClick: () => setCreatedKey(null) }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,4 +14,7 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
global: 'window'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user