diff --git a/package-lock.json b/package-lock.json index 1a87541..e3df2e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,16 @@ "name": "almeira-web", "version": "0.1.0", "dependencies": { + "@tiptap/extension-color": "^3.21.0", + "@tiptap/extension-font-family": "^3.21.0", + "@tiptap/extension-highlight": "^3.21.0", + "@tiptap/extension-link": "^3.21.0", + "@tiptap/extension-text-align": "^3.21.0", + "@tiptap/extension-text-style": "^3.21.0", + "@tiptap/extension-underline": "^3.21.0", + "@tiptap/react": "^3.21.0", + "@tiptap/starter-kit": "^3.21.0", + "@types/video.js": "^7.3.58", "appwrite": "^23.0.0", "axios": "^1.13.6", "clsx": "^2.1.1", @@ -16,7 +26,9 @@ "node-appwrite": "^22.1.3", "react": "19.2.3", "react-dom": "19.2.3", - "tailwind-merge": "^3.5.0" + "tailwind-merge": "^3.5.0", + "video.js": "^8.23.7", + "zustand": "^5.0.12" }, "devDependencies": { "@tailwindcss/postcss": "^4.2.2", @@ -236,6 +248,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -461,6 +482,34 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT", + "optional": true + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1234,6 +1283,12 @@ "node": ">=12.4.0" } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1521,6 +1576,505 @@ "tailwindcss": "4.2.2" } }, + "node_modules/@tiptap/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.21.0.tgz", + "integrity": "sha512-IfnQiuEeabDSPr1C/zHFTbnvlTf5z0DE/d/xz4C6bkL4ZBDJ3rr99h2qsaV0l8F+kbNswZMlQdM8rxNlMy95fQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.21.0.tgz", + "integrity": "sha512-JDM/RR6rM0dMCZ1UnEf7eqmN6pAdIa2llhN+E24HdTGNJCklMFhLAGE/OT8/1r7M0WWA9GVO7/PTe4EdGh6+lQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.21.0.tgz", + "integrity": "sha512-iyEJRzG7XTCPlHwEDzUw3HnuYYCfL7lNpcCHmxcpYMrIUA8rv7EUxerIwApT6xY8hQ/07ljuJKgOyPvnJOOzuA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.21.0.tgz", + "integrity": "sha512-/fabRRhhf8i4LAx9e8xz9ppqN5KgdJk3TxMuxAD5vAWGsejvhSoPa8O8H/QwwyntXm1Vue8aQiMHsUk48b2hGQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.21.0.tgz", + "integrity": "sha512-PWNF+xwxgOeXYGD88sCQLKL0eBoQqjUnZNALxBjN3Y7x4llalh42rHOp2Nt2t6UbQgqTBtBzU/uFcussTpxreQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.21.0.tgz", + "integrity": "sha512-D7wA9jp+4X2r1f3FIoga73s6Rn4rmZY57Jes6a4rK3HY+3yHk1r057pPIZSY8Drfs97jxHQVFdfUYUomLSFYBA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.21.0.tgz", + "integrity": "sha512-zrVOcOzDCjHQ8NJcC+qHmZZKiwnP/NMSb3qVJlSMN8TzuHept1MZCDa2Mbo70O6I0txo456SGuXB9sqV1vHmGg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-color": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.21.0.tgz", + "integrity": "sha512-LbzMcUPz148A+4g42GQ3FqkwI4+p8w+9W0s5HAo6aVMjsOaMSL12CqNhG4Kj/6zOq32WkVSa3FxpDNkuMn0jzA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.21.0.tgz", + "integrity": "sha512-7oCyzXI9ChvJQUlr23AURdfVar4OIsrYUvqdhEwo3bjcI/Q/j0KJiXfuh6ZzL5eVaINSailH53sZaGg4THQtUg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.21.0.tgz", + "integrity": "sha512-6fsDSVAM2iz7eElvT6iivMrGBGjIP/oPigVZ/SPm6f31phaYhz6TIOEgV/Lr2jaPIOgyK4U0cU4Yd4KUBCmhzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.21.0.tgz", + "integrity": "sha512-n2HzTB+I/5rAl8R/1sKMv92JiY1oDK1hroXizxEKYa6dskJcAMW0CfYyPcPOZWQQEe7qoeOvQISr2ooLAKW+Mw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-font-family": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.21.0.tgz", + "integrity": "sha512-8GzFgL34QOs/GkLuhHuy5FF3y8/4EVFeX4P86cNg6AMUQNFMfjAbDfqqqav+Bxv14T/JV9dxyW+KyYPb6eK5/Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.21.0.tgz", + "integrity": "sha512-wGjgAoYBTvPAe9QYMI5px355XcNeMkaUrMY9IHbMqgqdmHcDxqooxM4H6sYVX2CRcHwXy4I8NQAoOhSYrQJDMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.21.0.tgz", + "integrity": "sha512-6JFVSAOQ1qhQHi9mVcdn2/XO8YIMgYV8zjarzNUzP6Sf2waeE5BLXjlg6rIH/945sY1J+FndTojLru6gQ07a5A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.21.0.tgz", + "integrity": "sha512-ji6VJmoRnDzAHYflEYEZohMHRi77UGLW1o3ua7UhI32iJ9nuYssbPNuzEeE4SvENMQwZRszad5+a+dKAa+NC7g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-highlight": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.21.0.tgz", + "integrity": "sha512-3f/bVgfm2dJZxalh07TThDxcTaeXJ+dpYyRY9trnFeHbhyYQXSy6yzkNhNcYB4Ua5jxpKQv4b9Q448QVh+KNzA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.21.0.tgz", + "integrity": "sha512-vNBnOfFEY62CoJPGo4nonRM7RiOvhII1vhoO+WFr1GxDqCAfmEFjToflt7JT1UJdo6lMVcD+aaaAgOiuSz5p6g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.21.0.tgz", + "integrity": "sha512-2I8oPvwyXhRn1k8lbDFIutzvhtLEjoO5mmQCNX4TnT4PdxxaSrK9+ihYg12VeqhUeO7dg1MKiFqws0HVBrwzWg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.21.0.tgz", + "integrity": "sha512-oMU7Yve1sbgBsaFAUc2R0GPf4d3ZPVJeMUFC6b6X9rJIvx/IhEUEn9toQcSBGfp02uWK9NdQyIFYFdWlVXH++w==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.21.0.tgz", + "integrity": "sha512-KeBlEtLrGce2d3dgL89hmwWEtREuzlW4XY5bYWpKNvCbFqvdSb3n7vkdkw32YclZmMWxAcABgW6ucCStkE0rsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.21.0.tgz", + "integrity": "sha512-1ZymZmlQVbAoC4q5x3cro0v5+3I6l+BHqbhIMQLjQFlAOJfcE0pvqRzAFW7PduxUj41tXEtsYqp2NREvO9F5Fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.21.0.tgz", + "integrity": "sha512-EzrfW3ASNFPWKhR8sNOq7Kqw4hvaTAOn4dlI7chB8HIANSrlyPOUn+eKAnO6HQgsUgsbjg2GbTUrGrxcoLykUg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.21.0.tgz", + "integrity": "sha512-+d+0orokMfqaBfvr9tUBgGvo2ZCV+fR3JzsJTmnLBWOkhBSJN7H4pnfXPTue0qwspUwRmkLJxdIlU+J7HkMrng==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.21.0.tgz", + "integrity": "sha512-cMPG/jCoZ9NmLZ5ctFziILaxJGfDtMTb5OLBhifMFZeMVwF1pEJIygDEfnX/HSruv507weZSQG4pERO2tRszMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.21.0.tgz", + "integrity": "sha512-easnVaN11Wl+5fOtfvzJ10J762S9TRXZaMj5rLBGavgf82DCYHqhGhBqpLQrJ41r4nPABGlYvTRoxfvBLB74Lg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.21.0.tgz", + "integrity": "sha512-Zx8QdB8a5iBuE4uO21c3BjmpBfaJEr2Jd1QFnsdgx11fm6P7dGgZaGko1FaINhfOPRGTN6O/kiF02cDMdOHa/w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.21.0.tgz", + "integrity": "sha512-XaoUaq45nai9LLoUStumMItHhhCnmXirPR8mTjEDKZ2QD0Kg/YHEr6guJYc6qKB7YA+Wa1EgBIrZOv1+d2Pdag==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.21.0.tgz", + "integrity": "sha512-MI/3X75D45Wa/+0Fp8nYfNJq5makkjtG7B2/lIzNUe0kEKJ56RVQoV1GQSXxAiFNyAYRfKFq8dJslhesW3EkWA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.21.0.tgz", + "integrity": "sha512-gGmBEymbWnr8AIS8bI/bPw5rcwo7wAFcBw/TsLd1nAanu1dDqSRNDBrit3m02Ru+D88u2SfNvmbOPI1pz+1f5w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.21.0.tgz", + "integrity": "sha512-MN1uh5PmHT1F2BNsbc21MIS0AMFFA73oODlp/4ckpBR4o5AxRwV+8f43Cd52UL4MgMkKj/A+QfZ7iK9IDb0h5A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.21.0.tgz", + "integrity": "sha512-I3sNo7oMMsR6FFz1ecvPb9uCF0VQuS2WV67j8Io2M7DJicRWCE/GM5DaiYjTeWBbnByk6BuG0txoJATAqPVliQ==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.21.0.tgz", + "integrity": "sha512-p+OKJgxmFB3t5nY3mjaqjKaj8vJX9++OkdrZLRxYuG7ScAHemWraWQ25sgNZl1LDaRYrdnNYxx9MP0CXOSB6ew==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.21.0", + "@tiptap/extension-floating-menu": "^3.21.0" + }, + "peerDependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.21.0.tgz", + "integrity": "sha512-w7fWxglDtqXFBgRYH+LforJyUboSAQllnWQbGVSTyX4rsICqZjkb3f6CTSUWpGoGKmlmbb2ZpEuoik7tur9d8Q==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/extension-blockquote": "^3.21.0", + "@tiptap/extension-bold": "^3.21.0", + "@tiptap/extension-bullet-list": "^3.21.0", + "@tiptap/extension-code": "^3.21.0", + "@tiptap/extension-code-block": "^3.21.0", + "@tiptap/extension-document": "^3.21.0", + "@tiptap/extension-dropcursor": "^3.21.0", + "@tiptap/extension-gapcursor": "^3.21.0", + "@tiptap/extension-hard-break": "^3.21.0", + "@tiptap/extension-heading": "^3.21.0", + "@tiptap/extension-horizontal-rule": "^3.21.0", + "@tiptap/extension-italic": "^3.21.0", + "@tiptap/extension-link": "^3.21.0", + "@tiptap/extension-list": "^3.21.0", + "@tiptap/extension-list-item": "^3.21.0", + "@tiptap/extension-list-keymap": "^3.21.0", + "@tiptap/extension-ordered-list": "^3.21.0", + "@tiptap/extension-paragraph": "^3.21.0", + "@tiptap/extension-strike": "^3.21.0", + "@tiptap/extension-text": "^3.21.0", + "@tiptap/extension-underline": "^3.21.0", + "@tiptap/extensions": "^3.21.0", + "@tiptap/pm": "^3.21.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1553,6 +2107,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", @@ -1567,7 +2143,6 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1577,12 +2152,23 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/video.js": { + "version": "7.3.58", + "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.58.tgz", + "integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", @@ -2147,6 +2733,63 @@ "win32" ] }, + "node_modules/@videojs/http-streaming": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.4.tgz", + "integrity": "sha512-XAvdG2dolBuV2Fx8bu1kjmQ2D4TonGzZH68Pgv/O9xMSFWdZtITSMFismeQLEAtMmGwze8qNJp3RgV+jStrJqg==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "aes-decrypter": "^4.0.2", + "global": "^4.4.0", + "m3u8-parser": "^7.2.0", + "mpd-parser": "^1.3.1", + "mux.js": "7.1.0", + "video.js": "^7 || ^8" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^8.19.0" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2170,6 +2813,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -2216,7 +2871,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -2739,6 +3393,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2758,7 +3418,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2915,6 +3574,11 @@ "node": ">=0.10.0" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2957,6 +3621,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -3145,7 +3821,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3591,6 +4266,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3916,6 +4600,16 @@ "node": ">=10.13.0" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4309,6 +5003,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "license": "MIT" + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4979,6 +5679,21 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5034,6 +5749,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5044,6 +5770,23 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5053,6 +5796,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5098,6 +5847,15 @@ "node": ">= 0.6" } }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "license": "MIT", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5121,6 +5879,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mpd-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz", + "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5128,6 +5901,23 @@ "dev": true, "license": "MIT" }, + "node_modules/mux.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz", + "integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5433,6 +6223,12 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -5542,6 +6338,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5598,6 +6406,15 @@ "node": ">= 0.8.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5610,6 +6427,201 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", + "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz", + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.7", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz", + "integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5626,6 +6638,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5771,6 +6792,12 @@ "node": ">=0.10.0" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6553,6 +7580,12 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -6655,6 +7688,72 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/video.js": { + "version": "8.23.7", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.7.tgz", + "integrity": "sha512-cG4HOygYt+Z8j6Sf5DuK6OgEOoM+g9oGP6vpqoZRaD13aHE4PMITbyjJUXZcIQbgB0wJEadBRaVm5lJIzo2jAA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@videojs/http-streaming": "^3.17.3", + "@videojs/vhs-utils": "^4.1.1", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.2", + "global": "4.4.0", + "m3u8-parser": "^7.2.0", + "mpd-parser": "^1.3.1", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "node_modules/videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "license": "Apache-2.0", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "video.js": "^8" + } + }, + "node_modules/videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==", + "license": "Apache-2.0" + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "license": "Apache-2.0", + "dependencies": { + "global": "^4.3.1" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6812,6 +7911,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index e290a6a..2a51b9a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,16 @@ "lint": "eslint" }, "dependencies": { + "@tiptap/extension-color": "^3.21.0", + "@tiptap/extension-font-family": "^3.21.0", + "@tiptap/extension-highlight": "^3.21.0", + "@tiptap/extension-link": "^3.21.0", + "@tiptap/extension-text-align": "^3.21.0", + "@tiptap/extension-text-style": "^3.21.0", + "@tiptap/extension-underline": "^3.21.0", + "@tiptap/react": "^3.21.0", + "@tiptap/starter-kit": "^3.21.0", + "@types/video.js": "^7.3.58", "appwrite": "^23.0.0", "axios": "^1.13.6", "clsx": "^2.1.1", @@ -17,7 +27,9 @@ "node-appwrite": "^22.1.3", "react": "19.2.3", "react-dom": "19.2.3", - "tailwind-merge": "^3.5.0" + "tailwind-merge": "^3.5.0", + "video.js": "^8.23.7", + "zustand": "^5.0.12" }, "devDependencies": { "@tailwindcss/postcss": "^4.2.2", diff --git a/setup_whatsapp.js b/setup_whatsapp.js new file mode 100644 index 0000000..636ae2d --- /dev/null +++ b/setup_whatsapp.js @@ -0,0 +1,75 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client(); +const databases = new sdk.Databases(client); + +// Use credentials for Almeira Web Cloud (Coolify) +const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; +const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; +const API_KEY = process.env.APPWRITE_API_KEY || 'standard_ee81a4ca6784c0fa6a23abab0a983dbba9cd0c2d8e211a00c759d9801f5d8e27b43a605bfa373e13106da87acfc7673608f0ffb0eb04e10cae3dae93d2404bfda8676f96e3fcfd6bd642f6e12d69367916b796c54e98e2e4c4bc0b7a27aa42ba5048582fcbd45b1365b51c5014cb939c1853923c418048ff0e39c2237065da3c'; + +client + .setEndpoint(ENDPOINT) + .setProject(PROJECT_ID) + .setKey(API_KEY); + +const delay = ms => new Promise(res => setTimeout(res, ms)); +const dbId = 'almeira'; +const collectionId = 'whatsapp_messages'; + +async function setupWhatsApp() { + try { + console.log('========================================'); + console.log('Almeira Web — Appwrite Messaging Setup'); + console.log('========================================\n'); + + console.log('[1/2] Creando colección: whatsapp_messages...'); + try { + // Permisos de lectura públicos: SOLO para mensajes aprobados (se gestionará mediante queries o permisos más adelante) + // Por ahora, solo permitimos lectura por el rol 'any' para simular real-time en el dashboard (se puede restringir a usuarios autenticados después) + await databases.createCollection(dbId, collectionId, 'Mensajes WhatsApp (Evolution API)', [ + sdk.Permission.read(sdk.Role.any()), // Permitido lectura para el locutor y realtime (para simplicidad de demo) + sdk.Permission.write(sdk.Role.any()) // El webhook usará Client Admin con API_KEY, pero dejamos esto base + ], false); + console.log(' ✓ Colección creada.'); + } catch(e){ + console.log(' > Colección ya existe.'); + } + await delay(1000); + + console.log('[2/2] Configurando atributos...'); + + const attributes = [ + { name: 'sender_name', type: 'string', size: 255, required: true }, + { name: 'phone_number', type: 'string', size: 50, required: true }, + { name: 'content', type: 'string', size: 5000, required: true }, + { name: 'timestamp', type: 'datetime', required: true }, + { name: 'status', type: 'string', size: 50, required: true, default: 'pending' }, + { name: 'platform', type: 'string', size: 50, required: true, default: 'whatsapp' }, + { name: 'avatar', type: 'string', size: 1000, required: false } + ]; + + for (const attr of attributes) { + try { + if (attr.type === 'string') { + await databases.createStringAttribute(dbId, collectionId, attr.name, attr.size, attr.required, attr.default); + } else if (attr.type === 'datetime') { + await databases.createDatetimeAttribute(dbId, collectionId, attr.name, attr.required); + } + console.log(` ✓ Atributo ${attr.name} creado.`); + await delay(500); + } catch (e) { + console.log(` > Atributo ${attr.name} ya existe.`); + } + } + + console.log('\n========================================'); + console.log('✅ Base de Datos para WhatsApp LISTA!'); + console.log('========================================'); + + } catch(e) { + console.error('\n❌ Error setups:', e.message); + } +} + +setupWhatsApp(); diff --git a/src/app/actions/user.ts b/src/app/actions/user.ts new file mode 100644 index 0000000..1b0a8f1 --- /dev/null +++ b/src/app/actions/user.ts @@ -0,0 +1,23 @@ +'use server'; + +import { getUserPreferences, updateUserPreference, UserPreferences } from '@/lib/preferences'; +import { revalidatePath } from 'next/cache'; + +/** + * Server action to get user preferences + */ +export async function fetchUserPreferencesAction() { + return await getUserPreferences(); +} + +/** + * Server action to update user preferences and revalidate the dashboard + */ +export async function updateUserPreferencesAction(update: Partial) { + const success = await updateUserPreference(update); + if (success) { + // Revalidate all dashboard pages to reflect UI changes immediately + revalidatePath('/dashboard', 'layout'); + } + return { success }; +} diff --git a/src/app/api/webhooks/whatsapp/route.ts b/src/app/api/webhooks/whatsapp/route.ts new file mode 100644 index 0000000..6ec7940 --- /dev/null +++ b/src/app/api/webhooks/whatsapp/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server'; +import { ID, Databases } from 'node-appwrite'; +import { createAdminClient } from '@/lib/appwrite.server'; +import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; + +export async function POST(req: Request) { + try { + const payload = await req.json(); + + // Estructura típica de Evolution API + const messageData = { + sender_name: payload.data?.pushName || payload.data?.key?.remoteJid || 'Usuario WhatsApp', + phone_number: payload.data?.key?.remoteJid?.split('@')[0] || 'Desconocido', + content: payload.data?.message?.conversation || payload.data?.message?.extendedTextMessage?.text || 'Mensaje Multimedia', + timestamp: new Date().toISOString(), + status: 'pending', // Por defecto entran para moderación + platform: 'whatsapp', + avatar: payload.data?.profilePicUrl || '' + }; + + const { databases } = await createAdminClient(); + + // NOTA: Asegúrate de que la colección 'whatsapp_messages' exista en Appwrite + await databases.createDocument( + APPWRITE_ALMEIRA_CONFIG.databaseId, + 'whatsapp_messages', + ID.unique(), + messageData + ); + + return NextResponse.json({ success: true, message: 'Mensaje recibido y guardado' }, { status: 200 }); + } catch (error: any) { + console.error('Error in WhatsApp Webhook:', error); + return NextResponse.json({ success: false, error: error.message }, { status: 500 }); + } +} diff --git a/src/app/dashboard/[entity]/[id]/page.tsx b/src/app/dashboard/[entity]/[id]/page.tsx index 0ba2f57..54ad110 100644 --- a/src/app/dashboard/[entity]/[id]/page.tsx +++ b/src/app/dashboard/[entity]/[id]/page.tsx @@ -1,9 +1,9 @@ -import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; +import { APPWRITE_ALMEIRA_CONFIG, projectId, endpoint } from '@/lib/appwrite'; import { createSessionClient } from '@/lib/appwrite.server'; import { collectionMap, SCHEMA_MAP } from '@/lib/schema'; -import Link from 'next/link'; import { updateDocumentServer } from '@/app/actions/appwrite'; import { redirect } from 'next/navigation'; +import { EntityForm } from '@/components/dashboard/EntityForm'; export default async function EditEntityPage({ params }: { params: Promise<{ entity: string, id: string }> }) { const { entity, id } = await params; @@ -11,6 +11,13 @@ export default async function EditEntityPage({ params }: { params: Promise<{ ent const collectionId = collectionMap[entity]; const fields = SCHEMA_MAP[entity] || []; + const entityNames: Record = { + posts: 'Publicaciones', categories: 'Categorías', clients: 'Clientes', client_types: 'Tipos de Cliente', + countries: 'Países', departments: 'Departamentos', cities: 'Ciudades', contracts: 'Contratos', + payment_methods: 'Formas de Pago', publication_types: 'Tipos de Publicación' + }; + const entityName = entityNames[entity] || entity.replace(/_/g, ' '); + if (!collectionId || fields.length === 0) { return (

Configuración no encontrada para {entity}

@@ -32,8 +39,9 @@ export default async function EditEntityPage({ params }: { params: Promise<{ ent ); } catch (e) { return ( -

No se encontró el registro o no tienes permisos.

- ← Volver
+
+

No se encontró el registro o no tienes permisos.

+
); } @@ -53,7 +61,6 @@ export default async function EditEntityPage({ params }: { params: Promise<{ ent async function updateAction(formData: FormData) { 'use server'; - // Convert formData to simple object const data: Record = {}; for (const [key, value] of formData.entries()) { if (key.startsWith('$ACTION')) continue; @@ -64,6 +71,10 @@ export default async function EditEntityPage({ params }: { params: Promise<{ ent for (const nKey of numericFields) { if(data[nKey]) data[nKey] = parseFloat(data[nKey]); } + + if (data.estado && typeof data.estado === 'string') { + data.estado = data.estado.toLowerCase(); + } const res = await updateDocumentServer(collectionId, id, data); if(res.success) { @@ -73,105 +84,15 @@ export default async function EditEntityPage({ params }: { params: Promise<{ ent } } - const getDefValue = (field: any) => { - const val = document[field.name]; - if (val === undefined || val === null) return ''; - if (field.type === 'boolean') return val ? 'true' : 'false'; - if (field.type === 'date') return new Date(val).toISOString().slice(0, 16); - if (field.type === 'relation') return typeof val === 'object' ? val.$id : val; - return val; - }; - return ( -
-
- - ← Cancelar - -

- EDITAR: {id} -

-
- -
-
-
- - {fields.map(field => { - const defValue = getDefValue(field); - - return ( -
- - - {field.type === 'text' && ( - - )} - - {field.type === 'number' && ( - - )} - - {field.type === 'url' && ( - - )} - - {field.type === 'date' && ( - - )} - - {field.type === 'textarea' && ( - - )} - - {field.type === 'boolean' && ( -
- -
▼
-
- )} - - {field.type === 'select' && field.options && ( -
- -
▼
-
- )} - - {field.type === 'relation' && ( -
- -
▼
-
- )} - -
- ) - })} - -
- -
-
-
-
+ ); } diff --git a/src/app/dashboard/[entity]/new/page.tsx b/src/app/dashboard/[entity]/new/page.tsx index 27c0874..cc14a40 100644 --- a/src/app/dashboard/[entity]/new/page.tsx +++ b/src/app/dashboard/[entity]/new/page.tsx @@ -1,9 +1,9 @@ -import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; +import { APPWRITE_ALMEIRA_CONFIG, projectId, endpoint } from '@/lib/appwrite'; import { createSessionClient } from '@/lib/appwrite.server'; import { collectionMap, SCHEMA_MAP } from '@/lib/schema'; -import Link from 'next/link'; import { createDocumentServer } from '@/app/actions/appwrite'; import { redirect } from 'next/navigation'; +import { EntityForm } from '@/components/dashboard/EntityForm'; export default async function NewEntityPage({ params }: { params: Promise<{ entity: string }> }) { const { entity } = await params; @@ -24,14 +24,11 @@ export default async function NewEntityPage({ params }: { params: Promise<{ enti ); } - // Inject user session secure database client const sessionClient = await createSessionClient(); if (!sessionClient) { throw new Error("No sesión activa"); } - // Fetch relationship options - // For each field of type 'relation', fetch the collection const relationData: Record = {}; await Promise.all( fields.filter(f => f.type === 'relation' && f.relationCollection).map(async (field) => { @@ -47,116 +44,37 @@ export default async function NewEntityPage({ params }: { params: Promise<{ enti async function submitAction(formData: FormData) { 'use server'; - // Convert formData to simple object const data: Record = {}; for (const [key, value] of formData.entries()) { if (key.startsWith('$ACTION')) continue; data[key] = value; } - // Attempt coercing numeric fields manually if defined in schema as 'number' const numericFields = fields.filter(f => f.type === 'number').map(f => f.name); for (const nKey of numericFields) { if(data[nKey]) data[nKey] = parseFloat(data[nKey]); } + + if (data.estado && typeof data.estado === 'string') { + data.estado = data.estado.toLowerCase(); + } const res = await createDocumentServer(collectionId, data); if(res.success) { redirect(`/dashboard/${entity}`); } else { console.error("Server Action res error", res.error); - // Fallback redirect with error state could be used here } } return ( -
-
- - ← Regresar - -

- NUEVO: {entityName} -

-
- -
-
-
- - {fields.map(field => { - return ( -
- - - {field.type === 'text' && ( - - )} - - {field.type === 'number' && ( - - )} - - {field.type === 'url' && ( - - )} - - {field.type === 'date' && ( - - )} - - {field.type === 'textarea' && ( - - )} - - {field.type === 'boolean' && ( -
- -
▼
-
- )} - - {field.type === 'select' && field.options && ( -
- -
▼
-
- )} - - {field.type === 'relation' && ( -
- -
▼
-
- )} - -
- ) - })} - -
- -
-
-
-
+ ); } diff --git a/src/app/dashboard/[entity]/page.tsx b/src/app/dashboard/[entity]/page.tsx index 4ec10a3..d2173c2 100644 --- a/src/app/dashboard/[entity]/page.tsx +++ b/src/app/dashboard/[entity]/page.tsx @@ -2,22 +2,23 @@ import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; import { createSessionClient } from '@/lib/appwrite.server'; import { Query } from 'node-appwrite'; import Link from 'next/link'; -import { Pencil } from 'lucide-react'; -import { DeleteButton } from '@/components/DeleteButton'; - import { collectionMap, SCHEMA_MAP } from '@/lib/schema'; +import { DataTable } from '@/components/dashboard/DataTable'; +import { getUserPreferences } from '@/lib/preferences'; export default async function EntityPage({ params, searchParams }: { params: Promise<{ entity: string }>, - searchParams: Promise<{ q?: string, page?: string }> + searchParams: Promise<{ q?: string, page?: string, sort?: string, dir?: string }> }) { const { entity } = await params; const sParams = await searchParams; const q = sParams.q || ''; const page = Number(sParams.page) || 1; + const sort = sParams.sort || '$createdAt'; + const dir = (sParams.dir === 'asc' || sParams.dir === 'desc') ? sParams.dir : 'desc'; const limit = 15; const offset = (page - 1) * limit; @@ -41,6 +42,10 @@ export default async function EntityPage({ ); } + // Fetch preferences + const prefs = await getUserPreferences(); + const hiddenColumns = prefs.hiddenColumns?.[entity] || []; + let documents: any[] = []; let total = 0; try { @@ -52,18 +57,22 @@ export default async function EntityPage({ const queries = [ Query.limit(limit), Query.offset(offset), - Query.orderDesc('$createdAt') ]; + // Apply Sorting + if (dir === 'asc') { + queries.push(Query.orderAsc(sort)); + } else { + queries.push(Query.orderDesc(sort)); + } + if (q) { const searchableFields = fieldsConfig .filter(f => f.type === 'text' || f.type === 'textarea') .map(f => f.name) - .slice(0, 2); // Limit to first two text fields for performance + .slice(0, 2); if (searchableFields.length > 0) { - // Note: Appwrite multiple contains are AND-ed by default. - // For a better search we might need something else but this is a good start. queries.push(Query.contains(searchableFields[0], q)); } } @@ -79,18 +88,6 @@ export default async function EntityPage({ console.error(`Error fetching from ${collectionId}:`, error); } - let rawHeaders: string[] = []; - if (documents.length > 0) { - rawHeaders = Object.keys(documents[0]).filter(k => !k.startsWith('$')); - } else if (fieldsConfig.length > 0) { - rawHeaders = fieldsConfig.map(f => f.name); - } - - const getHeaderLabel = (key: string) => { - const field = fieldsConfig.find(f => f.name === key); - return field ? field.label : key.replace(/_/g, ' '); - }; - return (
@@ -121,73 +118,21 @@ export default async function EntityPage({
-
- {documents.length === 0 ? ( -
-

NO SE ENCONTRARON REGISTROS

- Prueba ajustando tu búsqueda o crea un nuevo elemento. -
- ) : ( -
- - - - {rawHeaders.map(h => ( - - ))} - - - - - {documents.map((doc) => ( - - {rawHeaders.map(h => { - let value = doc[h]; - const field = fieldsConfig.find(f => f.name === h); - - // Resolve Relations - if (typeof value === 'object' && value !== null) { - if (field?.type === 'relation' && field.relationLabel) { - value = value[field.relationLabel] || value.$id || '[Vinculado]'; - } else { - value = Array.isArray(value) ? `[Lista (${value.length})]` : value.$id || '[Vinculado]'; - } - } - - if (typeof value === 'boolean') { - value = value ? 'Sí' : 'No'; - } - if (typeof value === 'string' && value.length > 50) { - value = value.substring(0, 50) + '...'; - } - - return ( - - ) - })} - - - ))} - -
- {getHeaderLabel(h)} - Acciones
- {String(value || '')} - - - - - -
-
- )} -
+ {/* Pagination */} {total > limit && (
ANTERIOR @@ -196,7 +141,7 @@ export default async function EntityPage({ Página {page} de {Math.ceil(total / limit)} = Math.ceil(total / limit) ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`} > SIGUIENTE diff --git a/src/app/dashboard/locutor/page.tsx b/src/app/dashboard/locutor/page.tsx new file mode 100644 index 0000000..245aaeb --- /dev/null +++ b/src/app/dashboard/locutor/page.tsx @@ -0,0 +1,337 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import client, { databases, APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; +import { Query } from 'appwrite'; +import { RealtimeResponseEvent } from 'appwrite'; +import { + MessageCircle, CheckCircle, XCircle, Trash2, + User, Phone, Clock, Radio, ShieldAlert, + ExternalLink, Zap, Bell, Monitor, Settings +} from 'lucide-react'; + +export default function LocutorPage() { + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [lastReceivedId, setLastReceivedId] = useState(null); + + const fetchInitialMessages = async () => { + try { + const res = await databases.listDocuments( + APPWRITE_ALMEIRA_CONFIG.databaseId, + 'whatsapp_messages', + [Query.orderDesc('timestamp'), Query.limit(30)] + ); + setMessages(res.documents); + } catch (error) { + console.error('Error fetching messages:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchInitialMessages(); + + // Realtime Subscription + const unsubscribe = client.subscribe( + `databases.${APPWRITE_ALMEIRA_CONFIG.databaseId}.collections.whatsapp_messages.documents`, + (response: RealtimeResponseEvent) => { + if (response.events.includes('databases.*.collections.*.documents.*.create')) { + setMessages((prev) => { + // Evitar duplicados por si acaso + if (prev.some(m => m.$id === response.payload.$id)) return prev; + return [response.payload, ...prev].slice(0, 50); + }); + setLastReceivedId(response.payload.$id); + } + if (response.events.includes('databases.*.collections.*.documents.*.update')) { + setMessages((prev) => + prev.map((msg) => msg.$id === (response.payload as any).$id ? response.payload : msg) + ); + } + if (response.events.includes('databases.*.collections.*.documents.*.delete')) { + setMessages((prev) => prev.filter((msg) => msg.$id !== (response.payload as any).$id)); + } + } + ); + + return () => unsubscribe(); + }, []); + + const updateStatus = async (id: string, status: 'approved' | 'rejected') => { + try { + await databases.updateDocument( + APPWRITE_ALMEIRA_CONFIG.databaseId, + 'whatsapp_messages', + id, + { status } + ); + } catch (error) { + console.error('Error updating status:', error); + } + }; + + const deleteMessage = async (id: string) => { + if (!confirm('¿Eliminar este mensaje permanentemente?')) return; + try { + await databases.deleteDocument( + APPWRITE_ALMEIRA_CONFIG.databaseId, + 'whatsapp_messages', + id + ); + } catch (error) { + console.error('Error deleting message:', error); + } + }; + + return ( +
+ {/* Cabecera Técnica Estilo Studio */} +
+
+
+
+
+ +
+
+
+
+
+
+

+ Control Master LIVE 360 +

+

Almeira Multimedia Ecosystem

+
+
+ +
+
+ Estado del Sistema + + Webhook Activo + +
+
+ +
+
+
+ +
+ + {/* Panel Central de Mensajería (8 columnas) */} +
+
+
+
+ +
+

Interactividad WhatsApp

+
+
+ + Pendientes: {messages.filter(m => m.status === 'pending').length} + +
+
+ + {isLoading ? ( +
+
+

Sincronizando Base de Datos...

+
+ ) : messages.length === 0 ? ( +
+
+ +
+

Sin actividad reciente

+

Los mensajes de WhatsApp aparecerán aquí automáticamente en tiempo real una vez configurado el webhook.

+
+ ) : ( +
+ {messages.map((msg) => ( +
+ {/* Avatar & Icon */} +
+
+ {msg.avatar ? ( + Avatar + ) : ( + + )} +
+
+ +
+
+ + {/* Content */} +
+
+
+

{msg.sender_name}

+ {msg.phone_number} +
+
+ + {new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+
+

+ {msg.content} +

+
+
+ + {/* Actions */} +
+ {msg.status === 'pending' ? ( + <> + + + + ) : ( +
+ {msg.status === 'approved' ? : } +
+ )} + +
+ + {/* Indicadores de estado visuales */} + {msg.status === 'approved' && ( +
+
Al Aire
+
+ )} +
+ ))} +
+ )} +
+ + {/* Sidebar Moderno de Control (4 columnas) */} +
+ + {/* Tarjeta de Estadísticas Rápidas */} +
+
+

Aprobados

+

{messages.filter(m => m.status === 'approved').length}

+
+
+

Total Hoy

+

{messages.length}

+
+
+ +
+
+ +

+ Broadcast Engine +

+ +
+
+ +
+ + +
+
+ + + + +
+
+ +
+

Ayuda Cabina

+
    +
  • + 1 + Al marcar como "Al Aire", el mensaje rotará cada 15 segundos en el News Ticker global. +
  • +
  • + 2 + Las notificaciones LIVE interrumpen cualquier overlay previo en la App móvil. +
  • +
+
+
+
+ + +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1165485..5ea1100 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { Navbar } from "@/components/Navbar"; import NewsTicker from "@/components/NewsTicker"; import { GlobalTopBanner } from "@/components/AdWrappers"; import { getEntity } from "@/app/actions/entity"; +import { GlobalPlayer } from "@/components/media/GlobalPlayer"; import "./globals.css"; const outfit = Outfit({ subsets: ["latin"], variable: "--font-outfit" }); @@ -38,6 +39,7 @@ export default async function RootLayout({ {children}
+ ); diff --git a/src/components/NewsTicker.tsx b/src/components/NewsTicker.tsx index d321dcb..962fd64 100644 --- a/src/components/NewsTicker.tsx +++ b/src/components/NewsTicker.tsx @@ -2,12 +2,12 @@ import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; import { createSessionClient } from '@/lib/appwrite.server'; import { Query } from 'node-appwrite'; import Link from 'next/link'; +import { MessageCircle } from 'lucide-react'; export default async function NewsTicker() { - let noticias: any[] = []; + let tickerItems: any[] = []; try { - // NewsTicker is public, we use admin client if session is not available let client: any = await createSessionClient(); if (!client) { const { createAdminClient } = await import('@/lib/appwrite.server'); @@ -16,24 +16,18 @@ export default async function NewsTicker() { if (!client) throw new Error("No Appwrite client available"); - // We need an Anonymous or Public read client if this is shown in home. - // Since 'createSessionClient' uses the session cookie, we might have issues on public incognito pages. - // The user previously used Admin API key for everything or a local backdoor. - - // Settings + // 1. Fetch Settings const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []); const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {}; const tickerCount = settings.ticker_count || 10; - // 2. Get NOTICIA type ID + // 2. Fetch News (NOTICIA) const typeRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'publication_types', [ Query.equal('codigo', 'NOTICIA') ]); - let newsItems: any[] = []; if (typeRes.documents.length > 0) { const typeId = typeRes.documents[0].$id; - const postsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'posts', [ Query.equal('tipo_publicacion_id', typeId), Query.equal('estado', 'Publicado'), @@ -51,55 +45,86 @@ export default async function NewsTicker() { mediaRes.documents.forEach((m: any) => mediaMap.set(m.$id, m)); } - newsItems = postsRes.documents.map((post: any) => { + postsRes.documents.forEach((post: any) => { const media = mediaMap.get(post.imagen_destacada_id); let imagenUrl = post.og_image_url || null; if (media) { - imagenUrl = `${process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'}/storage/buckets/${media.bucket_id}/files/${media.file_id}/preview?project=${process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'}`; + imagenUrl = `${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${media.bucket_id}/files/${media.file_id}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}`; } - return { + tickerItems.push({ + type: 'news', id: post.$id, titulo: post.titulo, slug: post.slug || post.$id, imagenUrl - }; + }); }); } - noticias = newsItems; // Assign resolved news items to 'noticias' + + // 3. Fetch Approved WhatsApp Messages + try { + const waRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'whatsapp_messages', [ + Query.equal('status', 'approved'), + Query.orderDesc('timestamp'), + Query.limit(5) + ]); + + waRes.documents.forEach((msg: any) => { + tickerItems.push({ + type: 'whatsapp', + id: msg.$id, + titulo: `${msg.sender_name}: ${msg.content}`, + slug: null, + imagenUrl: msg.avatar || null + }); + }); + } catch (waError) { + console.warn("Skipping WhatsApp messages in ticker:", waError); + } + } catch (e) { console.error("Ticker fetch error", e); } - if (noticias.length === 0) return null; - - const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; - const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; + if (tickerItems.length === 0) return null; return ( -
-
-
+
+
+
-
- {/* Doble render para bucle infinito visual de CSS Marquee */} - {[...noticias, ...noticias].map((noticia, idx) => { - return ( - - {noticia.imagenUrl && ( - // eslint-disable-next-line @next/next/no-img-element - {noticia.titulo} +
+ {/* Doble render para bucle infinito visual */} + {[...tickerItems, ...tickerItems].map((item, idx) => { + const isWA = item.type === 'whatsapp'; + + const Content = ( +
+ {isWA ? ( +
+ +
+ ) : ( + item.imagenUrl && ( + // eslint-disable-next-line @next/next/no-img-element + {item.titulo} + ) )} - - {!noticia.imagenUrl && } - {noticia.titulo.length > 55 ? noticia.titulo.substring(0, 55) + '...' : noticia.titulo} + + + {isWA && COMUNIDAD} + {item.titulo.length > 80 ? item.titulo.substring(0, 80) + '...' : item.titulo} - - ) +
+ ); + + if (item.slug) { + return {Content}; + } + return
{Content}
; })}
@@ -109,7 +134,10 @@ export default async function NewsTicker() { 100% { transform: translateX(-50%); } } .animate-marquee { - animation: marquee 30s linear infinite; + animation: marquee 40s linear infinite; + } + .animate-marquee:hover { + animation-play-state: paused; } `}} />
diff --git a/src/components/dashboard/ColumnFilter.tsx b/src/components/dashboard/ColumnFilter.tsx new file mode 100644 index 0000000..367dc97 --- /dev/null +++ b/src/components/dashboard/ColumnFilter.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { Settings2, Check } from 'lucide-react'; +import { useState, useRef, useEffect } from 'react'; +import { FieldDefinition } from '@/lib/schema'; + +interface ColumnFilterProps { + fields: FieldDefinition[]; + visibleColumns: string[]; + onChange: (columns: string[]) => void; +} + +export const ColumnFilter = ({ fields, visibleColumns, onChange }: ColumnFilterProps) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const toggleColumn = (name: string) => { + if (visibleColumns.includes(name)) { + if (visibleColumns.length > 1) { // Prevents hiding all columns + onChange(visibleColumns.filter(c => c !== name)); + } + } else { + onChange([...visibleColumns, name]); + } + }; + + return ( +
+ + + {isOpen && ( +
+
+ Mostrar / Ocultar + {visibleColumns.length} visibles +
+
+ {fields.map((field) => ( + + ))} +
+
+ + +
+
+ )} +
+ ); +}; diff --git a/src/components/dashboard/DataTable.tsx b/src/components/dashboard/DataTable.tsx new file mode 100644 index 0000000..2ba1c21 --- /dev/null +++ b/src/components/dashboard/DataTable.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Pencil, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react'; +import Link from 'next/link'; +import { DeleteButton } from '@/components/DeleteButton'; +import { ColumnFilter } from './ColumnFilter'; +import { FieldDefinition } from '@/lib/schema'; +import { updateUserPreferencesAction } from '@/app/actions/user'; + +interface DataTableProps { + entity: string; + collectionId: string; + documents: any[]; + fieldsConfig: FieldDefinition[]; + initialHiddenColumns: string[]; + currentSortField: string; + currentSortDir: 'asc' | 'desc'; +} + +export const DataTable = ({ + entity, + collectionId, + documents, + fieldsConfig, + initialHiddenColumns, + currentSortField, + currentSortDir +}: DataTableProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const [isPending, startTransition] = useTransition(); + const [visibleColumns, setVisibleColumns] = useState(() => { + const allFields = fieldsConfig.map(f => f.name); + return allFields.filter(f => !initialHiddenColumns.includes(f)); + }); + + const handleColumnChange = async (newVisible: string[]) => { + setVisibleColumns(newVisible); + + // Calculate hidden columns to persist + const allFields = fieldsConfig.map(f => f.name); + const hidden = allFields.filter(f => !newVisible.includes(f)); + + // Persist to Appwrite Prefs + await updateUserPreferencesAction({ + hiddenColumns: { + [entity]: hidden + } + }); + }; + + const toggleSort = (field: string) => { + const params = new URLSearchParams(searchParams.toString()); + let newDir: 'asc' | 'desc' = 'desc'; + + if (currentSortField === field) { + newDir = currentSortDir === 'desc' ? 'asc' : 'desc'; + } + + params.set('sort', field); + params.set('dir', newDir); + + startTransition(() => { + router.push(`?${params.toString()}`); + }); + }; + + const getHeaderLabel = (key: string) => { + const field = fieldsConfig.find(f => f.name === key); + return field ? field.label : key.replace(/_/g, ' '); + }; + + const renderSortIcon = (field: string) => { + if (currentSortField !== field) return ; + return currentSortDir === 'asc' ? : ; + }; + + return ( +
+
+ +
+ +
+ {isPending && ( +
+
+
+ )} + +
+ + + + {fieldsConfig.map(field => { + if (!visibleColumns.includes(field.name)) return null; + return ( + + ); + })} + + + + + {documents.map((doc) => ( + + {fieldsConfig.map(field => { + if (!visibleColumns.includes(field.name)) return null; + + let value = doc[field.name]; + + // Resolve Relations + if (typeof value === 'object' && value !== null) { + if (field.type === 'relation' && field.relationLabel) { + value = value[field.relationLabel] || value.$id || '[Vinculado]'; + } else { + value = Array.isArray(value) ? `[Lista (${value.length})]` : value.$id || '[Vinculado]'; + } + } + + if (typeof value === 'boolean') { + value = value ? 'Sí' : 'No'; + } + if (typeof value === 'string' && value.length > 60) { + value = value.substring(0, 60) + '...'; + } + + return ( + + ) + })} + + + ))} + +
toggleSort(field.name)} + > +
+ {field.label} + {renderSortIcon(field.name)} +
+
Acciones
+ {String(value || '')} + + + + + +
+
+
+
+ ); +}; diff --git a/src/components/dashboard/EntityForm.tsx b/src/components/dashboard/EntityForm.tsx new file mode 100644 index 0000000..9f49076 --- /dev/null +++ b/src/components/dashboard/EntityForm.tsx @@ -0,0 +1,343 @@ +'use client'; + +import { useState, useEffect, useTransition } from 'react'; +import { useRouter } from 'next/navigation'; +import { ArrowLeft, Save, Globe, Calendar, Tag, Image as ImageIcon, Eye, Code } from 'lucide-react'; +import Link from 'next/link'; +import { FieldDefinition } from '@/lib/schema'; +import { MediaPickerMini } from './MediaPickerMini'; +import { RichTextEditor } from './RichTextEditor'; + +// Minimalistic image preview for relations with 'media' +const MediaPreview = ({ fileId, bucketId, endpoint, projectId }: any) => { + if(!fileId) return null; + const url = `${endpoint}/storage/buckets/${bucketId}/files/${fileId}/view?project=${projectId}`; + return ( +
+ // eslint-disable-next-line @next/next/no-img-element + Preview +
+ Imagen Seleccionada +
+
+ ); +} + +interface EntityFormProps { + entity: string; + entityName: string; + initialData?: any; + fields: FieldDefinition[]; + relationData: Record; + submitAction: (formData: FormData) => Promise; + appwriteConfig: { + endpoint: string; + projectId: string; + } +} + +export const EntityForm = ({ + entity, + entityName, + initialData, + fields, + relationData, + submitAction, + appwriteConfig +}: EntityFormProps) => { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [title, setTitle] = useState(initialData?.titulo || initialData?.nombre || ''); + const [slug, setSlug] = useState(initialData?.slug || ''); + const [isAutoSlug, setIsAutoSlug] = useState(!initialData?.slug); + const [featuredImageId, setFeaturedImageId] = useState(initialData?.imagen_destacada_id || ''); + const [editorValues, setEditorValues] = useState>({}); + + // Initialize editor values + useEffect(() => { + const initialValues: Record = {}; + fields.forEach(f => { + if (f.type === 'textarea') { + initialValues[f.name] = initialData?.[f.name] || ''; + } + }); + setEditorValues(initialValues); + }, [fields, initialData]); + + // Update slug automatically if auto-slug is enabled + useEffect(() => { + if (isAutoSlug) { + const generated = title + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + setSlug(generated); + } + }, [title, isAutoSlug]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + // Add editor values to formData + for (const [key, value] of Object.entries(editorValues)) { + formData.set(key, value); + } + + // Ensure featured image ID is set if present + if (featuredImageId) { + formData.set('imagen_destacada_id', featuredImageId); + } + + startTransition(async () => { + await submitAction(formData); + }); + }; + + // Organize fields into Main Content and Sidebar Metadata + const sidebarFieldNames = ['estado', 'fecha_publicacion', 'fecha_finalizacion', 'slug', 'imagen_destacada_id', 'categoria_id', 'tipo_publicacion_id']; + const mainFields = fields.filter(f => !sidebarFieldNames.includes(f.name)); + const sidebarFields = fields.filter(f => sidebarFieldNames.includes(f.name)); + + const getDefValue = (field: FieldDefinition) => { + const val = initialData ? initialData[field.name] : null; + + // Set smart defaults for new items + if (!initialData) { + if (field.name === 'estado') return 'borrador'; + if (field.name === 'fecha_publicacion' || field.type === 'date') { + // Get local ISO string for datetime-local input + const now = new Date(); + return new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16); + } + if (field.type === 'boolean') return 'false'; + } + + if (val === undefined || val === null) return ''; + if (field.type === 'boolean') return val ? 'true' : 'false'; + if (field.type === 'date') return new Date(val).toISOString().slice(0, 16); + if (field.type === 'relation') return typeof val === 'object' ? val.$id : val; + return val; + }; + + return ( +
+ {/* Header Sticky */} +
+
+ + + +
+

+ {initialData ? 'Editando' : 'Nuevo'}: {entityName} +

+ {initialData &&

{initialData.$id}

} +
+
+ +
+ + +
+
+ +
+ {/* Main Column */} +
+
+
+ + {mainFields.map(field => { + const defaultValue = getDefValue(field); + + if (field.name === 'titulo' || field.name === 'nombre_entidad' || field.name === 'numero_contrato') { + return ( +
+ + (field.name === 'titulo' || field.name === 'nombre_entidad') && setTitle(e.target.value)} + placeholder={`Ingresa el ${field.label.toLowerCase()}...`} + className="w-full bg-transparent border-b-2 border-slate-800 py-4 text-2xl md:text-3xl font-bold text-white focus:outline-none focus:border-cyan-500 transition-all placeholder:text-slate-700" + /> +
+ ); + } + + if (field.type === 'textarea') { + return ( + setEditorValues(prev => ({ ...prev, [field.name]: val }))} + /> + ); + } + + return ( +
+ + + {field.type === 'text' && ( + + )} + + {field.type === 'number' && ( + + )} + + {field.type === 'url' && ( + + )} + + {field.type === 'boolean' && ( + + )} + + {field.type === 'select' && field.options && ( + + )} + + {field.type === 'relation' && ( + + )} +
+ ); + })} +
+
+ + {/* Sidebar Column */} +
+
+

+ Configuración de Publicación +

+ + {sidebarFields.map(field => { + const defaultValue = getDefValue(field); + + return ( +
+
+ + {field.name === 'slug' && ( + + )} +
+ + {field.name === 'slug' ? ( + { setIsAutoSlug(false); setSlug(e.target.value); }} + required={field.required} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm text-cyan-100 font-mono focus:outline-none focus:border-cyan-500 transition-all font-bold" + /> + ) : field.name === 'imagen_destacada_id' ? ( + + ) : field.type === 'date' ? ( + + ) : field.type === 'select' ? ( + + ) : field.type === 'relation' ? ( + <> + + {field.relationCollection === 'media' && ( + + )} + + ) : ( + + )} +
+ ); + })} +
+ +
+
+
+ Estado del Sistema +
+

+ “Recuerda verificar el slug antes de publicar. Los cambios en el estado afectarán inmediatamente la visibilidad en el portal de noticias.” +

+
+
+
+
+ ); +}; diff --git a/src/components/dashboard/MediaPickerMini.tsx b/src/components/dashboard/MediaPickerMini.tsx new file mode 100644 index 0000000..afa29e2 --- /dev/null +++ b/src/components/dashboard/MediaPickerMini.tsx @@ -0,0 +1,205 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ImageIcon, X, Search, CheckCircle2 } from 'lucide-react'; +import Link from 'next/link'; +import { databases, APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite'; +import { Query } from 'appwrite'; + +interface MediaPickerMiniProps { + value: string; + onChange: (fileId: string) => void; + label: string; +} + +export const MediaPickerMini = ({ value, onChange, label }: MediaPickerMiniProps) => { + const [isOpen, setIsOpen] = useState(false); + const [mediaItems, setMediaItems] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (isOpen) { + fetchMedia(); + } + }, [isOpen]); + + const fetchMedia = async () => { + setIsLoading(true); + try { + // We use the client-side databases instance for browsing media + const res = await databases.listDocuments( + APPWRITE_ALMEIRA_CONFIG.databaseId, + 'media', + [ + Query.limit(100), + Query.orderDesc('$createdAt') + ] + ); + setMediaItems(res.documents); + } catch (error) { + console.error('Error fetching media:', error); + } finally { + setIsLoading(false); + } + }; + + const getImageUrl = (fileId: string) => { + return `${endpoint}/storage/buckets/media/files/${fileId}/view?project=${projectId}&width=200&height=200&gravity=center`; + }; + + const filteredItems = mediaItems.filter(item => + item.titulo.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( +
+
+ + {value && ( + + )} +
+ +
setIsOpen(true)} + className={`relative aspect-video rounded-2xl border-2 border-dashed transition-all cursor-pointer flex flex-col items-center justify-center gap-2 overflow-hidden bg-slate-950 group ${ + value ? 'border-cyan-500/50 hover:border-cyan-400' : 'border-slate-800 hover:border-slate-700' + }`} + > + {value ? ( + <> + Selected +
+ + Cambiar Imagen + +
+ + ) : ( + <> +
+ +
+ Seleccionar Medio + + )} +
+ + {/* Hidden input to carry the value in the form */} + + + {/* Modal */} + {isOpen && ( +
+
setIsOpen(false)}>
+ +
+ {/* Modal Header */} +
+
+
+ +
+
+

Biblioteca de Medios

+

Selecciona una imagen para esta publicación

+
+
+ +
+ + {/* Modal Search */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-2xl pl-12 pr-6 py-4 text-white focus:outline-none focus:border-cyan-500 transition-all font-medium" + /> +
+
+ + {/* Modal Content */} +
+ {isLoading ? ( +
+
+ Cargando Galería... +
+ ) : filteredItems.length > 0 ? ( +
+ {filteredItems.map((item) => ( +
{ + onChange(item.file_id); + setIsOpen(false); + }} + className={`group relative aspect-square rounded-2xl border-2 transition-all cursor-pointer overflow-hidden bg-slate-950 ${ + value === item.file_id ? 'border-cyan-500 shadow-xl shadow-cyan-500/20' : 'border-slate-800 hover:border-slate-600 shadow-lg' + }`} + > + {item.titulo} +
+ {item.titulo} +
+ {value === item.file_id && ( +
+ +
+ )} +
+ ))} +
+ ) : ( +
+

Sin Resultados

+

No encontramos imágenes con ese título.

+
+ )} +
+ + {/* Modal Footer */} +
+ + {filteredItems.length} Elementos Disponibles + + + Subir Nuevo Archivo + +
+
+
+ )} +
+ ); +}; diff --git a/src/components/dashboard/RichTextEditor.tsx b/src/components/dashboard/RichTextEditor.tsx new file mode 100644 index 0000000..421ec1a --- /dev/null +++ b/src/components/dashboard/RichTextEditor.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Underline from '@tiptap/extension-underline'; +import Link from '@tiptap/extension-link'; +import { TextStyle } from '@tiptap/extension-text-style'; +import { Color } from '@tiptap/extension-color'; +import TextAlign from '@tiptap/extension-text-align'; +import Highlight from '@tiptap/extension-highlight'; +import { + Bold, Italic, Underline as UnderlineIcon, + Link as LinkIcon, AlignLeft, AlignCenter, + AlignRight, Highlighter, Type, + List, ListOrdered, Quote, Undo, Redo +} from 'lucide-react'; +import { useEffect } from 'react'; + +interface RichTextEditorProps { + value: string; + onChange: (value: string) => void; + label: string; +} + +const MenuBar = ({ editor }: { editor: any }) => { + if (!editor) return null; + + const setLink = () => { + const url = window.prompt('URL del enlace:'); + if (url) { + editor.chain().focus().setLink({ href: url }).run(); + } + }; + + return ( +
+ + + + +
+ + + + + +
+ + + + +
+ + + + +
+ + + +
+ ); +}; + +export const RichTextEditor = ({ value, onChange, label }: RichTextEditorProps) => { + const editor = useEditor({ + extensions: [ + StarterKit, + Underline, + Link.configure({ + openOnClick: false, + HTMLAttributes: { + class: 'text-cyan-400 underline decoration-cyan-400/50 hover:text-cyan-300 transition-colors', + }, + }), + TextStyle, + Color, + TextAlign.configure({ + types: ['heading', 'paragraph'], + }), + Highlight.configure({ multicolor: true }), + ], + content: value, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); + }, + editorProps: { + attributes: { + class: 'prose prose-invert max-w-none focus:outline-none min-h-[300px] p-6 text-slate-200 leading-relaxed', + }, + }, + }); + + // Keep content synced if value changes externally + useEffect(() => { + if (editor && value !== editor.getHTML()) { + editor.commands.setContent(value); + } + }, [value, editor]); + + return ( +
+ +
+ + +
+ {/* Hidden input for form submission if needed, though we use controlled value */} + +
+ ); +}; diff --git a/src/components/media/GlobalPlayer.tsx b/src/components/media/GlobalPlayer.tsx new file mode 100644 index 0000000..2b82356 --- /dev/null +++ b/src/components/media/GlobalPlayer.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { usePlayerStore } from '@/store/usePlayerStore'; +import { Play, Pause, Volume2, VolumeX, Radio, Music, Maximize2, Minimize2, Video, X } from 'lucide-react'; +import { StreamOverlays } from './StreamOverlays'; +import videojs from 'video.js'; +import 'video.js/dist/video-js.css'; + +export const GlobalPlayer = () => { + const { + audioUrl, isAudioPlaying, audioVolume, toggleAudio, setAudioVolume, + isVideoActive, videoUrl, setVideoActive + } = usePlayerStore(); + + const audioRef = useRef(null); + const videoRef = useRef(null); + const playerRef = useRef(null); + const [isMuted, setIsMuted] = useState(false); + const [lastVolume, setLastVolume] = useState(audioVolume); + + // Sync Audio State + useEffect(() => { + if (!audioRef.current) return; + + if (isAudioPlaying) { + audioRef.current.play().catch(e => { + console.error("Audio playback error:", e); + toggleAudio(false); + }); + } else { + audioRef.current.pause(); + } + }, [isAudioPlaying, audioUrl, toggleAudio]); + + // Handle Video Player Initialization + useEffect(() => { + if (isVideoActive && videoRef.current && !playerRef.current) { + const videoElement = document.createElement('video-js'); + videoElement.classList.add('vjs-big-play-centered', 'vjs-almeira'); + videoRef.current.appendChild(videoElement); + + const player = playerRef.current = videojs(videoElement, { + autoplay: true, + controls: true, + responsive: true, + fluid: true, + sources: [{ + src: videoUrl || 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8', // Fallback para test + type: 'application/x-mpegURL' + }] + }); + + // Cuando el video está activo, bajamos el volumen de la radio + if (isAudioPlaying) setAudioVolume(0.1); + } + + return () => { + if (playerRef.current) { + playerRef.current.dispose(); + playerRef.current = null; + } + }; + }, [isVideoActive, videoUrl]); + + // Sync Volume + useEffect(() => { + if (audioRef.current) { + audioRef.current.volume = isMuted ? 0 : audioVolume; + } + }, [audioVolume, isMuted]); + + const handleToggleMute = () => { + if (isMuted) { + setIsMuted(false); + setAudioVolume(lastVolume); + } else { + setLastVolume(audioVolume); + setIsMuted(true); + setAudioVolume(0); + } + }; + + return ( + <> + {/* Hidden Audio Element */} +