* add editor

* implement pt1

* Add Lexical plugins and update editor functionality in ProfessionalBlogPostForm

* Comment out client tag in usePublishBlogPost hook

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
mroxso
2025-10-05 19:44:33 +02:00
committed by GitHub
parent bc1494374f
commit c6f8296cbe
16 changed files with 2172 additions and 42 deletions

View File

@@ -10,11 +10,15 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json"
}
}
}

764
package-lock.json generated
View File

@@ -11,6 +11,13 @@
"@fontsource-variable/inter": "^5.2.6",
"@getalby/sdk": "^5.1.1",
"@hookform/resolvers": "^3.9.0",
"@lexical/code": "^0.36.2",
"@lexical/link": "^0.36.2",
"@lexical/list": "^0.36.2",
"@lexical/markdown": "^0.36.2",
"@lexical/react": "^0.36.2",
"@lexical/rich-text": "^0.36.2",
"@lexical/selection": "^0.36.2",
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
"@radix-ui/react-accordion": "^1.2.0",
@@ -33,13 +40,13 @@
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.56.2",
"@unhead/addons": "^2.0.10",
"@unhead/react": "^2.0.10",
@@ -49,6 +56,7 @@
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lexical": "^0.36.2",
"lucide-react": "^0.462.0",
"nostr-tools": "^2.13.0",
"qrcode": "^1.5.4",
@@ -909,31 +917,46 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz",
"integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.0",
"@floating-ui/utils": "^0.2.9"
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.16",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.6",
"@floating-ui/utils": "^0.2.10",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
"@floating-ui/dom": "^1.7.4"
},
"peerDependencies": {
"react": ">=16.8.0",
@@ -941,9 +964,9 @@
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@fontsource-variable/inter": {
@@ -1201,6 +1224,281 @@
"resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/0.224.3.tgz",
"integrity": "sha512-zAuX2QV1zwJ5RSmrnDGVerAtN3pBXpYYNlGzhERW9AiQ1UJd2/xruyB3i5NdTWy2OK2pjETswOj+0+prYTPlxQ=="
},
"node_modules/@lexical/clipboard": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.36.2.tgz",
"integrity": "sha512-l7z52jltlMz1HmJRmG7ZdxySPjheRRxdV/75QEnzalMtqfLPgh4G5IpycISjbX+95PgEaC6rXbcjPix0CyHDJg==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.36.2",
"@lexical/list": "0.36.2",
"@lexical/selection": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/code": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.36.2.tgz",
"integrity": "sha512-dfS62rNo3uKwNAJQ39zC+8gYX0k8UAoW7u+JPIqx+K2VPukZlvpsPLNGft15pdWBkHc7Pv+o9gJlB6gGv+EBfA==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.36.2",
"lexical": "0.36.2",
"prismjs": "^1.30.0"
}
},
"node_modules/@lexical/devtools-core": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.36.2.tgz",
"integrity": "sha512-G+XW7gR/SCx3YgX4FK9wAIn6AIOkC+j8zRPWrS3GQNZ15CE0QkwQl3IyQ7XW9KzWmdRMs6yTmTVnENFa1JLzXg==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.36.2",
"@lexical/link": "0.36.2",
"@lexical/mark": "0.36.2",
"@lexical/table": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/dragon": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.36.2.tgz",
"integrity": "sha512-VWNjYaH74uQ8MFKkl80pTofojpEnTYSX2sgHyZmo1Lk1cKLHK25pMnWgAxPAMLQD5/RW/2PtZcK+j0Kfoe5lSQ==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/extension": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.36.2.tgz",
"integrity": "sha512-NWxtqMFMzScq4Eemqp1ST2KREIfj57fUbn7qHv+mMnYgQZK4iIhrHKo5klonxi1oBURcxUZMIbdtH7MJ4BdisA==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.36.2",
"@preact/signals-core": "^1.11.0",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/hashtag": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.36.2.tgz",
"integrity": "sha512-WdmKtzXFcahQT3ShFDeHF6LCR5C8yvFCj3ImI09rZwICrYeonbMrzsBUxS1joBz0HQ+ufF9Tx+RxLvGWx6WxzQ==",
"license": "MIT",
"dependencies": {
"@lexical/text": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/history": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.36.2.tgz",
"integrity": "sha512-pnS36gyMWz1yq/3Z2jv0gUxjJfas5j0GZOM4rFTzDAHjRVc5q3Ua4ElwekdcLaPPGpUlcg3jghIGWa2pSeoPvA==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/html": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.36.2.tgz",
"integrity": "sha512-fgqALzgKnoy93G0yFyYD4C4qJTSMZyUt4JE5kj/POFwWNOnXThIqJhQGwBvH/ibImpIfOeds2TrSr8PbStlrNg==",
"license": "MIT",
"dependencies": {
"@lexical/selection": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/link": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.36.2.tgz",
"integrity": "sha512-Zb+DeHA1po8VMiOAAXsBmAHhfWmQttsUkI5oiZUmOXJruRuQ2rVr01NoxHpoEpLwHOABVNzD3PMbwov+g3c7lg==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/list": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.36.2.tgz",
"integrity": "sha512-JpaIaE0lgNUrAR7iaCaIoETcCKG9EvZjM3G71VxiexTs7PltmEMq36LUlO2goafWurP7knG2rUpVnTcuSbYYeA==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.36.2",
"@lexical/selection": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/mark": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.36.2.tgz",
"integrity": "sha512-n0MNXtGH+1i43hglgHjpQV0093HmIiFR7Budg2BJb8ZNzO1KZRqeXAHlA5ZzJ698FkAnS4R5bqG9tZ0JJHgAuA==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/markdown": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.36.2.tgz",
"integrity": "sha512-jI4McaVKUo8ADOYNCB5LnYyxXDyOWBOofM05r42R9QIMyUxGryo43WNPMAYXzCgtHlkQv+FNles9OlQY0IlAag==",
"license": "MIT",
"dependencies": {
"@lexical/code": "0.36.2",
"@lexical/link": "0.36.2",
"@lexical/list": "0.36.2",
"@lexical/rich-text": "0.36.2",
"@lexical/text": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/offset": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.36.2.tgz",
"integrity": "sha512-+QQNwzFW/joes3DhNINpGdEX6O5scUTs4n8pYDyM/3pWb+8oCHRaRtEmpUU9HStbdy/pK2kQ9XdztkrNvP/ilA==",
"license": "MIT",
"dependencies": {
"lexical": "0.36.2"
}
},
"node_modules/@lexical/overflow": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.36.2.tgz",
"integrity": "sha512-bLaEe93iZIJH5wDh6e/DTZVNz7xO7lMS5akcJW8CIwopr4I/Qv2uCvc4G1bMMHx2xM1gVxstn5rFgIUP8/Gqlg==",
"license": "MIT",
"dependencies": {
"lexical": "0.36.2"
}
},
"node_modules/@lexical/plain-text": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.36.2.tgz",
"integrity": "sha512-c9F/+WHl2QuXVhu+1bBVo6BIrSjCcixLe5ePKxoUpy+B7W72s3VCoAQZp+pmtPIyodDLmZAx78hZBBlzoIOeeg==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.36.2",
"@lexical/dragon": "0.36.2",
"@lexical/selection": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/react": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.36.2.tgz",
"integrity": "sha512-mPVm1BmeuMsMpVyUplgc0btOI8+Vm9bZj4AftgfMSkvzkr8i6NkLn8LV5IlEnoRvxXkjOExwlwBwdQte5ZGvNw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.16",
"@lexical/devtools-core": "0.36.2",
"@lexical/dragon": "0.36.2",
"@lexical/extension": "0.36.2",
"@lexical/hashtag": "0.36.2",
"@lexical/history": "0.36.2",
"@lexical/link": "0.36.2",
"@lexical/list": "0.36.2",
"@lexical/mark": "0.36.2",
"@lexical/markdown": "0.36.2",
"@lexical/overflow": "0.36.2",
"@lexical/plain-text": "0.36.2",
"@lexical/rich-text": "0.36.2",
"@lexical/table": "0.36.2",
"@lexical/text": "0.36.2",
"@lexical/utils": "0.36.2",
"@lexical/yjs": "0.36.2",
"lexical": "0.36.2",
"react-error-boundary": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/rich-text": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.36.2.tgz",
"integrity": "sha512-dZ7zAIv5NBrh1ApxIT9bayn96zfQHHdnT+oaqmR+q100Vo2uROeR/ZF5igeAuwYGM1Z3ZWDBvNxRKd1d6FWiZw==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.36.2",
"@lexical/dragon": "0.36.2",
"@lexical/selection": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/selection": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.36.2.tgz",
"integrity": "sha512-n96joW3HCKBmPeESR172BxVE+m8V9SdidQm4kKb9jOZ1Ota+tnam2386TeI6795TWwgjDQJPK3HZNKcX6Gb+Bg==",
"license": "MIT",
"dependencies": {
"lexical": "0.36.2"
}
},
"node_modules/@lexical/table": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.36.2.tgz",
"integrity": "sha512-96rNNPiVbC65i+Jn1QzIsehCS7UVUc69ovrh9Bt4+pXDebZSdZai153Q7RUq8q3AQ5ocK4/SA2kLQfMu0grj3Q==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.36.2",
"@lexical/extension": "0.36.2",
"@lexical/utils": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/text": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.36.2.tgz",
"integrity": "sha512-IbbqgRdMAD6Uk9b2+qSVoy+8RVcczrz6OgXvg39+EYD+XEC7Rbw7kDTWzuNSJJpP7vxSO8YDZSaIlP5gNH3qKA==",
"license": "MIT",
"dependencies": {
"lexical": "0.36.2"
}
},
"node_modules/@lexical/utils": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.36.2.tgz",
"integrity": "sha512-P9+t2Ob10YNGYT/PWEER+1EqH8SAjCNRn+7SBvKbr0IdleGF2JvzbJwAWaRwZs1c18P11XdQZ779dGvWlfwBIw==",
"license": "MIT",
"dependencies": {
"@lexical/list": "0.36.2",
"@lexical/selection": "0.36.2",
"@lexical/table": "0.36.2",
"lexical": "0.36.2"
}
},
"node_modules/@lexical/yjs": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.36.2.tgz",
"integrity": "sha512-gZ66Mw+uKXTO8KeX/hNKAinXbFg3gnNYraG76lBXCwb/Ka3q34upIY9FUeGOwGVaau3iIDQhE49I+6MugAX2FQ==",
"license": "MIT",
"dependencies": {
"@lexical/offset": "0.36.2",
"@lexical/selection": "0.36.2",
"lexical": "0.36.2"
},
"peerDependencies": {
"yjs": ">=13.5.22"
}
},
"node_modules/@noble/ciphers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
@@ -1317,6 +1615,16 @@
"node": ">=14"
}
},
"node_modules/@preact/signals-core": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.12.1.tgz",
"integrity": "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -1388,6 +1696,24 @@
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz",
@@ -1547,6 +1873,24 @@
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -1641,6 +1985,24 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@@ -1864,6 +2226,24 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menubar": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.12.tgz",
@@ -1969,6 +2349,24 @@
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz",
@@ -2072,6 +2470,24 @@
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.4.tgz",
@@ -2233,6 +2649,24 @@
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz",
@@ -2290,9 +2724,9 @@
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
@@ -2455,23 +2889,205 @@
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.4.tgz",
"integrity": "sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==",
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.7",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.4",
"@radix-ui/react-portal": "1.1.6",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-slot": "1.2.0",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.0"
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -5907,6 +6523,17 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@@ -6047,6 +6674,34 @@
"node": ">= 0.8.0"
}
},
"node_modules/lexical": {
"version": "0.36.2",
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.36.2.tgz",
"integrity": "sha512-gIDJCmSAhtxD7h95WK17Nz19wCZu92Zn0p1/R45X01S/KAsLCwEtVJ2fTvIJNFTyx3QNJTuGcm5mYgRMUwq8rg==",
"license": "MIT"
},
"node_modules/lib0": {
"version": "0.2.114",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -7674,6 +8329,15 @@
"license": "MIT",
"peer": true
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7787,6 +8451,18 @@
"react": "^18.3.1"
}
},
"node_modules/react-error-boundary": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
"integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-hook-form": {
"version": "7.56.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz",
@@ -8580,6 +9256,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
@@ -9835,6 +10517,24 @@
"node": ">=8"
}
},
"node_modules/yjs": {
"version": "13.6.27",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
"integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
"license": "MIT",
"peer": true,
"dependencies": {
"lib0": "^0.2.99"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -13,6 +13,13 @@
"@fontsource-variable/inter": "^5.2.6",
"@getalby/sdk": "^5.1.1",
"@hookform/resolvers": "^3.9.0",
"@lexical/code": "^0.36.2",
"@lexical/link": "^0.36.2",
"@lexical/list": "^0.36.2",
"@lexical/markdown": "^0.36.2",
"@lexical/react": "^0.36.2",
"@lexical/rich-text": "^0.36.2",
"@lexical/selection": "^0.36.2",
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
"@radix-ui/react-accordion": "^1.2.0",
@@ -35,13 +42,13 @@
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.56.2",
"@unhead/addons": "^2.0.10",
"@unhead/react": "^2.0.10",
@@ -51,6 +58,7 @@
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lexical": "^0.36.2",
"lucide-react": "^0.462.0",
"nostr-tools": "^2.13.0",
"qrcode": "^1.5.4",

View File

@@ -0,0 +1,485 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { SerializedEditorState } from 'lexical';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { usePublishBlogPost } from '@/hooks/usePublishBlogPost';
import { useBlogPost } from '@/hooks/useBlogPost';
import { useUploadFile } from '@/hooks/useUploadFile';
import { useIsMobile } from '@/hooks/useIsMobile';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle, Loader2, Upload, Image as ImageIcon, FileText, Hash, Calendar } from 'lucide-react';
import { Editor } from '@/components/blocks/editor-00/editor';
interface ProfessionalBlogPostFormProps {
/** Existing post identifier for editing (optional) */
editIdentifier?: string;
}
const initialEditorState = {
root: {
children: [
{
children: [],
direction: "ltr",
format: "",
indent: 0,
type: "paragraph",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
type: "root",
version: 1,
},
} as unknown as SerializedEditorState;
export function ProfessionalBlogPostForm({ editIdentifier }: ProfessionalBlogPostFormProps) {
const { user } = useCurrentUser();
const navigate = useNavigate();
const isMobile = useIsMobile();
const { mutateAsync: publishPost, isPending: isPublishing } = usePublishBlogPost();
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
// Load existing post if editing (using the current user's pubkey)
const { data: existingPost, isLoading: isLoadingPost } = useBlogPost(
user?.pubkey || '',
editIdentifier || ''
);
const [editorState, setEditorState] = useState<SerializedEditorState>(initialEditorState);
const [metadata, setMetadata] = useState({
identifier: '',
title: '',
summary: '',
image: '',
hashtags: '',
});
const [showMetadata, setShowMetadata] = useState(true);
// Load existing post data when editing
useEffect(() => {
if (existingPost && editIdentifier) {
const d = existingPost.tags.find(([name]) => name === 'd')?.[1] || '';
const title = existingPost.tags.find(([name]) => name === 'title')?.[1] || '';
const summary = existingPost.tags.find(([name]) => name === 'summary')?.[1] || '';
const image = existingPost.tags.find(([name]) => name === 'image')?.[1] || '';
const hashtags = existingPost.tags
.filter(([name]) => name === 't')
.map(([, value]) => value)
.join(', ');
setMetadata({
identifier: d,
title,
summary,
image,
hashtags,
});
// Convert markdown content to editor state
// We'll use a simple approach - the editor will handle the markdown
// For now, we'll just set it as the initial state
if (existingPost.content) {
try {
// Create a simple editor state with the markdown content as text
// The Lexical markdown plugin should handle conversion
const contentState = {
root: {
children: [
{
children: [
{
detail: 0,
format: 0,
mode: "normal",
style: "",
text: existingPost.content,
type: "text",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
type: "paragraph",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
type: "root",
version: 1,
},
} as unknown as SerializedEditorState;
setEditorState(contentState);
} catch (error) {
console.error('Failed to parse existing content:', error);
}
}
}
}, [existingPost, editIdentifier]);
const handleMetadataChange = (field: keyof typeof metadata, value: string) => {
setMetadata(prev => ({ ...prev, [field]: value }));
};
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const [[_, url]] = await uploadFile(file);
setMetadata(prev => ({ ...prev, image: url }));
} catch (error) {
console.error('Failed to upload image:', error);
}
};
const getMarkdownFromEditor = (): string => {
// Extract text content from the editor state
// In a full implementation, you'd use $convertToMarkdownString with proper transformers
try {
interface EditorNode {
children?: Array<{ text?: string }>;
}
const root = editorState.root as { children?: EditorNode[] };
const content = (root.children || [])
.map((child: EditorNode) => {
if (child.children && Array.isArray(child.children)) {
return child.children
.map((textNode) => textNode.text || '')
.join('');
}
return '';
})
.join('\n\n');
return content;
} catch (error) {
console.error('Failed to extract markdown:', error);
return '';
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!metadata.identifier.trim()) {
alert('Please provide a unique identifier for your post');
return;
}
if (!metadata.title.trim()) {
alert('Please provide a title for your post');
return;
}
const markdownContent = getMarkdownFromEditor();
if (!markdownContent.trim()) {
alert('Please write some content for your post');
return;
}
try {
const publishedAt = editIdentifier && existingPost
? parseInt(existingPost.tags.find(([name]) => name === 'published_at')?.[1] || '0')
: Math.floor(Date.now() / 1000);
const event = await publishPost({
identifier: metadata.identifier,
title: metadata.title,
summary: metadata.summary || undefined,
image: metadata.image || undefined,
content: markdownContent,
hashtags: metadata.hashtags
? metadata.hashtags.split(',').map(t => t.trim()).filter(Boolean)
: undefined,
publishedAt: publishedAt || undefined,
});
// Navigate to the post
const naddr = nip19.naddrEncode({
kind: 30023,
pubkey: event.pubkey,
identifier: metadata.identifier,
});
navigate(`/${naddr}`);
} catch (error) {
console.error('Failed to publish post:', error);
alert('Failed to publish post. Please try again.');
}
};
if (!user) {
return (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
You must be logged in to create a blog post.
</AlertDescription>
</Alert>
);
}
// Check if user is trying to edit someone else's post
if (editIdentifier && existingPost && existingPost.pubkey !== user.pubkey) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
You can only edit your own posts.
</AlertDescription>
</Alert>
);
}
if (isLoadingPost) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
{editIdentifier ? 'Edit Article' : 'New Article'}
</h1>
<p className="text-muted-foreground mt-1">
{editIdentifier ? 'Update your article' : 'Share your thoughts with the world'}
</p>
</div>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate('/')}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={isPublishing}
>
{isPublishing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Publishing...
</>
) : (
editIdentifier ? 'Update Article' : 'Publish Article'
)}
</Button>
</div>
</div>
<Separator />
{/* Metadata Section */}
<Card>
<CardHeader className="cursor-pointer" onClick={() => setShowMetadata(!showMetadata)}>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<FileText className="h-5 w-5" />
Article Metadata
</CardTitle>
<Button variant="ghost" size="sm">
{showMetadata ? 'Hide' : 'Show'}
</Button>
</div>
</CardHeader>
{showMetadata && (
<CardContent className="space-y-6">
{/* Identifier */}
<div className="space-y-2">
<Label htmlFor="identifier" className="flex items-center gap-2">
<Hash className="h-4 w-4" />
Identifier <span className="text-destructive">*</span>
</Label>
<Input
id="identifier"
value={metadata.identifier}
onChange={(e) => handleMetadataChange('identifier', e.target.value)}
placeholder="my-awesome-article"
required
disabled={!!editIdentifier}
className="font-mono"
/>
<p className="text-xs text-muted-foreground">
URL-friendly identifier (e.g., "my-awesome-article"). Cannot be changed after publishing.
</p>
</div>
{/* Title */}
<div className="space-y-2">
<Label htmlFor="title" className="flex items-center gap-2">
<FileText className="h-4 w-4" />
Title <span className="text-destructive">*</span>
</Label>
<Input
id="title"
value={metadata.title}
onChange={(e) => handleMetadataChange('title', e.target.value)}
placeholder="The Amazing Story of..."
required
className="text-lg"
/>
</div>
{/* Summary */}
<div className="space-y-2">
<Label htmlFor="summary">
Summary
</Label>
<Textarea
id="summary"
value={metadata.summary}
onChange={(e) => handleMetadataChange('summary', e.target.value)}
placeholder="A compelling summary that will appear in previews and search results..."
rows={3}
/>
<p className="text-xs text-muted-foreground">
Brief description of your article (recommended for better discoverability)
</p>
</div>
{/* Cover Image */}
<div className="space-y-2">
<Label htmlFor="image" className="flex items-center gap-2">
<ImageIcon className="h-4 w-4" />
Cover Image
</Label>
<div className="flex gap-2">
<Input
id="image"
value={metadata.image}
onChange={(e) => handleMetadataChange('image', e.target.value)}
placeholder="https://example.com/image.jpg"
type="url"
/>
<div className="relative">
<input
type="file"
accept="image/*"
onChange={handleImageUpload}
className="absolute inset-0 opacity-0 cursor-pointer"
disabled={isUploading}
/>
<Button type="button" variant="outline" disabled={isUploading}>
{isUploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Upload className="h-4 w-4" />
)}
</Button>
</div>
</div>
{metadata.image && (
<div className="mt-2 rounded-lg overflow-hidden border">
<img
src={metadata.image}
alt="Cover preview"
className="w-full max-h-64 object-cover"
/>
</div>
)}
</div>
{/* Hashtags */}
<div className="space-y-2">
<Label htmlFor="hashtags" className="flex items-center gap-2">
<Hash className="h-4 w-4" />
Topics & Tags
</Label>
<Input
id="hashtags"
value={metadata.hashtags}
onChange={(e) => handleMetadataChange('hashtags', e.target.value)}
placeholder="bitcoin, technology, innovation"
/>
<p className="text-xs text-muted-foreground">
Comma-separated tags to help readers discover your content
</p>
</div>
{editIdentifier && existingPost && (
<div className="pt-2 border-t">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>
Originally published: {new Date(parseInt(existingPost.tags.find(([name]) => name === 'published_at')?.[1] || '0') * 1000).toLocaleDateString()}
</span>
</div>
</div>
)}
</CardContent>
)}
</Card>
{/* Editor Section */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Content</CardTitle>
</CardHeader>
<CardContent>
<div >
<Editor
editorSerializedState={editorState}
onSerializedChange={(value) => setEditorState(value)}
/>
</div>
<p className="text-xs text-muted-foreground mt-4">
Write your article using the rich text editor. Markdown formatting is supported.
</p>
</CardContent>
</Card>
{/* Mobile Action Buttons */}
{isMobile && (
<Card className="sticky bottom-4 shadow-lg">
<CardContent className="py-4">
<div className="flex gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate('/')}
className="flex-1"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={isPublishing}
className="flex-1"
>
{isPublishing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Publishing...
</>
) : (
editIdentifier ? 'Update' : 'Publish'
)}
</Button>
</div>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,61 @@
"use client"
import {
InitialConfigType,
LexicalComposer,
} from "@lexical/react/LexicalComposer"
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"
import { EditorState, SerializedEditorState } from "lexical"
import { editorTheme } from "@/components/editor/themes/editor-theme"
import { TooltipProvider } from "@/components/ui/tooltip"
import { nodes } from "./nodes"
import { Plugins } from "./plugins"
const editorConfig: InitialConfigType = {
namespace: "Editor",
theme: editorTheme,
nodes,
onError: (error: Error) => {
console.error(error)
},
}
export function Editor({
editorState,
editorSerializedState,
onChange,
onSerializedChange,
}: {
editorState?: EditorState
editorSerializedState?: SerializedEditorState
onChange?: (editorState: EditorState) => void
onSerializedChange?: (editorSerializedState: SerializedEditorState) => void
}) {
return (
<div className="bg-background overflow-hidden rounded-lg border shadow">
<LexicalComposer
initialConfig={{
...editorConfig,
...(editorState ? { editorState } : {}),
...(editorSerializedState
? { editorState: JSON.stringify(editorSerializedState) }
: {}),
}}
>
<TooltipProvider>
<Plugins />
<OnChangePlugin
ignoreSelectionChange={true}
onChange={(editorState) => {
onChange?.(editorState)
onSerializedChange?.(editorState.toJSON())
}}
/>
</TooltipProvider>
</LexicalComposer>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { HeadingNode, QuoteNode } from "@lexical/rich-text"
import { ListNode, ListItemNode } from "@lexical/list"
import { CodeNode, CodeHighlightNode } from "@lexical/code"
import { LinkNode, AutoLinkNode } from "@lexical/link"
import {
Klass,
LexicalNode,
LexicalNodeReplacement,
ParagraphNode,
TextNode,
} from "lexical"
export const nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement> =
[
HeadingNode,
ParagraphNode,
TextNode,
QuoteNode,
ListNode,
ListItemNode,
CodeNode,
CodeHighlightNode,
LinkNode,
AutoLinkNode,
]

View File

@@ -0,0 +1,49 @@
import { useState } from "react"
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
import { ListPlugin } from "@lexical/react/LexicalListPlugin"
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"
import { TRANSFORMERS } from "@lexical/markdown"
import { ContentEditable } from "@/components/editor/editor-ui/content-editable"
import { ToolbarPlugin } from "@/components/editor/plugins/toolbar/ToolbarPlugin"
export function Plugins() {
const [_floatingAnchorElem, setFloatingAnchorElem] =
useState<HTMLDivElement | null>(null)
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
if (_floatingAnchorElem !== null) {
setFloatingAnchorElem(_floatingAnchorElem)
}
}
return (
<div className="relative">
{/* Toolbar */}
<ToolbarPlugin />
{/* Core Plugins */}
<HistoryPlugin />
<ListPlugin />
<LinkPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
{/* Editor */}
<div className="relative">
<RichTextPlugin
contentEditable={
<div className="min-h-[400px]">
<div className="" ref={onRef}>
<ContentEditable placeholder={"Start typing your article..."} />
</div>
</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import { JSX } from "react"
import { ContentEditable as LexicalContentEditable } from "@lexical/react/LexicalContentEditable"
type Props = {
placeholder: string
className?: string
placeholderClassName?: string
}
export function ContentEditable({
placeholder,
className,
placeholderClassName,
}: Props): JSX.Element {
return (
<LexicalContentEditable
className={
className ??
`ContentEditable__root relative block min-h-72 min-h-full overflow-auto px-8 py-4 focus:outline-none`
}
aria-placeholder={placeholder}
placeholder={
<div
className={
placeholderClassName ??
`text-muted-foreground pointer-events-none absolute top-0 left-0 overflow-hidden px-8 py-[18px] text-ellipsis select-none`
}
>
{placeholder}
</div>
}
/>
)
}

View File

@@ -0,0 +1,3 @@
export function ActionsPlugin({ children }: { children: React.ReactNode }) {
return children
}

View File

@@ -0,0 +1,63 @@
import { useCallback } from "react"
import { $createCodeNode, $isCodeNode } from "@lexical/code"
import {
$convertFromMarkdownString,
$convertToMarkdownString,
Transformer,
} from "@lexical/markdown"
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
import { $createTextNode, $getRoot } from "lexical"
import { FileTextIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
export function MarkdownTogglePlugin({
shouldPreserveNewLinesInMarkdown,
transformers,
}: {
shouldPreserveNewLinesInMarkdown: boolean
transformers: Array<Transformer>
}) {
const [editor] = useLexicalComposerContext()
const handleMarkdownToggle = useCallback(() => {
editor.update(() => {
const root = $getRoot()
const firstChild = root.getFirstChild()
if ($isCodeNode(firstChild) && firstChild.getLanguage() === "markdown") {
$convertFromMarkdownString(
firstChild.getTextContent(),
transformers,
undefined, // node
shouldPreserveNewLinesInMarkdown
)
} else {
const markdown = $convertToMarkdownString(
transformers,
undefined, //node
shouldPreserveNewLinesInMarkdown
)
const codeNode = $createCodeNode("markdown")
codeNode.append($createTextNode(markdown))
root.clear().append(codeNode)
if (markdown.length === 0) {
codeNode.select()
}
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editor, shouldPreserveNewLinesInMarkdown])
return (
<Button
variant={"ghost"}
onClick={handleMarkdownToggle}
title="Convert From Markdown"
aria-label="Convert from markdown"
size={"sm"}
className="p-2"
>
<FileTextIcon className="size-4" />
</Button>
)
}

View File

@@ -0,0 +1,477 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useCallback, useEffect, useState } from 'react';
import {
$getSelection,
$isRangeSelection,
FORMAT_TEXT_COMMAND,
FORMAT_ELEMENT_COMMAND,
REDO_COMMAND,
UNDO_COMMAND,
$createParagraphNode,
} from 'lexical';
import { $setBlocksType } from '@lexical/selection';
import {
INSERT_ORDERED_LIST_COMMAND,
INSERT_UNORDERED_LIST_COMMAND,
REMOVE_LIST_COMMAND,
$isListNode,
ListNode,
} from '@lexical/list';
import {
$createHeadingNode,
$createQuoteNode,
$isHeadingNode,
type HeadingTagType,
} from '@lexical/rich-text';
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
import { $isCodeNode, $createCodeNode } from '@lexical/code';
import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
import {
Bold,
Italic,
Underline,
Strikethrough,
Code,
Heading1,
Heading2,
Heading3,
List,
ListOrdered,
Quote,
Link as LinkIcon,
AlignLeft,
AlignCenter,
AlignRight,
AlignJustify,
Undo,
Redo,
FileCode,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Toggle } from '@/components/ui/toggle';
import { Input } from '@/components/ui/input';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
const LowPriority = 1;
type BlockType = 'paragraph' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'quote' | 'code' | 'ul' | 'ol';
export function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const [blockType, setBlockType] = useState<BlockType>('paragraph');
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isCode, setIsCode] = useState(false);
const [isLink, setIsLink] = useState(false);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [linkUrl, setLinkUrl] = useState('');
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false);
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsCode(selection.hasFormat('code'));
// Update link
const node = selection.anchor.getNode();
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
} else {
setIsLink(false);
}
// Update block type
const anchorNode = selection.anchor.getNode();
const element =
anchorNode.getKey() === 'root'
? anchorNode
: anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM !== null) {
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
const type = parentList ? parentList.getListType() : element.getListType();
setBlockType(type === 'number' ? 'ol' : 'ul');
} else {
const type = $isHeadingNode(element)
? element.getTag()
: element.getType();
setBlockType(type as BlockType);
}
}
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateToolbar();
});
}),
editor.registerCommand(
REDO_COMMAND,
() => {
setCanRedo(editor.getEditorState()._nodeMap.size > 0);
return false;
},
LowPriority
),
editor.registerCommand(
UNDO_COMMAND,
() => {
setCanUndo(editor.getEditorState()._nodeMap.size > 0);
return false;
},
LowPriority
)
);
}, [editor, updateToolbar]);
const formatText = (format: 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code') => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
};
const formatBlockType = (type: BlockType) => {
if (type === 'paragraph') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createParagraphNode());
}
});
} else if (type === 'h1' || type === 'h2' || type === 'h3' || type === 'h4' || type === 'h5' || type === 'h6') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode(type as HeadingTagType));
}
});
} else if (type === 'quote') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createQuoteNode());
}
});
} else if (type === 'code') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
if ($isCodeNode(selection.anchor.getNode())) {
$setBlocksType(selection, () => $createParagraphNode());
} else {
$setBlocksType(selection, () => $createCodeNode());
}
}
});
} else if (type === 'ul') {
if (blockType !== 'ul') {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
}
} else if (type === 'ol') {
if (blockType !== 'ol') {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
}
}
};
const formatAlignment = (alignment: 'left' | 'center' | 'right' | 'justify') => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment);
};
const insertLink = () => {
if (!isLink) {
setIsLinkPopoverOpen(true);
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
};
const handleLinkSubmit = () => {
if (linkUrl) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
setLinkUrl('');
setIsLinkPopoverOpen(false);
}
};
return (
<div className="flex flex-wrap items-center gap-1 p-2 border-b bg-muted/30">
{/* Undo/Redo */}
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
disabled={!canUndo}
className="h-8 w-8 p-0"
title="Undo"
>
<Undo className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
disabled={!canRedo}
className="h-8 w-8 p-0"
title="Redo"
>
<Redo className="h-4 w-4" />
</Button>
</div>
<Separator orientation="vertical" className="h-8" />
{/* Block Type Selector */}
<Select value={blockType} onValueChange={(value) => formatBlockType(value as BlockType)}>
<SelectTrigger className="h-8 w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="paragraph">Paragraph</SelectItem>
<SelectItem value="h1">Heading 1</SelectItem>
<SelectItem value="h2">Heading 2</SelectItem>
<SelectItem value="h3">Heading 3</SelectItem>
<SelectItem value="quote">Quote</SelectItem>
<SelectItem value="code">Code Block</SelectItem>
</SelectContent>
</Select>
<Separator orientation="vertical" className="h-8" />
{/* Text Formatting */}
<div className="flex gap-1">
<Toggle
size="sm"
pressed={isBold}
onPressedChange={() => formatText('bold')}
title="Bold"
className="h-8 w-8 p-0"
>
<Bold className="h-4 w-4" />
</Toggle>
<Toggle
size="sm"
pressed={isItalic}
onPressedChange={() => formatText('italic')}
title="Italic"
className="h-8 w-8 p-0"
>
<Italic className="h-4 w-4" />
</Toggle>
<Toggle
size="sm"
pressed={isUnderline}
onPressedChange={() => formatText('underline')}
title="Underline"
className="h-8 w-8 p-0"
>
<Underline className="h-4 w-4" />
</Toggle>
<Toggle
size="sm"
pressed={isStrikethrough}
onPressedChange={() => formatText('strikethrough')}
title="Strikethrough"
className="h-8 w-8 p-0"
>
<Strikethrough className="h-4 w-4" />
</Toggle>
<Toggle
size="sm"
pressed={isCode}
onPressedChange={() => formatText('code')}
title="Inline Code"
className="h-8 w-8 p-0"
>
<Code className="h-4 w-4" />
</Toggle>
</div>
<Separator orientation="vertical" className="h-8" />
{/* Quick Headings */}
<div className="flex gap-1">
<Button
variant={blockType === 'h1' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => formatBlockType('h1')}
title="Heading 1"
className="h-8 w-8 p-0"
>
<Heading1 className="h-4 w-4" />
</Button>
<Button
variant={blockType === 'h2' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => formatBlockType('h2')}
title="Heading 2"
className="h-8 w-8 p-0"
>
<Heading2 className="h-4 w-4" />
</Button>
<Button
variant={blockType === 'h3' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => formatBlockType('h3')}
title="Heading 3"
className="h-8 w-8 p-0"
>
<Heading3 className="h-4 w-4" />
</Button>
</div>
<Separator orientation="vertical" className="h-8" />
{/* Lists and Quote */}
<div className="flex gap-1">
<Button
variant={blockType === 'ul' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => formatBlockType('ul')}
title="Bullet List"
className="h-8 w-8 p-0"
>
<List className="h-4 w-4" />
</Button>
<Button
variant={blockType === 'ol' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => formatBlockType('ol')}
title="Numbered List"
className="h-8 w-8 p-0"
>
<ListOrdered className="h-4 w-4" />
</Button>
<Button
variant={blockType === 'quote' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => formatBlockType('quote')}
title="Quote"
className="h-8 w-8 p-0"
>
<Quote className="h-4 w-4" />
</Button>
<Button
variant={blockType === 'code' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => formatBlockType('code')}
title="Code Block"
className="h-8 w-8 p-0"
>
<FileCode className="h-4 w-4" />
</Button>
</div>
<Separator orientation="vertical" className="h-8" />
{/* Link */}
<Popover open={isLinkPopoverOpen} onOpenChange={setIsLinkPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant={isLink ? 'secondary' : 'ghost'}
size="sm"
onClick={insertLink}
title="Insert Link"
className="h-8 w-8 p-0"
>
<LinkIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
<h4 className="font-medium text-sm">Insert Link</h4>
<div className="flex gap-2">
<Input
placeholder="https://example.com"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleLinkSubmit();
}
}}
/>
<Button size="sm" onClick={handleLinkSubmit}>
Insert
</Button>
</div>
</div>
</PopoverContent>
</Popover>
<Separator orientation="vertical" className="h-8" />
{/* Text Alignment */}
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => formatAlignment('left')}
title="Align Left"
className="h-8 w-8 p-0"
>
<AlignLeft className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => formatAlignment('center')}
title="Align Center"
className="h-8 w-8 p-0"
>
<AlignCenter className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => formatAlignment('right')}
title="Align Right"
className="h-8 w-8 p-0"
>
<AlignRight className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => formatAlignment('justify')}
title="Justify"
className="h-8 w-8 p-0"
>
<AlignJustify className="h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,91 @@
.EditorTheme__code {
background-color: transparent;
font-family: Menlo, Consolas, Monaco, monospace;
display: block;
padding: 8px 8px 8px 52px;
line-height: 1.53;
font-size: 13px;
margin: 0;
margin-top: 8px;
margin-bottom: 8px;
overflow-x: auto;
border: 1px solid #ccc;
position: relative;
border-radius: 8px;
tab-size: 2;
}
.EditorTheme__code:before {
content: attr(data-gutter);
position: absolute;
background-color: transparent;
border-right: 1px solid #ccc;
left: 0;
top: 0;
padding: 8px;
color: #777;
white-space: pre-wrap;
text-align: right;
min-width: 25px;
}
.EditorTheme__table {
border-collapse: collapse;
border-spacing: 0;
overflow-y: scroll;
overflow-x: scroll;
table-layout: fixed;
width: fit-content;
width: 100%;
margin: 0px 0px 30px 0px;
}
.EditorTheme__tokenComment {
color: slategray;
}
.EditorTheme__tokenPunctuation {
color: #999;
}
.EditorTheme__tokenProperty {
color: #905;
}
.EditorTheme__tokenSelector {
color: #690;
}
.EditorTheme__tokenOperator {
color: #9a6e3a;
}
.EditorTheme__tokenAttr {
color: #07a;
}
.EditorTheme__tokenVariable {
color: #e90;
}
.EditorTheme__tokenFunction {
color: #dd4a68;
}
.Collapsible__container {
background-color: var(--background);
border: 1px solid #ccc;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
}
.Collapsible__title{
padding: 0.25rem;
padding-left: 1rem;
position: relative;
font-weight: bold;
outline: none;
cursor: pointer;
list-style-type: disclosure-closed;
list-style-position: inside;
}
.Collapsible__title p{
display: inline-flex;
}
.Collapsible__title::marker{
color: lightgray;
}
.Collapsible__container[open] >.Collapsible__title {
list-style-type: disclosure-open;
}

View File

@@ -0,0 +1,130 @@
import { EditorThemeClasses } from "lexical"
import "./editor-theme.css"
export const editorTheme: EditorThemeClasses = {
ltr: "text-left",
rtl: "text-right",
heading: {
h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
h2: "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
h3: "scroll-m-20 text-2xl font-semibold tracking-tight",
h4: "scroll-m-20 text-xl font-semibold tracking-tight",
h5: "scroll-m-20 text-lg font-semibold tracking-tight",
h6: "scroll-m-20 text-base font-semibold tracking-tight",
},
paragraph: "leading-7 [&:not(:first-child)]:mt-6",
quote: "mt-6 border-l-2 pl-6 italic",
link: "text-blue-600 hover:underline hover:cursor-pointer",
list: {
checklist: "relative",
listitem: "mx-8",
listitemChecked:
'relative mx-2 px-6 list-none outline-none line-through before:content-[""] before:w-4 before:h-4 before:top-0.5 before:left-0 before:cursor-pointer before:block before:bg-cover before:absolute before:border before:border-primary before:rounded before:bg-primary before:bg-no-repeat after:content-[""] after:cursor-pointer after:border-white after:border-solid after:absolute after:block after:top-[6px] after:w-[3px] after:left-[7px] after:right-[7px] after:h-[6px] after:rotate-45 after:border-r-2 after:border-b-2 after:border-l-0 after:border-t-0',
listitemUnchecked:
'relative mx-2 px-6 list-none outline-none before:content-[""] before:w-4 before:h-4 before:top-0.5 before:left-0 before:cursor-pointer before:block before:bg-cover before:absolute before:border before:border-primary before:rounded',
nested: {
listitem: "list-none before:hidden after:hidden",
},
ol: "m-0 p-0 list-decimal [&>li]:mt-2",
olDepth: [
"list-outside !list-decimal",
"list-outside !list-[upper-roman]",
"list-outside !list-[lower-roman]",
"list-outside !list-[upper-alpha]",
"list-outside !list-[lower-alpha]",
],
ul: "m-0 p-0 list-outside [&>li]:mt-2",
ulDepth: [
"list-outside !list-disc",
"list-outside !list-disc",
"list-outside !list-disc",
"list-outside !list-disc",
"list-outside !list-disc",
],
},
hashtag: "text-blue-600 bg-blue-100 rounded-md px-1",
text: {
bold: "font-bold",
code: "bg-gray-100 p-1 rounded-md",
italic: "italic",
strikethrough: "line-through",
subscript: "sub",
superscript: "sup",
underline: "underline",
underlineStrikethrough: "underline line-through",
},
image: "relative inline-block user-select-none cursor-default editor-image",
inlineImage:
"relative inline-block user-select-none cursor-default inline-editor-image",
keyword: "text-purple-900 font-bold",
code: "EditorTheme__code",
codeHighlight: {
atrule: "EditorTheme__tokenAttr",
attr: "EditorTheme__tokenAttr",
boolean: "EditorTheme__tokenProperty",
builtin: "EditorTheme__tokenSelector",
cdata: "EditorTheme__tokenComment",
char: "EditorTheme__tokenSelector",
class: "EditorTheme__tokenFunction",
"class-name": "EditorTheme__tokenFunction",
comment: "EditorTheme__tokenComment",
constant: "EditorTheme__tokenProperty",
deleted: "EditorTheme__tokenProperty",
doctype: "EditorTheme__tokenComment",
entity: "EditorTheme__tokenOperator",
function: "EditorTheme__tokenFunction",
important: "EditorTheme__tokenVariable",
inserted: "EditorTheme__tokenSelector",
keyword: "EditorTheme__tokenAttr",
namespace: "EditorTheme__tokenVariable",
number: "EditorTheme__tokenProperty",
operator: "EditorTheme__tokenOperator",
prolog: "EditorTheme__tokenComment",
property: "EditorTheme__tokenProperty",
punctuation: "EditorTheme__tokenPunctuation",
regex: "EditorTheme__tokenVariable",
selector: "EditorTheme__tokenSelector",
string: "EditorTheme__tokenSelector",
symbol: "EditorTheme__tokenProperty",
tag: "EditorTheme__tokenProperty",
url: "EditorTheme__tokenOperator",
variable: "EditorTheme__tokenVariable",
},
characterLimit: "!bg-destructive/50",
table: "EditorTheme__table w-fit overflow-scroll border-collapse",
tableCell:
'EditorTheme__tableCell w-24 relative border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"',
tableCellActionButton:
"EditorTheme__tableCellActionButton bg-background block border-0 rounded-2xl w-5 h-5 text-foreground cursor-pointer",
tableCellActionButtonContainer:
"EditorTheme__tableCellActionButtonContainer block right-1 top-1.5 absolute z-10 w-5 h-5",
tableCellEditing: "EditorTheme__tableCellEditing rounded-sm shadow-sm",
tableCellHeader:
"EditorTheme__tableCellHeader bg-muted border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right",
tableCellPrimarySelected:
"EditorTheme__tableCellPrimarySelected border border-primary border-solid block h-[calc(100%-2px)] w-[calc(100%-2px)] absolute -left-[1px] -top-[1px] z-10 ",
tableCellResizer:
"EditorTheme__tableCellResizer absolute -right-1 h-full w-2 cursor-ew-resize z-10 top-0",
tableCellSelected: "EditorTheme__tableCellSelected bg-muted",
tableCellSortedIndicator:
"EditorTheme__tableCellSortedIndicator block opacity-50 bsolute bottom-0 left-0 w-full h-1 bg-muted",
tableResizeRuler:
"EditorTheme__tableCellResizeRuler block absolute w-[1px] h-full bg-primary top-0",
tableRowStriping:
"EditorTheme__tableRowStriping m-0 border-t p-0 even:bg-muted",
tableSelected: "EditorTheme__tableSelected ring-2 ring-primary ring-offset-2",
tableSelection: "EditorTheme__tableSelection bg-transparent",
layoutItem: "border border-dashed px-4 py-2",
layoutContainer: "grid gap-2.5 my-2.5 mx-0",
autocomplete: "text-muted-foreground",
blockCursor: "",
embedBlock: {
base: "user-select-none",
focus: "ring-2 ring-primary ring-offset-2",
},
hr: 'p-0.5 border-none my-1 mx-0 cursor-pointer after:content-[""] after:block after:h-0.5 after:bg-muted selected:ring-2 selected:ring-primary selected:ring-offset-2 selected:user-select-none',
indent: "[--lexical-indent-base-value:40px]",
mark: "",
markOverlap: "",
}

View File

@@ -52,7 +52,7 @@ export function usePublishBlogPost() {
}
// Add client tag
tags.push(['client', 'Nostr Blog']);
// tags.push(['client', 'zelo.news']);
const eventTemplate = {
kind: 30023,

View File

@@ -1,9 +1,9 @@
import { BlogPostForm } from '@/components/BlogPostForm';
import { ProfessionalBlogPostForm } from '@/components/ProfessionalBlogPostForm';
export default function CreatePostPage() {
return (
<div className="container max-w-4xl py-8">
<BlogPostForm />
<div className="container max-w-7xl py-6 px-4 sm:px-6 lg:px-8">
<ProfessionalBlogPostForm />
</div>
);
}

View File

@@ -1,5 +1,5 @@
import { useParams, Navigate } from 'react-router-dom';
import { BlogPostForm } from '@/components/BlogPostForm';
import { ProfessionalBlogPostForm } from '@/components/ProfessionalBlogPostForm';
export default function EditPostPage() {
const { identifier } = useParams<{ identifier: string }>();
@@ -9,8 +9,8 @@ export default function EditPostPage() {
}
return (
<div className="container max-w-4xl py-8">
<BlogPostForm editIdentifier={identifier} />
<div className="container max-w-7xl py-6 px-4 sm:px-6 lg:px-8">
<ProfessionalBlogPostForm editIdentifier={identifier} />
</div>
);
}