fix: CSS class slide animation, mobile dot, flex task indicators
This commit is contained in:
parent
5e235b65ad
commit
499b012d82
819
package-lock.json
generated
819
package-lock.json
generated
@ -1,17 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.11.2",
|
"version": "1.13.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.11.1",
|
"version": "1.13.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@tiptap/extension-link": "^3.20.0",
|
||||||
|
"@tiptap/extension-placeholder": "^3.20.0",
|
||||||
|
"@tiptap/extension-underline": "^3.20.0",
|
||||||
|
"@tiptap/react": "^3.20.0",
|
||||||
|
"@tiptap/starter-kit": "^3.20.0",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/nodemailer": "^7.0.11",
|
"@types/nodemailer": "^7.0.11",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@ -840,6 +845,34 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.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/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@ -2290,6 +2323,12 @@
|
|||||||
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
|
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@ -2347,6 +2386,453 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-MDosUfs8Tj+nwg8RC+wTMWGkLJORXmbR6YZgbiX4hrc7G90Gopdd6kj6ht5/T8t7dLLaX7N0+DEHdUEPGED7dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-rYs4Bv5pVjqZ/2vvR6oe7ammZapkAwN51As/WDbemvYDjfOGRqK58qGauUjYZiDzPOEIzI2mxGwsZ4eJhPW4Ig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-placeholder": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-ZhYD3L5m16ydSe2z8vqz+RdtAG/iOQaFHHedFct70tKRoLqi2ajF5kgpemu8DwpaRTcyiCN4G99J/+MqehKNjQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-LzNXuy2jwR/y+ymoUqC72TiGzbOCjioIjsDu0MNYpHuHqTWPK5aV9Mh0nbZcYFy/7fPlV1q0W139EbJeYBZEAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==",
|
||||||
|
"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.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-jFLNzkmn18zqefJwPje0PPd9VhZ7Oy28YHiSvSc7YpBnQIbuN/HIxZ2lrOsKyEHta0WjRZjfU5X1pGxlbcGwOA==",
|
||||||
|
"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.20.0",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.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.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/extension-blockquote": "^3.20.0",
|
||||||
|
"@tiptap/extension-bold": "^3.20.0",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.20.0",
|
||||||
|
"@tiptap/extension-code": "^3.20.0",
|
||||||
|
"@tiptap/extension-code-block": "^3.20.0",
|
||||||
|
"@tiptap/extension-document": "^3.20.0",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.20.0",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.20.0",
|
||||||
|
"@tiptap/extension-hard-break": "^3.20.0",
|
||||||
|
"@tiptap/extension-heading": "^3.20.0",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.20.0",
|
||||||
|
"@tiptap/extension-italic": "^3.20.0",
|
||||||
|
"@tiptap/extension-link": "^3.20.0",
|
||||||
|
"@tiptap/extension-list": "^3.20.0",
|
||||||
|
"@tiptap/extension-list-item": "^3.20.0",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.20.0",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.20.0",
|
||||||
|
"@tiptap/extension-paragraph": "^3.20.0",
|
||||||
|
"@tiptap/extension-strike": "^3.20.0",
|
||||||
|
"@tiptap/extension-text": "^3.20.0",
|
||||||
|
"@tiptap/extension-underline": "^3.20.0",
|
||||||
|
"@tiptap/extensions": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@ -2472,6 +2958,28 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.34",
|
"version": "20.19.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.34.tgz",
|
||||||
@ -2494,14 +3002,12 @@
|
|||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.28",
|
"version": "18.3.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
||||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@ -2512,7 +3018,6 @@
|
|||||||
"version": "18.3.7",
|
"version": "18.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
@ -2525,6 +3030,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||||
@ -3233,7 +3744,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
@ -4152,6 +4662,12 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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-fetch": {
|
"node_modules/cross-fetch": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
|
||||||
@ -4212,7 +4728,6 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
@ -4508,6 +5023,18 @@
|
|||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/error-ex": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||||
@ -4705,7 +5232,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@ -5252,6 +5778,15 @@
|
|||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
@ -8393,6 +8928,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@ -8487,6 +9037,23 @@
|
|||||||
"tmpl": "1.0.5"
|
"tmpl": "1.0.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": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -8496,6 +9063,12 @@
|
|||||||
"node": ">= 0.4"
|
"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/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
@ -9091,6 +9664,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/own-keys": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||||
@ -9775,6 +10354,201 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
|
||||||
|
"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.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz",
|
||||||
|
"integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.20.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@ -9785,6 +10559,15 @@
|
|||||||
"node": ">=6"
|
"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/pure-rand": {
|
"node_modules/pure-rand": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||||
@ -10116,6 +10899,12 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@ -11290,6 +12079,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/uglify-js": {
|
"node_modules/uglify-js": {
|
||||||
"version": "3.19.3",
|
"version": "3.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||||
@ -11469,6 +12264,12 @@
|
|||||||
"node": ">=10.12.0"
|
"node": ">=10.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/walker": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||||
@ -11879,4 +12680,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,11 @@
|
|||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@tiptap/extension-link": "^3.20.0",
|
||||||
|
"@tiptap/extension-placeholder": "^3.20.0",
|
||||||
|
"@tiptap/extension-underline": "^3.20.0",
|
||||||
|
"@tiptap/react": "^3.20.0",
|
||||||
|
"@tiptap/starter-kit": "^3.20.0",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/nodemailer": "^7.0.11",
|
"@types/nodemailer": "^7.0.11",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@ -59,4 +64,4 @@
|
|||||||
"ts-jest": "^29.0.0",
|
"ts-jest": "^29.0.0",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ model User {
|
|||||||
pastDayColor String? @default("#a6a6a7")
|
pastDayColor String? @default("#a6a6a7")
|
||||||
goalScope String @default("week")
|
goalScope String @default("week")
|
||||||
dateLayout String @default("right")
|
dateLayout String @default("right")
|
||||||
|
mobileDateLayout String @default("below")
|
||||||
dateAlignment String @default("center")
|
dateAlignment String @default("center")
|
||||||
hourLabelFormat String @default("short")
|
hourLabelFormat String @default("short")
|
||||||
showSubHourSlots Boolean @default(true)
|
showSubHourSlots Boolean @default(true)
|
||||||
@ -158,6 +159,7 @@ model Task {
|
|||||||
recurrenceUnit String?
|
recurrenceUnit String?
|
||||||
recurrenceTime String?
|
recurrenceTime String?
|
||||||
recurrenceEndDate DateTime?
|
recurrenceEndDate DateTime?
|
||||||
|
recurrenceDays String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
externalId String?
|
externalId String?
|
||||||
|
|||||||
9
query.js
Normal file
9
query.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const conns = await prisma.calendarConnection.findMany({ where: { provider: 'synology' } });
|
||||||
|
console.log(JSON.stringify(conns, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||||
@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
|
|||||||
dbId: conn.id,
|
dbId: conn.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
id: conn.id,
|
id: conn.id,
|
||||||
provider: conn.provider as 'google' | 'apple' | 'outlook',
|
provider: conn.provider as 'google' | 'apple' | 'outlook' | 'synology',
|
||||||
accessToken: conn.accessToken,
|
accessToken: conn.accessToken,
|
||||||
refreshToken: conn.refreshToken ?? undefined,
|
refreshToken: conn.refreshToken ?? undefined,
|
||||||
expiresAt: conn.expiresAt ?? undefined,
|
expiresAt: conn.expiresAt ?? undefined,
|
||||||
|
|||||||
88
src/app/api/calendar/synology/connect/route.ts
Normal file
88
src/app/api/calendar/synology/connect/route.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { validateCredentials } from '@/lib/synology-calendar';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const { serverUrl, username, password } = body;
|
||||||
|
|
||||||
|
if (!serverUrl || !username || !password) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Server URL, username, and password are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate credentials and get calendars
|
||||||
|
const calendars = await validateCredentials(serverUrl, username, password);
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if connection already exists and update it, otherwise create new
|
||||||
|
const existingConnection = await prisma.calendarConnection.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
provider: 'synology'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
if (existingConnection) {
|
||||||
|
connection = await prisma.calendarConnection.update({
|
||||||
|
where: { id: existingConnection.id },
|
||||||
|
data: {
|
||||||
|
accessToken: `${username}:${password}`,
|
||||||
|
refreshToken: serverUrl, // Use refreshToken field to store the server URL since Basic Auth doesn't need refresh tokens
|
||||||
|
calendars: calendars.map(cal => ({
|
||||||
|
id: cal.id,
|
||||||
|
title: cal.title,
|
||||||
|
isPrimary: cal.isPrimary,
|
||||||
|
selected: true // Default to selected
|
||||||
|
})) as any,
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
connection = await prisma.calendarConnection.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
provider: 'synology',
|
||||||
|
accessToken: `${username}:${password}`,
|
||||||
|
refreshToken: serverUrl,
|
||||||
|
calendars: calendars.map(cal => ({
|
||||||
|
id: cal.id,
|
||||||
|
title: cal.title,
|
||||||
|
isPrimary: cal.isPrimary,
|
||||||
|
selected: true
|
||||||
|
})) as any
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, connection });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error connecting Synology Calendar:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error.message || 'Failed to connect Synology Calendar' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/app/api/projects/route.ts
Normal file
104
src/app/api/projects/route.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
|
||||||
|
const projects = await prisma.project.findMany({
|
||||||
|
where: { userId: user.id },
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ projects });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
|
||||||
|
const { name, color, icon } = await request.json();
|
||||||
|
if (!name) {
|
||||||
|
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await prisma.project.create({
|
||||||
|
data: { name, color: color || null, icon: icon || null, userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ project });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
|
||||||
|
const { id, name, color, icon } = await request.json();
|
||||||
|
if (!id) return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||||
|
|
||||||
|
const existing = await prisma.project.findFirst({ where: { id, userId: user.id } });
|
||||||
|
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||||
|
|
||||||
|
const project = await prisma.project.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...(name !== undefined && { name }),
|
||||||
|
...(color !== undefined && { color: color || null }),
|
||||||
|
...(icon !== undefined && { icon: icon || null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ project });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const id = searchParams.get('id');
|
||||||
|
if (!id) return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||||
|
|
||||||
|
const existing = await prisma.project.findFirst({ where: { id, userId: user.id } });
|
||||||
|
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||||
|
|
||||||
|
// Unlink tasks from this project
|
||||||
|
await prisma.task.updateMany({ where: { projectId: id }, data: { projectId: null } });
|
||||||
|
await prisma.project.delete({ where: { id } });
|
||||||
|
|
||||||
|
return NextResponse.json({ message: 'Project deleted' });
|
||||||
|
}
|
||||||
@ -35,7 +35,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { provider, sourceLists } = body;
|
const { provider, sourceLists } = body;
|
||||||
|
|
||||||
if (!provider || !['google', 'outlook'].includes(provider)) {
|
if (!provider || !['google', 'outlook', 'synology'].includes(provider)) {
|
||||||
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +151,47 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider === 'synology') {
|
||||||
|
const connection = await prisma.calendarConnection.findFirst({
|
||||||
|
where: { userId: user.id, provider: 'synology' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connection || !connection.accessToken || !connection.refreshToken) {
|
||||||
|
return NextResponse.json({ error: 'Synology account not connected' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [username, password] = connection.accessToken.split(':');
|
||||||
|
const serverUrl = connection.refreshToken;
|
||||||
|
|
||||||
|
const { fetchSynologyReminderLists, fetchSynologyTasks } = await import('@/lib/synology-tasks');
|
||||||
|
|
||||||
|
if (targetLists.length === 0) {
|
||||||
|
const synoLists = await fetchSynologyReminderLists(serverUrl, username, password);
|
||||||
|
if (synoLists.length > 0) {
|
||||||
|
targetLists = [{ id: synoLists[0].id, title: synoLists[0].title }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sourceList of targetLists) {
|
||||||
|
try {
|
||||||
|
const synoTasks = await fetchSynologyTasks(serverUrl, username, password, sourceList.id);
|
||||||
|
for (const t of synoTasks) {
|
||||||
|
importedTasks.push({
|
||||||
|
title: t.title,
|
||||||
|
description: t.notes || '',
|
||||||
|
externalId: t.id,
|
||||||
|
externalListId: sourceList.id,
|
||||||
|
dueDate: t.due ? new Date(t.due) : null,
|
||||||
|
status: t.status === 'completed' ? 'completed' : 'notStarted',
|
||||||
|
sourceListTitle: sourceList.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error fetching Synology tasks for list ${sourceList.id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Group tasks by source list title
|
// Group tasks by source list title
|
||||||
const tasksByList = new Map<string, ImportedTask[]>();
|
const tasksByList = new Map<string, ImportedTask[]>();
|
||||||
// Pre-initialize with all target lists to ensure they are created even if empty
|
// Pre-initialize with all target lists to ensure they are created even if empty
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export async function GET(req: NextRequest) {
|
|||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const provider = searchParams.get('provider');
|
const provider = searchParams.get('provider');
|
||||||
|
|
||||||
if (!provider || !['google', 'outlook'].includes(provider)) {
|
if (!provider || !['google', 'outlook', 'synology'].includes(provider)) {
|
||||||
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +60,27 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider === 'synology') {
|
||||||
|
const connection = await prisma.calendarConnection.findFirst({
|
||||||
|
where: { userId: user.id, provider: 'synology' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connection || !connection.accessToken || !connection.refreshToken) {
|
||||||
|
return NextResponse.json({ error: 'Synology account not connected' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [username, password] = connection.accessToken.split(':');
|
||||||
|
const serverUrl = connection.refreshToken;
|
||||||
|
|
||||||
|
// Re-export dynamically to avoid circular dependencies in route
|
||||||
|
const { fetchSynologyReminderLists } = await import('@/lib/synology-tasks');
|
||||||
|
const lists = await fetchSynologyReminderLists(serverUrl, username, password);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
lists: lists.map(l => ({ id: l.id, title: l.title }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 });
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@ -53,19 +53,33 @@ const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
|||||||
const interval = latestTask.recurrenceInterval || 1;
|
const interval = latestTask.recurrenceInterval || 1;
|
||||||
const unit = latestTask.recurrenceUnit || 'weeks'; // 'days', 'weeks', 'months', 'years'
|
const unit = latestTask.recurrenceUnit || 'weeks'; // 'days', 'weeks', 'months', 'years'
|
||||||
|
|
||||||
// Advance to next slot
|
// Parse recurrenceDays for weekly multi-day recurrence
|
||||||
// We only generate UP TO horizon.
|
let recDays: number[] | null = null;
|
||||||
// We avoid generating duplicates if a real task already exists at that date?
|
if (latestTask.recurrenceDays) {
|
||||||
// We already grouped by series, and we are projecting from the *latest* one.
|
try { recDays = JSON.parse(latestTask.recurrenceDays); } catch { recDays = null; }
|
||||||
// So any future dates we generate *should* be new.
|
}
|
||||||
|
|
||||||
// Safety break
|
// Safety break
|
||||||
let iterations = 0;
|
let iterations = 0;
|
||||||
while (currentDate < horizonDate && iterations < 100) {
|
while (currentDate < horizonDate && iterations < 500) {
|
||||||
iterations++;
|
iterations++;
|
||||||
|
|
||||||
// Advance date
|
// Advance date
|
||||||
if (unit === 'days') {
|
if (unit === 'weeks' && recDays && recDays.length > 0) {
|
||||||
|
// Multi-day weekly: advance day by day within each week cycle
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
// If we've moved past the current week (interval > 1), skip ahead
|
||||||
|
const daysSinceBase = Math.floor((currentDate.getTime() - baseDate.getTime()) / 86400000);
|
||||||
|
const weeksSinceBase = Math.floor(daysSinceBase / 7);
|
||||||
|
if (weeksSinceBase % interval !== 0) {
|
||||||
|
// Skip to start of next valid week
|
||||||
|
const nextValidWeek = (Math.floor(weeksSinceBase / interval) + 1) * interval;
|
||||||
|
const daysToSkip = (nextValidWeek * 7) - daysSinceBase;
|
||||||
|
currentDate.setDate(currentDate.getDate() + daysToSkip);
|
||||||
|
}
|
||||||
|
// Only emit if current day matches one of the selected days
|
||||||
|
if (!recDays.includes(currentDate.getDay())) continue;
|
||||||
|
} else if (unit === 'days') {
|
||||||
currentDate.setDate(currentDate.getDate() + interval);
|
currentDate.setDate(currentDate.getDate() + interval);
|
||||||
} else if (unit === 'weeks') {
|
} else if (unit === 'weeks') {
|
||||||
currentDate.setDate(currentDate.getDate() + (interval * 7));
|
currentDate.setDate(currentDate.getDate() + (interval * 7));
|
||||||
@ -74,7 +88,6 @@ const projectFutureTasks = (tasks: Task[], horizonDays = 90) => {
|
|||||||
} else if (unit === 'years') {
|
} else if (unit === 'years') {
|
||||||
currentDate.setFullYear(currentDate.getFullYear() + interval);
|
currentDate.setFullYear(currentDate.getFullYear() + interval);
|
||||||
} else {
|
} else {
|
||||||
// Default to weekly if unknown
|
|
||||||
currentDate.setDate(currentDate.getDate() + 7);
|
currentDate.setDate(currentDate.getDate() + 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,14 +133,24 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = (session.user as any).id;
|
// Resolve user by email (session ID can be stale after DB changes)
|
||||||
|
const dbUser = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = dbUser.id;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const start = searchParams.get('start');
|
const start = searchParams.get('start');
|
||||||
const end = searchParams.get('end');
|
const end = searchParams.get('end');
|
||||||
@ -195,10 +218,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = (session.user as any).id;
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
const { title, description, dayOfWeek, order, markdownContent, somedayListId, somedaySlotIndex, startTime, scheduledDate, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, parentTaskId, projectId } = body;
|
const { title, description, dayOfWeek, order, markdownContent, somedayListId, somedaySlotIndex, startTime, scheduledDate, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, recurrenceDays, parentTaskId, projectId } = body;
|
||||||
let { isRolling } = body;
|
let { isRolling } = body;
|
||||||
const { isRecurring } = body;
|
const { isRecurring } = body;
|
||||||
|
|
||||||
@ -209,30 +231,27 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If isRolling is not specified, check user preference
|
// Resolve user by email (session ID can be stale after DB changes)
|
||||||
if (isRolling === undefined) {
|
const user = await prisma.user.findUnique({
|
||||||
const user = await prisma.user.findUnique({
|
where: { email: session.user.email },
|
||||||
where: { email: session.user.email },
|
select: { id: true, autoRolling: true }
|
||||||
select: { autoRolling: true }
|
|
||||||
});
|
|
||||||
isRolling = user?.autoRolling || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('API POST - Creating task for user:', userId);
|
|
||||||
|
|
||||||
// Safety check: ensure user exists before creation to provide better error
|
|
||||||
const userExists = await prisma.user.findUnique({
|
|
||||||
where: { id: userId }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userExists) {
|
if (!user) {
|
||||||
console.error('CRITICAL: User ID in session does not exist in database:', userId);
|
console.error('CRITICAL: User email in session does not exist in database:', session.user.email);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'User does not exist in database. Please log out and back in.' },
|
{ error: 'User does not exist in database. Please log out and back in.' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
|
// If isRolling is not specified, check user preference
|
||||||
|
if (isRolling === undefined) {
|
||||||
|
isRolling = user.autoRolling || false;
|
||||||
|
}
|
||||||
|
|
||||||
const task = await prisma.task.create({
|
const task = await prisma.task.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
@ -250,6 +269,7 @@ export async function POST(request: NextRequest) {
|
|||||||
recurrenceUnit,
|
recurrenceUnit,
|
||||||
recurrenceTime,
|
recurrenceTime,
|
||||||
recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null,
|
recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null,
|
||||||
|
recurrenceDays: recurrenceDays ? JSON.stringify(recurrenceDays) : null,
|
||||||
somedaySlotIndex: somedaySlotIndex !== undefined ? parseInt(somedaySlotIndex) : null,
|
somedaySlotIndex: somedaySlotIndex !== undefined ? parseInt(somedaySlotIndex) : null,
|
||||||
parentTaskId: parentTaskId || null,
|
parentTaskId: parentTaskId || null,
|
||||||
...(projectId !== undefined && { projectId: projectId || null }),
|
...(projectId !== undefined && { projectId: projectId || null }),
|
||||||
@ -274,18 +294,28 @@ export async function PATCH(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = (session.user as any).id;
|
// Resolve user by email (session ID can be stale after DB changes)
|
||||||
|
const dbUser = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = dbUser.id;
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
const { id } = body;
|
const { id } = body;
|
||||||
const { title, description, completed, dayOfWeek, order, markdownContent, scheduledDate, startTime, somedayListId, somedaySlotIndex, isRecurring, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, restore, parentTaskId, projectId } = body;
|
const { title, description, completed, dayOfWeek, order, markdownContent, scheduledDate, startTime, somedayListId, somedaySlotIndex, isRecurring, recurrenceInterval, recurrenceUnit, recurrenceTime, recurrenceEndDate, recurrenceDays, restore, parentTaskId, projectId } = body;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -325,6 +355,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
recurrenceUnit: originalTask.recurrenceUnit,
|
recurrenceUnit: originalTask.recurrenceUnit,
|
||||||
recurrenceTime: originalTask.recurrenceTime,
|
recurrenceTime: originalTask.recurrenceTime,
|
||||||
recurrenceEndDate: originalTask.recurrenceEndDate,
|
recurrenceEndDate: originalTask.recurrenceEndDate,
|
||||||
|
recurrenceDays: originalTask.recurrenceDays,
|
||||||
isRolling: originalTask.isRolling,
|
isRolling: originalTask.isRolling,
|
||||||
// Apply overrides from the patch body immediately
|
// Apply overrides from the patch body immediately
|
||||||
completed: completed !== undefined ? completed : false,
|
completed: completed !== undefined ? completed : false,
|
||||||
@ -368,6 +399,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(recurrenceUnit !== undefined && { recurrenceUnit }),
|
...(recurrenceUnit !== undefined && { recurrenceUnit }),
|
||||||
...(recurrenceTime !== undefined && { recurrenceTime }),
|
...(recurrenceTime !== undefined && { recurrenceTime }),
|
||||||
...(recurrenceEndDate !== undefined && { recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null }),
|
...(recurrenceEndDate !== undefined && { recurrenceEndDate: recurrenceEndDate ? new Date(recurrenceEndDate) : null }),
|
||||||
|
...(recurrenceDays !== undefined && { recurrenceDays: recurrenceDays ? JSON.stringify(recurrenceDays) : null }),
|
||||||
...(restore === true && { deletedAt: null }),
|
...(restore === true && { deletedAt: null }),
|
||||||
...(somedaySlotIndex !== undefined && { somedaySlotIndex: somedaySlotIndex !== null ? parseInt(somedaySlotIndex) : null }),
|
...(somedaySlotIndex !== undefined && { somedaySlotIndex: somedaySlotIndex !== null ? parseInt(somedaySlotIndex) : null }),
|
||||||
...(parentTaskId !== undefined && { parentTaskId: parentTaskId || null }),
|
...(parentTaskId !== undefined && { parentTaskId: parentTaskId || null }),
|
||||||
@ -396,14 +428,24 @@ export async function DELETE(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = (session.user as any).id;
|
// Resolve user by email (session ID can be stale after DB changes)
|
||||||
|
const dbUser = await prisma.user.findUnique({
|
||||||
|
where: { email: session.user.email },
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = dbUser.id;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const id = searchParams.get('id');
|
const id = searchParams.get('id');
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,106 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Synology pull-sync ---
|
||||||
|
const synologySyncedLists = syncedLists.filter(l => l.externalProvider === 'synology');
|
||||||
|
const synologyLocalTasks = allExternalTasks.filter(t => t.externalProvider === 'synology');
|
||||||
|
const hasSynologyTasks = synologyLocalTasks.length > 0 || synologySyncedLists.length > 0;
|
||||||
|
|
||||||
|
if (hasSynologyTasks) {
|
||||||
|
const synoConnection = await prisma.calendarConnection.findFirst({
|
||||||
|
where: { userId: user.id, provider: 'synology' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (synoConnection?.accessToken && synoConnection?.refreshToken) {
|
||||||
|
const [synoUsername, synoPassword] = synoConnection.accessToken.split(':');
|
||||||
|
const synoServerUrl = synoConnection.refreshToken;
|
||||||
|
|
||||||
|
if (synoUsername && synoPassword && synoServerUrl) {
|
||||||
|
const { fetchSynologyTasks } = await import('@/lib/synology-tasks');
|
||||||
|
|
||||||
|
const synoListIds = new Set<string>();
|
||||||
|
const synoListIdToSomedayList = new Map<string, { id: string; title: string }>();
|
||||||
|
|
||||||
|
for (const task of synologyLocalTasks) {
|
||||||
|
if (task.externalListId) synoListIds.add(task.externalListId);
|
||||||
|
}
|
||||||
|
for (const sl of synologySyncedLists) {
|
||||||
|
if (sl.externalId) {
|
||||||
|
synoListIds.add(sl.externalId);
|
||||||
|
synoListIdToSomedayList.set(sl.externalId, { id: sl.id, title: sl.title });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const synoByList = new Map<string, typeof synologyLocalTasks>();
|
||||||
|
for (const task of synologyLocalTasks) {
|
||||||
|
if (!task.externalListId) continue;
|
||||||
|
if (!synoByList.has(task.externalListId)) synoByList.set(task.externalListId, []);
|
||||||
|
synoByList.get(task.externalListId)!.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listId of synoListIds) {
|
||||||
|
const localTasks = synoByList.get(listId) || [];
|
||||||
|
try {
|
||||||
|
const remoteTasks = await fetchSynologyTasks(synoServerUrl, synoUsername, synoPassword, listId);
|
||||||
|
// Remap ID: the fetchSynologyTasks prefixes ids with "synology::"
|
||||||
|
const remoteMap = new Map(remoteTasks.map(t => [t.id.replace(/^synology::/, ''), t]));
|
||||||
|
const existingExternalIds = new Set(localTasks.map(t => t.externalId?.replace(/^synology::/, '')));
|
||||||
|
|
||||||
|
for (const localTask of localTasks) {
|
||||||
|
const cleanExtId = localTask.externalId?.replace(/^synology::/, '');
|
||||||
|
const remote = cleanExtId ? remoteMap.get(cleanExtId) : null;
|
||||||
|
|
||||||
|
if (!remote) {
|
||||||
|
await prisma.task.update({ where: { id: localTask.id }, data: { deletedAt: new Date() } });
|
||||||
|
deleted++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: any = { lastSyncedAt: new Date() };
|
||||||
|
const remoteCompleted = remote.status === 'completed';
|
||||||
|
if (remoteCompleted !== localTask.completed) updateData.completed = remoteCompleted;
|
||||||
|
if (remote.title && remote.title !== localTask.title) updateData.title = remote.title;
|
||||||
|
|
||||||
|
await prisma.task.update({ where: { id: localTask.id }, data: updateData });
|
||||||
|
if (Object.keys(updateData).length > 1) updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new local tasks from remote
|
||||||
|
const somedayListInfo = synoListIdToSomedayList.get(listId);
|
||||||
|
if (somedayListInfo) {
|
||||||
|
const newRemoteTasks = remoteTasks.filter(rt => {
|
||||||
|
const cleanId = rt.id.replace(/^synology::/, '');
|
||||||
|
return !existingExternalIds.has(cleanId);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const remote of newRemoteTasks) {
|
||||||
|
if (!remote.title?.trim()) continue;
|
||||||
|
await prisma.task.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
title: remote.title,
|
||||||
|
description: remote.notes || null,
|
||||||
|
completed: remote.status === 'completed',
|
||||||
|
somedayListId: somedayListInfo.id,
|
||||||
|
externalId: remote.id,
|
||||||
|
externalProvider: 'synology',
|
||||||
|
externalListId: listId,
|
||||||
|
scheduledDate: remote.due ? new Date(remote.due) : null,
|
||||||
|
lastSyncedAt: new Date(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
existingExternalIds.add(remote.id.replace(/^synology::/, ''));
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (listError) {
|
||||||
|
console.error(`Error syncing Synology list ${listId}:`, listError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true, updated, deleted, created });
|
return NextResponse.json({ success: true, updated, deleted, created });
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export async function GET(request: NextRequest) {
|
|||||||
goalFontWeight: true,
|
goalFontWeight: true,
|
||||||
goalScope: true,
|
goalScope: true,
|
||||||
dateLayout: true,
|
dateLayout: true,
|
||||||
|
mobileDateLayout: true,
|
||||||
headlineFont: true,
|
headlineFont: true,
|
||||||
headlineFontSize: true,
|
headlineFontSize: true,
|
||||||
headlineFontWeight: true,
|
headlineFontWeight: true,
|
||||||
@ -120,7 +121,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
||||||
pastDayColor, goalFallbackType, goalDefaultSentence,
|
pastDayColor, goalFallbackType, goalDefaultSentence,
|
||||||
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
|
goalFontFamily, goalFontSize, goalFontWeight, goalScope,
|
||||||
dateLayout,
|
dateLayout, mobileDateLayout,
|
||||||
hourLabelFormat, showSubHourSlots, allDayPosition,
|
hourLabelFormat, showSubHourSlots, allDayPosition,
|
||||||
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
||||||
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
||||||
@ -182,6 +183,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(goalFontWeight !== undefined && { goalFontWeight }),
|
...(goalFontWeight !== undefined && { goalFontWeight }),
|
||||||
...(goalScope !== undefined && { goalScope }),
|
...(goalScope !== undefined && { goalScope }),
|
||||||
...(dateLayout !== undefined && { dateLayout }),
|
...(dateLayout !== undefined && { dateLayout }),
|
||||||
|
...(mobileDateLayout !== undefined && { mobileDateLayout }),
|
||||||
...(hourLabelFormat !== undefined && { hourLabelFormat }),
|
...(hourLabelFormat !== undefined && { hourLabelFormat }),
|
||||||
...(showSubHourSlots !== undefined && { showSubHourSlots }),
|
...(showSubHourSlots !== undefined && { showSubHourSlots }),
|
||||||
...(allDayPosition !== undefined && { allDayPosition }),
|
...(allDayPosition !== undefined && { allDayPosition }),
|
||||||
@ -265,6 +267,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
goalFontWeight: true,
|
goalFontWeight: true,
|
||||||
goalScope: true,
|
goalScope: true,
|
||||||
dateLayout: true,
|
dateLayout: true,
|
||||||
|
mobileDateLayout: true,
|
||||||
cwFontFamily: true,
|
cwFontFamily: true,
|
||||||
cwFontSize: true,
|
cwFontSize: true,
|
||||||
cwFontWeight: true,
|
cwFontWeight: true,
|
||||||
|
|||||||
@ -1041,6 +1041,10 @@ h3 {
|
|||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
color: var(--weekly-text);
|
color: var(--weekly-text);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.subtask-item:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtask-item.completed .subtask-title {
|
.subtask-item.completed .subtask-title {
|
||||||
@ -2750,27 +2754,27 @@ h3 {
|
|||||||
/* Class-based animation that works on ALL browsers (no View Transitions API needed) */
|
/* Class-based animation that works on ALL browsers (no View Transitions API needed) */
|
||||||
|
|
||||||
@keyframes navSlideInFromRight {
|
@keyframes navSlideInFromRight {
|
||||||
from { transform: translateX(40%); opacity: 0.3; }
|
from { transform: translateX(30%); opacity: 0; }
|
||||||
to { transform: translateX(0); opacity: 1; }
|
to { transform: translateX(0); opacity: 1; }
|
||||||
}
|
}
|
||||||
@keyframes navSlideInFromLeft {
|
@keyframes navSlideInFromLeft {
|
||||||
from { transform: translateX(-40%); opacity: 0.3; }
|
from { transform: translateX(-30%); opacity: 0; }
|
||||||
to { transform: translateX(0); opacity: 1; }
|
to { transform: translateX(0); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-animate-next {
|
.slide-animate-next {
|
||||||
animation: navSlideInFromRight 0.35s cubic-bezier(0.25, 1, 0.5, 1) both;
|
animation: navSlideInFromRight 0.3s cubic-bezier(0.25, 1, 0.5, 1) both;
|
||||||
}
|
}
|
||||||
.slide-animate-prev {
|
.slide-animate-prev {
|
||||||
animation: navSlideInFromLeft 0.35s cubic-bezier(0.25, 1, 0.5, 1) both;
|
animation: navSlideInFromLeft 0.3s cubic-bezier(0.25, 1, 0.5, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Week navigation: 5x longer for a more dramatic slide */
|
/* Week navigation: slightly longer slide with more offset */
|
||||||
.slide-animate-week-next {
|
.slide-animate-week-next {
|
||||||
animation: navSlideInFromRight 1.75s cubic-bezier(0.25, 1, 0.5, 1) both;
|
animation: navSlideInFromRight 0.45s cubic-bezier(0.25, 1, 0.5, 1) both;
|
||||||
}
|
}
|
||||||
.slide-animate-week-prev {
|
.slide-animate-week-prev {
|
||||||
animation: navSlideInFromLeft 1.75s cubic-bezier(0.25, 1, 0.5, 1) both;
|
animation: navSlideInFromLeft 0.45s cubic-bezier(0.25, 1, 0.5, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth View Transitions */
|
/* Smooth View Transitions */
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, lazy, Suspense } from 'react';
|
||||||
|
|
||||||
|
const RichTextEditor = lazy(() => import('./RichTextEditor'));
|
||||||
|
|
||||||
interface CalendarEventModalProps {
|
interface CalendarEventModalProps {
|
||||||
event?: any; // Existing event if editing
|
event?: any; // Existing event if editing
|
||||||
@ -26,7 +28,8 @@ export default function CalendarEventModal({
|
|||||||
provider: conn.provider,
|
provider: conn.provider,
|
||||||
providerName: conn.provider === 'google' ? 'Google Calendar' :
|
providerName: conn.provider === 'google' ? 'Google Calendar' :
|
||||||
conn.provider === 'apple' ? 'Apple Calendar' :
|
conn.provider === 'apple' ? 'Apple Calendar' :
|
||||||
'Outlook Calendar'
|
conn.provider === 'synology' ? 'Synology Calendar' :
|
||||||
|
'Outlook Calendar'
|
||||||
})))
|
})))
|
||||||
.filter((cal: any) => cal.editable); // Only editable calendars
|
.filter((cal: any) => cal.editable); // Only editable calendars
|
||||||
|
|
||||||
@ -284,16 +287,25 @@ export default function CalendarEventModal({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description - Rich text */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Notes</label>
|
<label style={{ display: 'block', marginBottom: '5px', fontSize: '0.9rem', color: '#666' }}>Notes</label>
|
||||||
<textarea
|
<Suspense fallback={
|
||||||
value={description}
|
<textarea
|
||||||
onChange={e => setDescription(e.target.value)}
|
value={description}
|
||||||
placeholder="Notes..."
|
onChange={e => setDescription(e.target.value)}
|
||||||
rows={3}
|
placeholder="Notes..."
|
||||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', resize: 'vertical' }}
|
rows={3}
|
||||||
/>
|
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<RichTextEditor
|
||||||
|
value={description}
|
||||||
|
onChange={setDescription}
|
||||||
|
placeholder="Notes..."
|
||||||
|
minHeight="120px"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ interface ImportListModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onImport: (selectedLists: { id: string, title: string }[]) => void;
|
onImport: (selectedLists: { id: string, title: string }[]) => void;
|
||||||
provider: 'google' | 'apple' | 'outlook' | null;
|
provider: 'google' | 'apple' | 'outlook' | 'synology' | null;
|
||||||
lists: { id: string, title: string }[];
|
lists: { id: string, title: string }[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,13 @@ interface RecurrenceModalProps {
|
|||||||
task: any;
|
task: any;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (taskId: string, recurrence: any) => Promise<void>;
|
onSave: (taskId: string, recurrence: any) => Promise<void>;
|
||||||
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceModalProps) {
|
const WEEKDAYS_EN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
const WEEKDAYS_DE = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||||
|
|
||||||
|
export default function RecurrenceModal({ task, onClose, onSave, language = 'en' }: RecurrenceModalProps) {
|
||||||
const [isRecurring, setIsRecurring] = useState(task.isRecurring || false);
|
const [isRecurring, setIsRecurring] = useState(task.isRecurring || false);
|
||||||
const [interval, setInterval] = useState(task.recurrenceInterval || 1);
|
const [interval, setInterval] = useState(task.recurrenceInterval || 1);
|
||||||
const [unit, setUnit] = useState(task.recurrenceUnit || 'weeks');
|
const [unit, setUnit] = useState(task.recurrenceUnit || 'weeks');
|
||||||
@ -14,6 +18,13 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
const [endDate, setEndDate] = useState(task.recurrenceEndDate ? new Date(task.recurrenceEndDate).toISOString().split('T')[0] : '');
|
const [endDate, setEndDate] = useState(task.recurrenceEndDate ? new Date(task.recurrenceEndDate).toISOString().split('T')[0] : '');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// Day-of-week for weekly recurrence (0=Sun, 1=Mon, etc.)
|
||||||
|
const taskDow = task.scheduledDate ? new Date(task.scheduledDate).getDay() : (task.dayOfWeek ?? new Date().getDay());
|
||||||
|
const [selectedDays, setSelectedDays] = useState<number[]>(task.recurrenceDays?.length ? task.recurrenceDays : [taskDow]);
|
||||||
|
|
||||||
|
const weekdays = language === 'de' ? WEEKDAYS_DE : WEEKDAYS_EN;
|
||||||
|
const isDE = language === 'de';
|
||||||
|
|
||||||
// Helper to format date as DD.MM.YYYY
|
// Helper to format date as DD.MM.YYYY
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDate = (dateStr: string) => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
@ -33,6 +44,15 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
return input; // fallback to raw
|
return input; // fallback to raw
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleDay = (day: number) => {
|
||||||
|
setSelectedDays(prev => {
|
||||||
|
if (prev.includes(day)) {
|
||||||
|
return prev.length > 1 ? prev.filter(d => d !== day) : prev; // keep at least one
|
||||||
|
}
|
||||||
|
return [...prev, day].sort();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
@ -42,7 +62,8 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
recurrenceInterval: isRecurring ? interval : null,
|
recurrenceInterval: isRecurring ? interval : null,
|
||||||
recurrenceUnit: isRecurring ? unit : null,
|
recurrenceUnit: isRecurring ? unit : null,
|
||||||
recurrenceTime: isRecurring ? time : null,
|
recurrenceTime: isRecurring ? time : null,
|
||||||
recurrenceEndDate: isRecurring && parsedEndDate ? new Date(parsedEndDate) : null
|
recurrenceEndDate: isRecurring && parsedEndDate ? new Date(parsedEndDate) : null,
|
||||||
|
recurrenceDays: isRecurring && unit === 'weeks' ? selectedDays : null,
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -54,7 +75,7 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
return (
|
return (
|
||||||
<div className="weekly-modal-overlay" onClick={onClose}>
|
<div className="weekly-modal-overlay" onClick={onClose}>
|
||||||
<div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: '400px' }}>
|
<div className="weekly-modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: '400px' }}>
|
||||||
<h3 style={{ marginBottom: '1.5rem' }}>Recurring Task</h3>
|
<h3 style={{ marginBottom: '1.5rem' }}>{isDE ? 'Wiederkehrende Aufgabe' : 'Recurring Task'}</h3>
|
||||||
|
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '1rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
@ -65,13 +86,13 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
onChange={e => setIsRecurring(e.target.checked)}
|
onChange={e => setIsRecurring(e.target.checked)}
|
||||||
style={{ width: '18px', height: '18px', marginRight: '10px' }}
|
style={{ width: '18px', height: '18px', marginRight: '10px' }}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="isRecurring" style={{ fontSize: '1rem', fontWeight: 500 }}>Enable Recurrence</label>
|
<label htmlFor="isRecurring" style={{ fontSize: '1rem', fontWeight: 500 }}>{isDE ? 'Wiederholung aktivieren' : 'Enable Recurrence'}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isRecurring && (
|
{isRecurring && (
|
||||||
<div style={{ paddingLeft: '28px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ paddingLeft: '28px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>Repeat every</label>
|
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'Wiederholen alle' : 'Repeat every'}</label>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -85,16 +106,46 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
onChange={e => setUnit(e.target.value)}
|
onChange={e => setUnit(e.target.value)}
|
||||||
style={{ flex: 1, padding: '6px', borderRadius: '4px', border: '1px solid #ddd' }}
|
style={{ flex: 1, padding: '6px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||||
>
|
>
|
||||||
<option value="days">Days</option>
|
<option value="days">{isDE ? 'Tage' : 'Days'}</option>
|
||||||
<option value="weeks">Weeks</option>
|
<option value="weeks">{isDE ? 'Wochen' : 'Weeks'}</option>
|
||||||
<option value="months">Months</option>
|
<option value="months">{isDE ? 'Monate' : 'Months'}</option>
|
||||||
<option value="years">Years</option>
|
<option value="years">{isDE ? 'Jahre' : 'Years'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Weekday selector for weekly recurrence */}
|
||||||
|
{unit === 'weeks' && (
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '6px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'An diesen Tagen' : 'On these days'}</label>
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
{weekdays.map((dayName, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleDay(i)}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '6px 2px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontWeight: selectedDays.includes(i) ? 700 : 400,
|
||||||
|
background: selectedDays.includes(i) ? '#3b82f6' : '#f3f4f6',
|
||||||
|
color: selectedDays.includes(i) ? '#fff' : '#666',
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dayName}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>Time</label>
|
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'Uhrzeit' : 'Time'}</label>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
value={time}
|
value={time}
|
||||||
@ -104,7 +155,7 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>End Date (Optional, DD.MM.YYYY)</label>
|
<label style={{ display: 'block', marginBottom: '4px', fontSize: '0.9rem', color: '#666' }}>{isDE ? 'Enddatum (Optional, TT.MM.JJJJ)' : 'End Date (Optional, DD.MM.YYYY)'}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="DD.MM.YYYY"
|
placeholder="DD.MM.YYYY"
|
||||||
@ -147,14 +198,14 @@ export default function RecurrenceModal({ task, onClose, onSave }: RecurrenceMod
|
|||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
{isSaving ? 'Saving...' : 'Save'}
|
{isSaving ? (isDE ? 'Speichere...' : 'Saving...') : (isDE ? 'Speichern' : 'Save')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="weekly-btn weekly-btn-secondary"
|
className="weekly-btn weekly-btn-secondary"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
Cancel
|
{isDE ? 'Abbrechen' : 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
150
src/components/RichTextEditor.tsx
Normal file
150
src/components/RichTextEditor.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
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 Placeholder from '@tiptap/extension-placeholder';
|
||||||
|
|
||||||
|
interface RichTextEditorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (html: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
minHeight?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RichTextEditor({ value, onChange, placeholder = 'Notes...', minHeight = '120px' }: RichTextEditorProps) {
|
||||||
|
const editor = useEditor({
|
||||||
|
immediatelyRender: false,
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
heading: { levels: [1, 2, 3] },
|
||||||
|
}),
|
||||||
|
Underline,
|
||||||
|
Link.configure({
|
||||||
|
openOnClick: false,
|
||||||
|
HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
|
||||||
|
}),
|
||||||
|
Placeholder.configure({ placeholder }),
|
||||||
|
],
|
||||||
|
content: value,
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
// When content is just empty paragraph, return empty string
|
||||||
|
const html = editor.isEmpty ? '' : editor.getHTML();
|
||||||
|
onChange(html);
|
||||||
|
},
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: 'rte-editor-content',
|
||||||
|
style: `min-height: ${minHeight}; outline: none;`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync external value changes (e.g. when editing an existing event)
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && editor.getHTML() !== value && !editor.isFocused) {
|
||||||
|
editor.commands.setContent(value || '');
|
||||||
|
}
|
||||||
|
}, [value, editor]);
|
||||||
|
|
||||||
|
if (!editor) return null;
|
||||||
|
|
||||||
|
const btn = (active: boolean) => ({
|
||||||
|
padding: '3px 7px',
|
||||||
|
border: '1px solid var(--weekly-border, #ddd)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: active ? 'var(--weekly-accent, #2563eb)' : 'var(--weekly-bg, white)',
|
||||||
|
color: active ? 'white' : 'var(--weekly-text, #333)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
fontWeight: active ? '700' : '500',
|
||||||
|
lineHeight: 1,
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
} as React.CSSProperties);
|
||||||
|
|
||||||
|
const handleLink = () => {
|
||||||
|
const prev = editor.getAttributes('link').href || '';
|
||||||
|
const url = window.prompt('Enter URL:', prev);
|
||||||
|
if (url === null) return;
|
||||||
|
if (url === '') {
|
||||||
|
editor.chain().focus().unsetLink().run();
|
||||||
|
} else {
|
||||||
|
editor.chain().focus().setLink({ href: url }).run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid var(--weekly-border, #ddd)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: 'var(--weekly-bg, white)',
|
||||||
|
}}>
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '4px',
|
||||||
|
padding: '6px 8px',
|
||||||
|
borderBottom: '1px solid var(--weekly-border, #ddd)',
|
||||||
|
background: 'var(--weekly-settings-item-bg, #f8f8f8)',
|
||||||
|
}}>
|
||||||
|
<button type="button" style={btn(editor.isActive('bold'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleBold().run(); }} title="Bold">
|
||||||
|
<b>B</b>
|
||||||
|
</button>
|
||||||
|
<button type="button" style={btn(editor.isActive('italic'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleItalic().run(); }} title="Italic">
|
||||||
|
<i>I</i>
|
||||||
|
</button>
|
||||||
|
<button type="button" style={btn(editor.isActive('underline'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleUnderline().run(); }} title="Underline">
|
||||||
|
<u>U</u>
|
||||||
|
</button>
|
||||||
|
<div style={{ width: 1, background: 'var(--weekly-border, #ddd)', margin: '0 2px' }} />
|
||||||
|
<button type="button" style={btn(editor.isActive('bulletList'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleBulletList().run(); }} title="Bullet list">
|
||||||
|
≡
|
||||||
|
</button>
|
||||||
|
<button type="button" style={btn(editor.isActive('orderedList'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleOrderedList().run(); }} title="Numbered list">
|
||||||
|
1.
|
||||||
|
</button>
|
||||||
|
<div style={{ width: 1, background: 'var(--weekly-border, #ddd)', margin: '0 2px' }} />
|
||||||
|
<button type="button" style={btn(editor.isActive('blockquote'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleBlockquote().run(); }} title="Blockquote">
|
||||||
|
"
|
||||||
|
</button>
|
||||||
|
<button type="button" style={btn(editor.isActive('link'))} onMouseDown={e => { e.preventDefault(); handleLink(); }} title="Link">
|
||||||
|
🔗
|
||||||
|
</button>
|
||||||
|
<div style={{ width: 1, background: 'var(--weekly-border, #ddd)', margin: '0 2px' }} />
|
||||||
|
<button type="button" style={btn(false)} onMouseDown={e => { e.preventDefault(); editor.chain().focus().undo().run(); }} title="Undo">
|
||||||
|
↩
|
||||||
|
</button>
|
||||||
|
<button type="button" style={btn(false)} onMouseDown={e => { e.preventDefault(); editor.chain().focus().redo().run(); }} title="Redo">
|
||||||
|
↪
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor content */}
|
||||||
|
<EditorContent editor={editor} style={{ padding: '10px 12px' }} />
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.rte-editor-content p { margin: 0 0 0.5em; }
|
||||||
|
.rte-editor-content p:last-child { margin-bottom: 0; }
|
||||||
|
.rte-editor-content ul, .rte-editor-content ol { padding-left: 1.4em; margin: 0.3em 0; }
|
||||||
|
.rte-editor-content blockquote {
|
||||||
|
border-left: 3px solid var(--weekly-accent, #2563eb);
|
||||||
|
margin: 0.3em 0;
|
||||||
|
padding-left: 0.8em;
|
||||||
|
color: var(--weekly-text-muted, #666);
|
||||||
|
}
|
||||||
|
.rte-editor-content a { color: var(--weekly-accent, #2563eb); text-decoration: underline; }
|
||||||
|
.rte-editor-content .is-editor-empty:first-child::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
color: var(--weekly-text-muted, #adb5bd);
|
||||||
|
pointer-events: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -113,6 +113,16 @@ export const authOptions: NextAuthOptions = {
|
|||||||
if (user) {
|
if (user) {
|
||||||
token.id = user.id;
|
token.id = user.id;
|
||||||
}
|
}
|
||||||
|
// Refresh user ID from DB to handle stale sessions after DB changes
|
||||||
|
if (token.email) {
|
||||||
|
const dbUser = await prisma.user.findUnique({
|
||||||
|
where: { email: token.email as string },
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
if (dbUser) {
|
||||||
|
token.id = dbUser.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export async function readCachedEvents(
|
|||||||
isRecurring: row.isRecurring,
|
isRecurring: row.isRecurring,
|
||||||
startTime: row.startDateTime?.toISOString() ?? row.startDate ?? '',
|
startTime: row.startDateTime?.toISOString() ?? row.startDate ?? '',
|
||||||
endTime: row.endDateTime?.toISOString() ?? row.endDate ?? '',
|
endTime: row.endDateTime?.toISOString() ?? row.endDate ?? '',
|
||||||
source: row.provider as 'google' | 'apple' | 'outlook',
|
source: row.provider as 'google' | 'apple' | 'outlook' | 'synology',
|
||||||
calendarId: row.calendarId,
|
calendarId: row.calendarId,
|
||||||
calendarTitle: row.calendarTitle,
|
calendarTitle: row.calendarTitle,
|
||||||
calendarColor: row.calendarColor,
|
calendarColor: row.calendarColor,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Google Calendar OAuth wrapper
|
|
||||||
import { GoogleCalendarEvent, getUserCalendars as getGoogleCalendars, getUpcomingEvents as getGoogleEvents, initializeOAuth as initializeGoogleOAuth } from './google-calendar';
|
import { GoogleCalendarEvent, getUserCalendars as getGoogleCalendars, getUpcomingEvents as getGoogleEvents, initializeOAuth as initializeGoogleOAuth } from './google-calendar';
|
||||||
import { AppleCalendarEvent, getUserCalendars as getAppleCalendars, getUpcomingEvents as getAppleEvents } from './apple-calendar';
|
import { AppleCalendarEvent, getUserCalendars as getAppleCalendars, getUpcomingEvents as getAppleEvents } from './apple-calendar';
|
||||||
import { getUpcomingEvents as getOutlookEvents, refreshAccessToken as refreshOutlookTokenAPI, createEvent as createOutlookEvent, updateEvent as updateOutlookEvent, deleteEvent as deleteOutlookEvent } from './outlook-calendar';
|
import { getUpcomingEvents as getOutlookEvents, refreshAccessToken as refreshOutlookTokenAPI, createEvent as createOutlookEvent, updateEvent as updateOutlookEvent, deleteEvent as deleteOutlookEvent } from './outlook-calendar';
|
||||||
|
import { getUserCalendars as getSynologyCalendars, getUpcomingEvents as getSynologyEvents } from './synology-calendar';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
@ -23,7 +23,7 @@ export interface CalendarEvent {
|
|||||||
recurrence?: string;
|
recurrence?: string;
|
||||||
recurringEventId?: string;
|
recurringEventId?: string;
|
||||||
isRecurring?: boolean;
|
isRecurring?: boolean;
|
||||||
source: 'google' | 'apple' | 'outlook';
|
source: 'google' | 'apple' | 'outlook' | 'synology';
|
||||||
calendarId: string;
|
calendarId: string;
|
||||||
calendarTitle: string;
|
calendarTitle: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
@ -90,7 +90,7 @@ function getGoogleEventColor(colorId: string): string {
|
|||||||
|
|
||||||
export interface CalendarConnection {
|
export interface CalendarConnection {
|
||||||
id: string;
|
id: string;
|
||||||
provider: 'google' | 'apple' | 'outlook';
|
provider: 'google' | 'apple' | 'outlook' | 'synology';
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
@ -372,6 +372,81 @@ export const getCalendarEvents = async (
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
} else if (connection.provider === 'synology') {
|
||||||
|
const [username, password] = connection.accessToken.split(':');
|
||||||
|
const serverUrl = connection.refreshToken;
|
||||||
|
|
||||||
|
if (!username || !password || !serverUrl) {
|
||||||
|
console.error('[CALENDAR] Invalid Synology credentials format');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use stored calendar selection (same pattern as Outlook/Google)
|
||||||
|
let calendarIds: string[] = [];
|
||||||
|
let calendars: any[] = [];
|
||||||
|
|
||||||
|
if (connection.calendars && Array.isArray(connection.calendars) && connection.calendars.length > 0) {
|
||||||
|
calendars = connection.calendars as any[];
|
||||||
|
calendarIds = calendars
|
||||||
|
.filter((c: any) => c.selected !== false)
|
||||||
|
.map((c: any) => c.id);
|
||||||
|
console.log('[CALENDAR] Synology: using stored calendars, selected:', calendarIds.length, 'of', calendars.length);
|
||||||
|
} else {
|
||||||
|
// Fallback: fetch fresh from server if no stored calendars
|
||||||
|
console.log('[CALENDAR] Synology: no stored calendars, fetching from server...');
|
||||||
|
const fresh = await getSynologyCalendars(serverUrl, username, password);
|
||||||
|
calendars = fresh;
|
||||||
|
calendarIds = fresh.map(c => c.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarIds.length === 0) {
|
||||||
|
console.log('[CALENDAR] Synology: no calendars selected, skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const calendarId of calendarIds) {
|
||||||
|
try {
|
||||||
|
const calendarEvents = await getSynologyEvents(
|
||||||
|
serverUrl,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
calendarId,
|
||||||
|
timeMin,
|
||||||
|
timeMax
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[CALENDAR] Synology: fetched ${calendarEvents.length} events from calendar ${calendarId}`);
|
||||||
|
const calendarData = calendars.find(c => c.id === calendarId);
|
||||||
|
|
||||||
|
events = events.concat(calendarEvents.map((event: any) => {
|
||||||
|
const isDateOnly = (s: string) => s && !s.includes('T');
|
||||||
|
const startIsAllDay = isDateOnly(event.startDate);
|
||||||
|
return {
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
start: {
|
||||||
|
dateTime: startIsAllDay ? undefined : event.startDate,
|
||||||
|
date: startIsAllDay ? event.startDate : undefined,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: startIsAllDay ? undefined : event.endDate,
|
||||||
|
date: startIsAllDay ? event.endDate : undefined,
|
||||||
|
},
|
||||||
|
location: event.location,
|
||||||
|
url: event.url,
|
||||||
|
recurringEventId: event.recurringEventId,
|
||||||
|
isRecurring: event.isRecurring,
|
||||||
|
source: 'synology' as const,
|
||||||
|
calendarId,
|
||||||
|
calendarTitle: calendarData?.title || 'Synology Calendar',
|
||||||
|
backgroundColor: calendarData?.color || '#1b85ff'
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
} catch (calError) {
|
||||||
|
console.error(`[CALENDAR] Synology: error fetching events from calendar ${calendarId}:`, calError);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (connection.provider === 'outlook') {
|
} else if (connection.provider === 'outlook') {
|
||||||
console.log('[CALENDAR] Processing Outlook connection:', connection.id);
|
console.log('[CALENDAR] Processing Outlook connection:', connection.id);
|
||||||
|
|
||||||
@ -643,13 +718,44 @@ export const createCalendarEvent = async (
|
|||||||
id: createdEvent.id,
|
id: createdEvent.id,
|
||||||
title: createdEvent.title,
|
title: createdEvent.title,
|
||||||
description: createdEvent.description,
|
description: createdEvent.description,
|
||||||
start: { dateTime: createdEvent.startDate }, // Mapping back might need adjustment if date-only
|
start: { dateTime: createdEvent.startDate },
|
||||||
end: { dateTime: createdEvent.endDate },
|
end: { dateTime: createdEvent.endDate },
|
||||||
location: createdEvent.location,
|
location: createdEvent.location,
|
||||||
source: 'apple',
|
source: 'apple',
|
||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: '', // Fetch if needed
|
calendarTitle: '', // Fetch if needed
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
|
} else if (connection.provider === 'synology') {
|
||||||
|
const [username, password] = connection.accessToken.split(':');
|
||||||
|
const serverUrl = connection.refreshToken;
|
||||||
|
|
||||||
|
if (!username || !password || !serverUrl) throw new Error('Invalid Synology credentials');
|
||||||
|
if (!event.title) throw new Error('Event title is required');
|
||||||
|
if (!event.start || !event.end) throw new Error('Event start and end times are required');
|
||||||
|
|
||||||
|
const createdEvent = await import('./synology-calendar').then(m =>
|
||||||
|
m.createEvent(serverUrl, username, password, calendarId, {
|
||||||
|
title: event.title!,
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
url: event.url,
|
||||||
|
recurrence: event.recurrence,
|
||||||
|
start: event.start!,
|
||||||
|
end: event.end!
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: createdEvent.id,
|
||||||
|
title: createdEvent.title,
|
||||||
|
description: createdEvent.description,
|
||||||
|
start: { dateTime: createdEvent.startDate },
|
||||||
|
end: { dateTime: createdEvent.endDate },
|
||||||
|
location: createdEvent.location,
|
||||||
|
source: 'synology',
|
||||||
|
calendarId,
|
||||||
|
calendarTitle: '',
|
||||||
|
} as CalendarEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Provider ${connection.provider} does not support creating events yet.`);
|
throw new Error(`Provider ${connection.provider} does not support creating events yet.`);
|
||||||
@ -768,6 +874,35 @@ export const updateCalendarEvent = async (
|
|||||||
calendarId,
|
calendarId,
|
||||||
calendarTitle: '',
|
calendarTitle: '',
|
||||||
} as CalendarEvent;
|
} as CalendarEvent;
|
||||||
|
} else if (connection.provider === 'synology') {
|
||||||
|
const [username, password] = connection.accessToken.split(':');
|
||||||
|
const serverUrl = connection.refreshToken;
|
||||||
|
|
||||||
|
if (!username || !password || !serverUrl) throw new Error('Invalid Synology credentials');
|
||||||
|
|
||||||
|
const updatedEvent = await import('./synology-calendar').then(m =>
|
||||||
|
m.updateEvent(serverUrl, username, password, calendarId, eventId, {
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
url: event.url,
|
||||||
|
start: event.start,
|
||||||
|
end: event.end
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updatedEvent.id,
|
||||||
|
title: updatedEvent.title,
|
||||||
|
description: updatedEvent.description,
|
||||||
|
start: { dateTime: updatedEvent.startDate },
|
||||||
|
end: { dateTime: updatedEvent.endDate },
|
||||||
|
location: updatedEvent.location,
|
||||||
|
url: updatedEvent.url,
|
||||||
|
source: 'synology',
|
||||||
|
calendarId,
|
||||||
|
calendarTitle: '',
|
||||||
|
} as CalendarEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Provider ${connection.provider} does not support updating events yet.`);
|
throw new Error(`Provider ${connection.provider} does not support updating events yet.`);
|
||||||
@ -823,6 +958,16 @@ export const deleteCalendarEvent = async (
|
|||||||
m.deleteEvent(email, appPassword, calendarId, eventId)
|
m.deleteEvent(email, appPassword, calendarId, eventId)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
} else if (connection.provider === 'synology') {
|
||||||
|
const [username, password] = connection.accessToken.split(':');
|
||||||
|
const serverUrl = connection.refreshToken;
|
||||||
|
|
||||||
|
if (!username || !password || !serverUrl) throw new Error('Invalid Synology credentials');
|
||||||
|
|
||||||
|
await import('./synology-calendar').then(m =>
|
||||||
|
m.deleteEvent(serverUrl, username, password, calendarId, eventId)
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Provider ${connection.provider} does not support deleting events yet.`);
|
throw new Error(`Provider ${connection.provider} does not support deleting events yet.`);
|
||||||
|
|||||||
548
src/lib/synology-calendar.ts
Normal file
548
src/lib/synology-calendar.ts
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
import { DAVClient } from 'tsdav';
|
||||||
|
import ICAL from 'ical.js';
|
||||||
|
|
||||||
|
export interface SynologyCalendarEvent {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
url?: string;
|
||||||
|
recurringEventId?: string;
|
||||||
|
isRecurring?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SynologyCalendar {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
color?: string;
|
||||||
|
isPrimary?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a configured DAV client for Synology Calendar
|
||||||
|
*/
|
||||||
|
const createClient = (serverUrl: string, username: string, password: string) => {
|
||||||
|
let url = serverUrl.replace(/\/$/, '');
|
||||||
|
if (!url.includes('/caldav')) {
|
||||||
|
url = `${url}/caldav/${username}`;
|
||||||
|
}
|
||||||
|
return new DAVClient({
|
||||||
|
serverUrl: url,
|
||||||
|
credentials: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
authMethod: 'Basic',
|
||||||
|
defaultAccountType: 'caldav',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate credentials by attempting to fetch calendars
|
||||||
|
* @returns List of found calendars if successful
|
||||||
|
*/
|
||||||
|
export const validateCredentials = async (serverUrl: string, username: string, password: string): Promise<SynologyCalendar[]> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
|
||||||
|
// Filter to VEVENT-only calendars — exclude VTODO (Reminders) collections
|
||||||
|
const eventCalendars = calendars.filter(cal => {
|
||||||
|
const components: string[] = (cal as any).components || [];
|
||||||
|
// Keep if components include VEVENT, or if components is empty/undefined
|
||||||
|
if (components.length === 0) return true;
|
||||||
|
return components.includes('VEVENT');
|
||||||
|
});
|
||||||
|
|
||||||
|
const mappedCalendars = eventCalendars.map(cal => ({
|
||||||
|
id: cal.url, // Using URL as ID for CalDAV
|
||||||
|
title: (cal.displayName as string) || 'Untitled Calendar',
|
||||||
|
color: cal.calendarColor,
|
||||||
|
isPrimary: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('[SYNOLOGY CALENDAR] Found calendars:', mappedCalendars.length, '(filtered from', calendars.length, 'total)');
|
||||||
|
return mappedCalendars;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Synology Calendar validation failed:', error);
|
||||||
|
throw new Error('Invalid credentials or unable to connect to Synology Server.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user calendars (wrapper around validateCredentials for now as they do the same)
|
||||||
|
*/
|
||||||
|
export const getUserCalendars = validateCredentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upcoming events for a specified time period from a specific calendar
|
||||||
|
*/
|
||||||
|
export const getUpcomingEvents = async (
|
||||||
|
serverUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
calendarUrl: string,
|
||||||
|
timeMin: string,
|
||||||
|
timeMax: string
|
||||||
|
): Promise<SynologyCalendarEvent[]> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
const getPath = (url: string) => {
|
||||||
|
try { return new URL(url).pathname; } catch { return url; }
|
||||||
|
};
|
||||||
|
const targetPath = getPath(calendarUrl);
|
||||||
|
const targetCalendar = calendars.find(c => getPath(c.url) === targetPath);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Calendar not found: ${calendarUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await client.fetchCalendarObjects({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
timeRange: {
|
||||||
|
start: new Date(timeMin).toISOString(),
|
||||||
|
end: new Date(timeMax).toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedEvents: SynologyCalendarEvent[] = [];
|
||||||
|
const minTime = new Date(timeMin).getTime();
|
||||||
|
const maxTime = new Date(timeMax).getTime();
|
||||||
|
|
||||||
|
events.forEach(eventObj => {
|
||||||
|
const data = (eventObj as any).data;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jcalData = ICAL.parse(data);
|
||||||
|
const comp = new ICAL.Component(jcalData);
|
||||||
|
const vevents = comp.getAllSubcomponents('vevent');
|
||||||
|
|
||||||
|
// Separate base events (with RRULE) from recurrence exceptions (with RECURRENCE-ID)
|
||||||
|
const baseEvents: any[] = [];
|
||||||
|
const exceptions: Map<string, any[]> = new Map();
|
||||||
|
|
||||||
|
vevents.forEach((vevent: any) => {
|
||||||
|
const recurrenceId = vevent.getFirstPropertyValue('recurrence-id');
|
||||||
|
if (recurrenceId) {
|
||||||
|
const event = new ICAL.Event(vevent);
|
||||||
|
const uid = event.uid;
|
||||||
|
if (!exceptions.has(uid)) exceptions.set(uid, []);
|
||||||
|
exceptions.get(uid)!.push(vevent);
|
||||||
|
} else {
|
||||||
|
baseEvents.push(vevent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
baseEvents.forEach((vevent: any) => {
|
||||||
|
const event = new ICAL.Event(vevent);
|
||||||
|
const rrule = vevent.getFirstPropertyValue('rrule');
|
||||||
|
|
||||||
|
if (rrule) {
|
||||||
|
const uid = event.uid;
|
||||||
|
const exceptionVevents = exceptions.get(uid) || [];
|
||||||
|
const exceptionDates = new Set<string>();
|
||||||
|
const isAllDayRecurring = event.startDate.isDate === true;
|
||||||
|
|
||||||
|
exceptionVevents.forEach((exVevent: any) => {
|
||||||
|
const exEvent = new ICAL.Event(exVevent);
|
||||||
|
const recId = exVevent.getFirstPropertyValue('recurrence-id');
|
||||||
|
if (recId) {
|
||||||
|
exceptionDates.add(recId.toJSDate().toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const exStart = exEvent.startDate.toJSDate();
|
||||||
|
const exEnd = exEvent.endDate.toJSDate();
|
||||||
|
const exIsAllDay = exEvent.startDate.isDate === true;
|
||||||
|
|
||||||
|
if (exEnd.getTime() >= minTime && exStart.getTime() <= maxTime) {
|
||||||
|
parsedEvents.push({
|
||||||
|
id: `caldav::${eventObj.url}::${exEvent.uid}::${exStart.toISOString()}`,
|
||||||
|
title: exEvent.summary || 'Untitled Event',
|
||||||
|
startDate: exIsAllDay ? exStart.toISOString().slice(0, 10) : exStart.toISOString(),
|
||||||
|
endDate: exIsAllDay ? exEnd.toISOString().slice(0, 10) : exEnd.toISOString(),
|
||||||
|
description: exEvent.description,
|
||||||
|
location: exEvent.location,
|
||||||
|
url: exVevent.getFirstPropertyValue('url')?.toString() || undefined,
|
||||||
|
recurringEventId: exEvent.uid,
|
||||||
|
isRecurring: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const duration = event.endDate.toJSDate().getTime() - event.startDate.toJSDate().getTime();
|
||||||
|
const iter = event.iterator();
|
||||||
|
let next;
|
||||||
|
let safetyCount = 0;
|
||||||
|
|
||||||
|
while ((next = iter.next()) && safetyCount < 500) {
|
||||||
|
safetyCount++;
|
||||||
|
const occStart = next.toJSDate();
|
||||||
|
const occEnd = new Date(occStart.getTime() + duration);
|
||||||
|
|
||||||
|
if (occStart.getTime() > maxTime) break;
|
||||||
|
if (occEnd.getTime() < minTime) continue;
|
||||||
|
if (exceptionDates.has(occStart.toISOString())) continue;
|
||||||
|
|
||||||
|
parsedEvents.push({
|
||||||
|
id: `caldav::${eventObj.url}::${event.uid}::${occStart.toISOString()}`,
|
||||||
|
title: event.summary || 'Untitled Event',
|
||||||
|
startDate: isAllDayRecurring ? occStart.toISOString().slice(0, 10) : occStart.toISOString(),
|
||||||
|
endDate: isAllDayRecurring ? occEnd.toISOString().slice(0, 10) : occEnd.toISOString(),
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
url: vevent.getFirstPropertyValue('url')?.toString() || undefined,
|
||||||
|
recurringEventId: event.uid,
|
||||||
|
isRecurring: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (expandErr: any) {
|
||||||
|
console.error(`[SYNOLOGY CALENDAR] Error expanding recurrence for "${event.summary}":`, expandErr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const start = event.startDate.toJSDate();
|
||||||
|
const end = event.endDate.toJSDate();
|
||||||
|
const isAllDay = event.startDate.isDate === true;
|
||||||
|
|
||||||
|
if (end.getTime() < minTime || start.getTime() > maxTime) return;
|
||||||
|
|
||||||
|
parsedEvents.push({
|
||||||
|
id: `caldav::${eventObj.url}::${event.uid || 'unknown'}`,
|
||||||
|
title: event.summary || 'Untitled Event',
|
||||||
|
startDate: isAllDay ? start.toISOString().slice(0, 10) : start.toISOString(),
|
||||||
|
endDate: isAllDay ? end.toISOString().slice(0, 10) : end.toISOString(),
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
url: vevent.getFirstPropertyValue('url')?.toString() || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (parseErr: any) {
|
||||||
|
console.error(`[SYNOLOGY CALENDAR] Error parsing event data for calendar ${calendarUrl}:`, parseErr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsedEvents;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[SYNOLOGY CALENDAR] Error fetching events for ${calendarUrl}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event in the specified calendar
|
||||||
|
*/
|
||||||
|
export const createEvent = async (
|
||||||
|
serverUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
calendarUrl: string,
|
||||||
|
eventData: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
url?: string;
|
||||||
|
recurrence?: string;
|
||||||
|
start: { dateTime?: string; date?: string };
|
||||||
|
end: { dateTime?: string; date?: string };
|
||||||
|
}
|
||||||
|
): Promise<SynologyCalendarEvent> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Calendar not found: ${calendarUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const uid = crypto.randomUUID();
|
||||||
|
|
||||||
|
const dtStamp = now.toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';
|
||||||
|
|
||||||
|
let dtStart = '';
|
||||||
|
let dtEnd = '';
|
||||||
|
let dtStartParam = '';
|
||||||
|
let dtEndParam = '';
|
||||||
|
|
||||||
|
if (eventData.start.date) {
|
||||||
|
dtStart = eventData.start.date.replace(/-/g, '');
|
||||||
|
dtEnd = eventData.end.date ? eventData.end.date.replace(/-/g, '') : dtStart; // Fallback
|
||||||
|
dtStartParam = ';VALUE=DATE';
|
||||||
|
dtEndParam = ';VALUE=DATE';
|
||||||
|
} else if (eventData.start.dateTime) {
|
||||||
|
dtStart = new Date(eventData.start.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z';
|
||||||
|
dtEnd = eventData.end.dateTime
|
||||||
|
? new Date(eventData.end.dateTime).toISOString().replace(/[-:.]/g, '').substring(0, 15) + 'Z'
|
||||||
|
: dtStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = eventData.description ? `DESCRIPTION:${eventData.description.replace(/\n/g, '\\n')}\r\n` : '';
|
||||||
|
const location = eventData.location ? `LOCATION:${eventData.location.replace(/,/g, '\\,')}\r\n` : '';
|
||||||
|
const url = eventData.url ? `URL:${eventData.url}\r\n` : '';
|
||||||
|
let rruleLine = '';
|
||||||
|
if (eventData.recurrence) {
|
||||||
|
const rruleMap: Record<string, string> = {
|
||||||
|
daily: 'RRULE:FREQ=DAILY',
|
||||||
|
weekly: 'RRULE:FREQ=WEEKLY',
|
||||||
|
biweekly: 'RRULE:FREQ=WEEKLY;INTERVAL=2',
|
||||||
|
monthly: 'RRULE:FREQ=MONTHLY',
|
||||||
|
yearly: 'RRULE:FREQ=YEARLY',
|
||||||
|
};
|
||||||
|
if (rruleMap[eventData.recurrence]) {
|
||||||
|
rruleLine = `${rruleMap[eventData.recurrence]}\r\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const iCalString = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//My Weekly ToDo List//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:${uid}
|
||||||
|
DTSTAMP:${dtStamp}
|
||||||
|
DTSTART${dtStartParam}:${dtStart}
|
||||||
|
DTEND${dtEndParam}:${dtEnd}
|
||||||
|
SUMMARY:${eventData.title}
|
||||||
|
${description}${location}${url}${rruleLine}END:VEVENT
|
||||||
|
END:VCALENDAR`;
|
||||||
|
|
||||||
|
console.log('[SYNOLOGY CALENDAR] Creating event with iCal:', iCalString);
|
||||||
|
|
||||||
|
const filename = `${uid}.ics`;
|
||||||
|
|
||||||
|
await client.createCalendarObject({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
filename,
|
||||||
|
iCalString
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${uid}-${filename}`,
|
||||||
|
title: eventData.title,
|
||||||
|
startDate: eventData.start.dateTime || eventData.start.date || '',
|
||||||
|
endDate: eventData.end.dateTime || eventData.end.date || '',
|
||||||
|
description: eventData.description,
|
||||||
|
location: eventData.location,
|
||||||
|
url: eventData.url
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SYNOLOGY CALENDAR] Error creating event:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the new caldav:: ID format to extract the object URL and UID.
|
||||||
|
*/
|
||||||
|
function parseCaldavId(eventId: string): { objectUrl: string; uid: string } | null {
|
||||||
|
if (!eventId.startsWith('caldav::')) return null;
|
||||||
|
const parts = eventId.split('::');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
return { objectUrl: parts[1], uid: parts[2] };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing event
|
||||||
|
*/
|
||||||
|
export const updateEvent = async (
|
||||||
|
serverUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
calendarUrl: string,
|
||||||
|
eventId: string,
|
||||||
|
eventData: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
location?: string;
|
||||||
|
url?: string;
|
||||||
|
start?: { dateTime?: string; date?: string };
|
||||||
|
end?: { dateTime?: string; date?: string };
|
||||||
|
}
|
||||||
|
): Promise<SynologyCalendarEvent> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
let targetObject: any = null;
|
||||||
|
|
||||||
|
const parsed = parseCaldavId(eventId);
|
||||||
|
if (parsed) {
|
||||||
|
const objects = await client.fetchCalendarObjects({
|
||||||
|
calendar: { url: calendarUrl } as any,
|
||||||
|
objectUrls: [parsed.objectUrl],
|
||||||
|
});
|
||||||
|
targetObject = objects?.[0] || null;
|
||||||
|
if (!targetObject) {
|
||||||
|
throw new Error('Event not found on server at URL: ' + parsed.objectUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Calendar not found: ${calendarUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = eventId.split('-')[0];
|
||||||
|
|
||||||
|
const allObjects = await client.fetchCalendarObjects({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
});
|
||||||
|
|
||||||
|
targetObject = allObjects.find(obj => {
|
||||||
|
if (obj.data) {
|
||||||
|
return obj.data.includes(`UID:${uid}`);
|
||||||
|
}
|
||||||
|
return obj.url.includes(uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetObject) {
|
||||||
|
throw new Error('Event not found on server');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jcal = ICAL.parse(targetObject.data);
|
||||||
|
const comp = new ICAL.Component(jcal);
|
||||||
|
const vevent = comp.getFirstSubcomponent('vevent');
|
||||||
|
|
||||||
|
if (!vevent) {
|
||||||
|
throw new Error('No VEVENT found in calendar object');
|
||||||
|
}
|
||||||
|
const event = new ICAL.Event(vevent);
|
||||||
|
|
||||||
|
if (eventData.title) event.summary = eventData.title;
|
||||||
|
if (eventData.description) event.description = eventData.description;
|
||||||
|
if (eventData.location) event.location = eventData.location;
|
||||||
|
|
||||||
|
if (eventData.url !== undefined) {
|
||||||
|
if (eventData.url) {
|
||||||
|
vevent.updatePropertyWithValue('url', eventData.url);
|
||||||
|
} else {
|
||||||
|
vevent.removeProperty('url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.start) {
|
||||||
|
if (eventData.start.date) {
|
||||||
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
||||||
|
event.startDate.isDate = true;
|
||||||
|
} else if (eventData.start.dateTime) {
|
||||||
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
||||||
|
event.startDate.isDate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.end) {
|
||||||
|
if (eventData.end.date) {
|
||||||
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.date), true);
|
||||||
|
event.endDate.isDate = true;
|
||||||
|
} else if (eventData.end.dateTime) {
|
||||||
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
||||||
|
event.endDate.isDate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.sequence = (event.sequence || 0) + 1;
|
||||||
|
if (vevent) {
|
||||||
|
vevent.updatePropertyWithValue('dtstamp', ICAL.Time.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedIcalString = comp.toString();
|
||||||
|
console.log('[SYNOLOGY CALENDAR] Updating event with iCal:', updatedIcalString);
|
||||||
|
|
||||||
|
await client.updateObject({
|
||||||
|
url: targetObject.url,
|
||||||
|
data: updatedIcalString,
|
||||||
|
etag: targetObject.etag
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: eventId,
|
||||||
|
title: event.summary,
|
||||||
|
startDate: event.startDate.toString(),
|
||||||
|
endDate: event.endDate.toString(),
|
||||||
|
description: event.description,
|
||||||
|
location: event.location,
|
||||||
|
url: vevent.getFirstPropertyValue('url')?.toString() || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SYNOLOGY CALENDAR] Error updating event:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an event
|
||||||
|
*/
|
||||||
|
export const deleteEvent = async (
|
||||||
|
serverUrl: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
calendarUrl: string,
|
||||||
|
eventId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const parsed = parseCaldavId(eventId);
|
||||||
|
if (parsed) {
|
||||||
|
await client.deleteObject({
|
||||||
|
url: parsed.objectUrl,
|
||||||
|
etag: undefined,
|
||||||
|
} as any);
|
||||||
|
console.log('[SYNOLOGY CALENDAR] Event deleted via direct URL (O(1))');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
const targetCalendar = calendars.find(c => c.url === calendarUrl);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`Calendar not found: ${calendarUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = eventId.split('-')[0];
|
||||||
|
|
||||||
|
const allObjects = await client.fetchCalendarObjects({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetObject = allObjects.find(obj => {
|
||||||
|
if (obj.data) {
|
||||||
|
return obj.data.includes(`UID:${uid}`);
|
||||||
|
}
|
||||||
|
return obj.url.includes(uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetObject) {
|
||||||
|
console.warn('[SYNOLOGY CALENDAR] Event to delete not found, maybe already deleted?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.deleteObject({
|
||||||
|
url: targetObject.url,
|
||||||
|
etag: targetObject.etag
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
console.log('[SYNOLOGY CALENDAR] Event deleted via legacy scan');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SYNOLOGY CALENDAR] Error deleting event:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
157
src/lib/synology-tasks.ts
Normal file
157
src/lib/synology-tasks.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { DAVClient } from 'tsdav';
|
||||||
|
import ICAL from 'ical.js';
|
||||||
|
|
||||||
|
export interface SynologyReminderList {
|
||||||
|
id: string; // URL
|
||||||
|
title: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a configured DAV client for Synology Tasks
|
||||||
|
*/
|
||||||
|
const createClient = (serverUrl: string, username: string, password: string) => {
|
||||||
|
let url = serverUrl.replace(/\/$/, '');
|
||||||
|
if (!url.includes('/caldav')) {
|
||||||
|
url = `${url}/caldav/${username}`;
|
||||||
|
}
|
||||||
|
return new DAVClient({
|
||||||
|
serverUrl: url,
|
||||||
|
credentials: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
authMethod: 'Basic',
|
||||||
|
defaultAccountType: 'caldav',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all reminder lists (VTODO collections) from Synology
|
||||||
|
*/
|
||||||
|
export const fetchSynologyReminderLists = async (serverUrl: string, username: string, password: string): Promise<SynologyReminderList[]> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
|
||||||
|
// Filter to VTODO collections (Reminders)
|
||||||
|
const reminderLists = calendars.filter(cal => {
|
||||||
|
const components: string[] = (cal as any).components || [];
|
||||||
|
return components.includes('VTODO');
|
||||||
|
});
|
||||||
|
|
||||||
|
return reminderLists.map(cal => ({
|
||||||
|
id: cal.url,
|
||||||
|
title: (cal.displayName as string) || 'Untitled List',
|
||||||
|
color: (cal as any).calendarColor,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Synology reminder lists:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new reminder list (collection)
|
||||||
|
*/
|
||||||
|
export const createSynologyReminderList = async (serverUrl: string, username: string, password: string, title: string): Promise<SynologyReminderList> => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
if (calendars.length === 0) {
|
||||||
|
throw new Error('No existing Synology calendars found to derive base URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstCalUrl = calendars[0].url;
|
||||||
|
const urlParts = firstCalUrl.split('/');
|
||||||
|
if (firstCalUrl.endsWith('/')) urlParts.pop();
|
||||||
|
urlParts.pop();
|
||||||
|
const baseWebDavUrl = urlParts.join('/') + '/';
|
||||||
|
|
||||||
|
const newId = crypto.randomUUID();
|
||||||
|
const newCalendarUrl = `${baseWebDavUrl}${newId}/`;
|
||||||
|
|
||||||
|
await (client as any).makeCalendar({
|
||||||
|
url: newCalendarUrl,
|
||||||
|
props: {
|
||||||
|
'displayname': title,
|
||||||
|
'supported-calendar-component-set': {
|
||||||
|
'comp': { _attributes: { name: 'VTODO' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newCalendarUrl,
|
||||||
|
title: title,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating Synology reminder list:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch tasks (VTODOs) from a specific Synology list
|
||||||
|
*/
|
||||||
|
export const fetchSynologyTasks = async (serverUrl: string, username: string, password: string, listId: string) => {
|
||||||
|
try {
|
||||||
|
const client = createClient(serverUrl, username, password);
|
||||||
|
await client.login();
|
||||||
|
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
// Match by URL path only (ignoring host/port differences that can arise
|
||||||
|
// when the stored URL was saved before the port was included in the server URL)
|
||||||
|
const getPath = (url: string) => {
|
||||||
|
try { return new URL(url).pathname; } catch { return url; }
|
||||||
|
};
|
||||||
|
const targetPath = getPath(listId);
|
||||||
|
const targetCalendar = calendars.find(c => getPath(c.url) === targetPath);
|
||||||
|
|
||||||
|
if (!targetCalendar) {
|
||||||
|
throw new Error(`List not found: ${listId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objects = await client.fetchCalendarObjects({
|
||||||
|
calendar: targetCalendar,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedTasks: any[] = [];
|
||||||
|
|
||||||
|
objects.forEach(obj => {
|
||||||
|
const data = (obj as any).data;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jcalData = ICAL.parse(data);
|
||||||
|
const comp = new ICAL.Component(jcalData);
|
||||||
|
const vtodos = comp.getAllSubcomponents('vtodo');
|
||||||
|
|
||||||
|
vtodos.forEach((vtodo: any) => {
|
||||||
|
const status = vtodo.getFirstPropertyValue('status') || 'NEEDS-ACTION';
|
||||||
|
const isCompleted = status === 'COMPLETED';
|
||||||
|
const dueProp = vtodo.getFirstProperty('due');
|
||||||
|
|
||||||
|
parsedTasks.push({
|
||||||
|
id: `synology::${vtodo.getFirstPropertyValue('uid')}`,
|
||||||
|
title: vtodo.getFirstPropertyValue('summary') || 'Untitled Task',
|
||||||
|
notes: vtodo.getFirstPropertyValue('description') || '',
|
||||||
|
status: isCompleted ? 'completed' : 'needsAction',
|
||||||
|
due: dueProp ? dueProp.getFirstValue().toJSDate() : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[SYNOLOGY TASKS] Error parsing VTODO:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsedTasks;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SYNOLOGY TASKS] Error fetching tasks:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
25
test-syno.js
Normal file
25
test-syno.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const { DAVClient } = require('tsdav');
|
||||||
|
|
||||||
|
async function test() {
|
||||||
|
const client = new DAVClient({
|
||||||
|
serverUrl: 'https://ds.martin-bierschenk.de:91/caldav/martin',
|
||||||
|
credentials: {
|
||||||
|
username: 'martin',
|
||||||
|
password: 'DS916-MB-ImC.2019',
|
||||||
|
},
|
||||||
|
authMethod: 'Basic',
|
||||||
|
defaultAccountType: 'caldav',
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.login();
|
||||||
|
console.log("Logged in:", client.account);
|
||||||
|
const calendars = await client.fetchCalendars();
|
||||||
|
console.log('Calendars count:', calendars.length);
|
||||||
|
calendars.forEach((c, idx) => {
|
||||||
|
console.log(`\nCalendar ${idx + 1}:`, c.displayName);
|
||||||
|
console.log('url:', c.url);
|
||||||
|
console.log('components:', c.components);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test().catch(console.error);
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user