├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── .gitignore ├── .postcssrc ├── LICENSE.md ├── README.md ├── icon.svg ├── package-lock.json ├── package.json ├── screenshots ├── demo.gif └── demo2.gif ├── src ├── App.css ├── RecurCard.tsx ├── Recurrence.tsx ├── handleClosePopup.ts ├── index.html ├── index.tsx └── tailwind.css └── tailwind.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [hkgnp] 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build plugin 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10 8 | 9 | env: 10 | PLUGIN_NAME: ${{ github.event.repository.name }} 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '16.x' # You might need to adjust this value to your own version 22 | - name: Build 23 | id: build 24 | run: | 25 | npm i && npm run build 26 | mkdir ${{ env.PLUGIN_NAME }} 27 | cp README.md package.json icon.svg ${{ env.PLUGIN_NAME }} 28 | mv dist ${{ env.PLUGIN_NAME }} 29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 30 | ls 31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 32 | - name: Create Release 33 | uses: ncipollo/release-action@v1 34 | id: create_release 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | VERSION: ${{ github.ref }} 38 | with: 39 | allowUpdates: true 40 | draft: false 41 | prerelease: false 42 | 43 | - name: Upload zip file 44 | id: upload_zip 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} 50 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 51 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 52 | asset_content_type: application/zip 53 | 54 | - name: Upload package.json 55 | id: upload_metadata 56 | uses: actions/upload-release-asset@v1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | upload_url: ${{ steps.create_release.outputs.upload_url }} 61 | asset_path: ./package.json 62 | asset_name: package.json 63 | asset_content_type: application/json 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | .env.production 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | .parcel-cache 81 | 82 | # Next.js build output 83 | .next 84 | out 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # yarn v2 115 | .yarn/cache 116 | .yarn/unplugged 117 | .yarn/build-state.yml 118 | .yarn/install-state.gz 119 | .pnp.* -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | "tailwindcss": {}, 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 hkgnp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [[:gift_heart: Sponsor this project on Github](https://github.com/sponsors/hkgnp) or [:coffee: Get me a coffee](https://www.buymeacoffee.com/hkgnp.dev) if you like this plugin! 2 | 3 | # Overview 4 | 5 | This plugin allows you to quickly add recurring blocks based on your desired recurrence. It also allows you to delete inserted recurring blocks as well! 6 | 7 | When used with the [logseq-trackhabits2-plugin](https://github.com/hkgnp/logseq-trackhabits2-plugin), you can easily create habits to track! 8 | 9 | ![](/screenshots/demo.gif) 10 | 11 | ![](/screenshots/demo2.gif) 12 | 13 | # Installation 14 | 15 | If not in the marketplace, [download the release](https://github.com/hkgnp/logseq-recurrence-plugin/releases) and manually load it in Logseq. 16 | 17 | # Usage 18 | 19 | 1. Create the block that you would want to recur. 20 | 2. At the end of the line, type `/Set recurrence`. 21 | 3. Choose the desired recurrence options. 22 | 4. Click `Create Blocks` and you are done! 23 | 24 | # Credits 25 | 26 | - [DayJS](https://day.js.org/) 27 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "logseq": { 3 | "id": "logseq-recurrence-plugin", 4 | "title": "logseq-recurrence-plugin", 5 | "icon": "./icon.svg" 6 | }, 7 | "name": "logseq-recurrence-plugin", 8 | "version": "1.0.7", 9 | "description": "", 10 | "main": "dist/index.html", 11 | "targets": { 12 | "main": false 13 | }, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "build": "postcss src/tailwind.css -o src/App.css && parcel build --no-source-maps src/index.html --public-url ./" 17 | }, 18 | "keywords": [], 19 | "author": "hkgnp", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@logseq/libs": "^0.0.1-alpha.34", 23 | "autoprefix": "^1.0.1", 24 | "dayjs": "^1.10.7", 25 | "logseq-dateutils": "^0.0.12", 26 | "postcss-cli": "^9.1.0", 27 | "postcss-import": "^14.0.2", 28 | "react": "^17.0.2", 29 | "react-datepicker": "^4.6.0", 30 | "react-dom": "^17.0.2", 31 | "tailwindcss": "^3.0.15" 32 | }, 33 | "devDependencies": { 34 | "@types/react": "^17.0.38", 35 | "@types/react-datepicker": "^4.4.0", 36 | "@types/react-dom": "^17.0.11", 37 | "parcel": "^2.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /screenshots/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-recurrence-plugin/e4d8ea01cc3f55cf48faa659b899304cd2575d7a/screenshots/demo.gif -------------------------------------------------------------------------------- /screenshots/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-recurrence-plugin/e4d8ea01cc3f55cf48faa659b899304cd2575d7a/screenshots/demo2.gif -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.0.19 | MIT License | https://tailwindcss.com 3 | *//* 4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 6 | */ 7 | 8 | *, 9 | ::before, 10 | ::after { 11 | box-sizing: border-box; /* 1 */ 12 | border-width: 0; /* 2 */ 13 | border-style: solid; /* 2 */ 14 | border-color: #e5e7eb; /* 2 */ 15 | } 16 | 17 | ::before, 18 | ::after { 19 | --tw-content: ''; 20 | } 21 | 22 | /* 23 | 1. Use a consistent sensible line-height in all browsers. 24 | 2. Prevent adjustments of font size after orientation changes in iOS. 25 | 3. Use a more readable tab size. 26 | 4. Use the user's configured `sans` font-family by default. 27 | */ 28 | 29 | html { 30 | line-height: 1.5; /* 1 */ 31 | -webkit-text-size-adjust: 100%; /* 2 */ 32 | -moz-tab-size: 4; /* 3 */ 33 | -o-tab-size: 4; 34 | tab-size: 4; /* 3 */ 35 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ 36 | } 37 | 38 | /* 39 | 1. Remove the margin in all browsers. 40 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 41 | */ 42 | 43 | body { 44 | margin: 0; /* 1 */ 45 | line-height: inherit; /* 2 */ 46 | } 47 | 48 | /* 49 | 1. Add the correct height in Firefox. 50 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 51 | 3. Ensure horizontal rules are visible by default. 52 | */ 53 | 54 | hr { 55 | height: 0; /* 1 */ 56 | color: inherit; /* 2 */ 57 | border-top-width: 1px; /* 3 */ 58 | } 59 | 60 | /* 61 | Add the correct text decoration in Chrome, Edge, and Safari. 62 | */ 63 | 64 | abbr:where([title]) { 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | } 68 | 69 | /* 70 | Remove the default font size and weight for headings. 71 | */ 72 | 73 | h1, 74 | h2, 75 | h3, 76 | h4, 77 | h5, 78 | h6 { 79 | font-size: inherit; 80 | font-weight: inherit; 81 | } 82 | 83 | /* 84 | Reset links to optimize for opt-in styling instead of opt-out. 85 | */ 86 | 87 | a { 88 | color: inherit; 89 | text-decoration: inherit; 90 | } 91 | 92 | /* 93 | Add the correct font weight in Edge and Safari. 94 | */ 95 | 96 | b, 97 | strong { 98 | font-weight: bolder; 99 | } 100 | 101 | /* 102 | 1. Use the user's configured `mono` font family by default. 103 | 2. Correct the odd `em` font sizing in all browsers. 104 | */ 105 | 106 | code, 107 | kbd, 108 | samp, 109 | pre { 110 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 111 | font-size: 1em; /* 2 */ 112 | } 113 | 114 | /* 115 | Add the correct font size in all browsers. 116 | */ 117 | 118 | small { 119 | font-size: 80%; 120 | } 121 | 122 | /* 123 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 124 | */ 125 | 126 | sub, 127 | sup { 128 | font-size: 75%; 129 | line-height: 0; 130 | position: relative; 131 | vertical-align: baseline; 132 | } 133 | 134 | sub { 135 | bottom: -0.25em; 136 | } 137 | 138 | sup { 139 | top: -0.5em; 140 | } 141 | 142 | /* 143 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 144 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 145 | 3. Remove gaps between table borders by default. 146 | */ 147 | 148 | table { 149 | text-indent: 0; /* 1 */ 150 | border-color: inherit; /* 2 */ 151 | border-collapse: collapse; /* 3 */ 152 | } 153 | 154 | /* 155 | 1. Change the font styles in all browsers. 156 | 2. Remove the margin in Firefox and Safari. 157 | 3. Remove default padding in all browsers. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: inherit; /* 1 */ 168 | color: inherit; /* 1 */ 169 | margin: 0; /* 2 */ 170 | padding: 0; /* 3 */ 171 | } 172 | 173 | /* 174 | Remove the inheritance of text transform in Edge and Firefox. 175 | */ 176 | 177 | button, 178 | select { 179 | text-transform: none; 180 | } 181 | 182 | /* 183 | 1. Correct the inability to style clickable types in iOS and Safari. 184 | 2. Remove default button styles. 185 | */ 186 | 187 | button, 188 | [type='button'], 189 | [type='reset'], 190 | [type='submit'] { 191 | -webkit-appearance: button; /* 1 */ 192 | background-color: transparent; /* 2 */ 193 | background-image: none; /* 2 */ 194 | } 195 | 196 | /* 197 | Use the modern Firefox focus style for all focusable elements. 198 | */ 199 | 200 | :-moz-focusring { 201 | outline: auto; 202 | } 203 | 204 | /* 205 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 206 | */ 207 | 208 | :-moz-ui-invalid { 209 | box-shadow: none; 210 | } 211 | 212 | /* 213 | Add the correct vertical alignment in Chrome and Firefox. 214 | */ 215 | 216 | progress { 217 | vertical-align: baseline; 218 | } 219 | 220 | /* 221 | Correct the cursor style of increment and decrement buttons in Safari. 222 | */ 223 | 224 | ::-webkit-inner-spin-button, 225 | ::-webkit-outer-spin-button { 226 | height: auto; 227 | } 228 | 229 | /* 230 | 1. Correct the odd appearance in Chrome and Safari. 231 | 2. Correct the outline style in Safari. 232 | */ 233 | 234 | [type='search'] { 235 | -webkit-appearance: textfield; /* 1 */ 236 | outline-offset: -2px; /* 2 */ 237 | } 238 | 239 | /* 240 | Remove the inner padding in Chrome and Safari on macOS. 241 | */ 242 | 243 | ::-webkit-search-decoration { 244 | -webkit-appearance: none; 245 | } 246 | 247 | /* 248 | 1. Correct the inability to style clickable types in iOS and Safari. 249 | 2. Change font properties to `inherit` in Safari. 250 | */ 251 | 252 | ::-webkit-file-upload-button { 253 | -webkit-appearance: button; /* 1 */ 254 | font: inherit; /* 2 */ 255 | } 256 | 257 | /* 258 | Add the correct display in Chrome and Safari. 259 | */ 260 | 261 | summary { 262 | display: list-item; 263 | } 264 | 265 | /* 266 | Removes the default spacing and border for appropriate elements. 267 | */ 268 | 269 | blockquote, 270 | dl, 271 | dd, 272 | h1, 273 | h2, 274 | h3, 275 | h4, 276 | h5, 277 | h6, 278 | hr, 279 | figure, 280 | p, 281 | pre { 282 | margin: 0; 283 | } 284 | 285 | fieldset { 286 | margin: 0; 287 | padding: 0; 288 | } 289 | 290 | legend { 291 | padding: 0; 292 | } 293 | 294 | ol, 295 | ul, 296 | menu { 297 | list-style: none; 298 | margin: 0; 299 | padding: 0; 300 | } 301 | 302 | /* 303 | Prevent resizing textareas horizontally by default. 304 | */ 305 | 306 | textarea { 307 | resize: vertical; 308 | } 309 | 310 | /* 311 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 312 | 2. Set the default placeholder color to the user's configured gray 400 color. 313 | */ 314 | 315 | input::-moz-placeholder, textarea::-moz-placeholder { 316 | opacity: 1; /* 1 */ 317 | color: #9ca3af; /* 2 */ 318 | } 319 | 320 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 321 | opacity: 1; /* 1 */ 322 | color: #9ca3af; /* 2 */ 323 | } 324 | 325 | input::placeholder, 326 | textarea::placeholder { 327 | opacity: 1; /* 1 */ 328 | color: #9ca3af; /* 2 */ 329 | } 330 | 331 | /* 332 | Set the default cursor for buttons. 333 | */ 334 | 335 | button, 336 | [role="button"] { 337 | cursor: pointer; 338 | } 339 | 340 | /* 341 | Make sure disabled buttons don't get the pointer cursor. 342 | */ 343 | :disabled { 344 | cursor: default; 345 | } 346 | 347 | /* 348 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 349 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 350 | This can trigger a poorly considered lint error in some tools but is included by design. 351 | */ 352 | 353 | img, 354 | svg, 355 | video, 356 | canvas, 357 | audio, 358 | iframe, 359 | embed, 360 | object { 361 | display: block; /* 1 */ 362 | vertical-align: middle; /* 2 */ 363 | } 364 | 365 | /* 366 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 367 | */ 368 | 369 | img, 370 | video { 371 | max-width: 100%; 372 | height: auto; 373 | } 374 | 375 | /* 376 | Ensure the default browser behavior of the `hidden` attribute. 377 | */ 378 | 379 | [hidden] { 380 | display: none; 381 | } 382 | 383 | *, ::before, ::after { 384 | --tw-translate-x: 0; 385 | --tw-translate-y: 0; 386 | --tw-rotate: 0; 387 | --tw-skew-x: 0; 388 | --tw-skew-y: 0; 389 | --tw-scale-x: 1; 390 | --tw-scale-y: 1; 391 | --tw-pan-x: ; 392 | --tw-pan-y: ; 393 | --tw-pinch-zoom: ; 394 | --tw-scroll-snap-strictness: proximity; 395 | --tw-ordinal: ; 396 | --tw-slashed-zero: ; 397 | --tw-numeric-figure: ; 398 | --tw-numeric-spacing: ; 399 | --tw-numeric-fraction: ; 400 | --tw-ring-inset: ; 401 | --tw-ring-offset-width: 0px; 402 | --tw-ring-offset-color: #fff; 403 | --tw-ring-color: rgb(59 130 246 / 0.5); 404 | --tw-ring-offset-shadow: 0 0 #0000; 405 | --tw-ring-shadow: 0 0 #0000; 406 | --tw-shadow: 0 0 #0000; 407 | --tw-shadow-colored: 0 0 #0000; 408 | --tw-blur: ; 409 | --tw-brightness: ; 410 | --tw-contrast: ; 411 | --tw-grayscale: ; 412 | --tw-hue-rotate: ; 413 | --tw-invert: ; 414 | --tw-saturate: ; 415 | --tw-sepia: ; 416 | --tw-drop-shadow: ; 417 | --tw-backdrop-blur: ; 418 | --tw-backdrop-brightness: ; 419 | --tw-backdrop-contrast: ; 420 | --tw-backdrop-grayscale: ; 421 | --tw-backdrop-hue-rotate: ; 422 | --tw-backdrop-invert: ; 423 | --tw-backdrop-opacity: ; 424 | --tw-backdrop-saturate: ; 425 | --tw-backdrop-sepia: ; 426 | } 427 | .absolute { 428 | position: absolute; 429 | } 430 | .top-10 { 431 | top: 2.5rem; 432 | } 433 | .mb-3 { 434 | margin-bottom: 0.75rem; 435 | } 436 | .mb-6 { 437 | margin-bottom: 1.5rem; 438 | } 439 | .mb-1 { 440 | margin-bottom: 0.25rem; 441 | } 442 | .mb-10 { 443 | margin-bottom: 2.5rem; 444 | } 445 | .mr-2 { 446 | margin-right: 0.5rem; 447 | } 448 | .block { 449 | display: block; 450 | } 451 | .inline { 452 | display: inline; 453 | } 454 | .flex { 455 | display: flex; 456 | } 457 | .w-2\/3 { 458 | width: 66.666667%; 459 | } 460 | .w-full { 461 | width: 100%; 462 | } 463 | .w-1\/3 { 464 | width: 33.333333%; 465 | } 466 | .appearance-none { 467 | -webkit-appearance: none; 468 | -moz-appearance: none; 469 | appearance: none; 470 | } 471 | .flex-row { 472 | flex-direction: row; 473 | } 474 | .items-center { 475 | align-items: center; 476 | } 477 | .justify-center { 478 | justify-content: center; 479 | } 480 | .justify-between { 481 | justify-content: space-between; 482 | } 483 | .rounded { 484 | border-radius: 0.25rem; 485 | } 486 | .rounded-lg { 487 | border-radius: 0.5rem; 488 | } 489 | .border { 490 | border-width: 1px; 491 | } 492 | .border-2 { 493 | border-width: 2px; 494 | } 495 | .border-gray-400 { 496 | --tw-border-opacity: 1; 497 | border-color: rgb(156 163 175 / var(--tw-border-opacity)); 498 | } 499 | .border-gray-200 { 500 | --tw-border-opacity: 1; 501 | border-color: rgb(229 231 235 / var(--tw-border-opacity)); 502 | } 503 | .bg-red-500 { 504 | --tw-bg-opacity: 1; 505 | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); 506 | } 507 | .bg-white { 508 | --tw-bg-opacity: 1; 509 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 510 | } 511 | .bg-purple-500 { 512 | --tw-bg-opacity: 1; 513 | background-color: rgb(168 85 247 / var(--tw-bg-opacity)); 514 | } 515 | .bg-pink-500 { 516 | --tw-bg-opacity: 1; 517 | background-color: rgb(236 72 153 / var(--tw-bg-opacity)); 518 | } 519 | .p-3 { 520 | padding: 0.75rem; 521 | } 522 | .py-2 { 523 | padding-top: 0.5rem; 524 | padding-bottom: 0.5rem; 525 | } 526 | .px-4 { 527 | padding-left: 1rem; 528 | padding-right: 1rem; 529 | } 530 | .pr-4 { 531 | padding-right: 1rem; 532 | } 533 | .pr-8 { 534 | padding-right: 2rem; 535 | } 536 | .text-xl { 537 | font-size: 1.25rem; 538 | line-height: 1.75rem; 539 | } 540 | .text-lg { 541 | font-size: 1.125rem; 542 | line-height: 1.75rem; 543 | } 544 | .font-bold { 545 | font-weight: 700; 546 | } 547 | .font-extrabold { 548 | font-weight: 800; 549 | } 550 | .leading-tight { 551 | line-height: 1.25; 552 | } 553 | .text-white { 554 | --tw-text-opacity: 1; 555 | color: rgb(255 255 255 / var(--tw-text-opacity)); 556 | } 557 | .text-blue-800 { 558 | --tw-text-opacity: 1; 559 | color: rgb(30 64 175 / var(--tw-text-opacity)); 560 | } 561 | .text-gray-500 { 562 | --tw-text-opacity: 1; 563 | color: rgb(107 114 128 / var(--tw-text-opacity)); 564 | } 565 | .text-gray-700 { 566 | --tw-text-opacity: 1; 567 | color: rgb(55 65 81 / var(--tw-text-opacity)); 568 | } 569 | .underline { 570 | -webkit-text-decoration-line: underline; 571 | text-decoration-line: underline; 572 | } 573 | .shadow { 574 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 575 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); 576 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 577 | } 578 | .hover\:border-gray-500:hover { 579 | --tw-border-opacity: 1; 580 | border-color: rgb(107 114 128 / var(--tw-border-opacity)); 581 | } 582 | .hover\:bg-red-400:hover { 583 | --tw-bg-opacity: 1; 584 | background-color: rgb(248 113 113 / var(--tw-bg-opacity)); 585 | } 586 | .hover\:bg-purple-400:hover { 587 | --tw-bg-opacity: 1; 588 | background-color: rgb(192 132 252 / var(--tw-bg-opacity)); 589 | } 590 | .hover\:bg-pink-400:hover { 591 | --tw-bg-opacity: 1; 592 | background-color: rgb(244 114 182 / var(--tw-bg-opacity)); 593 | } 594 | .focus\:border-purple-500:focus { 595 | --tw-border-opacity: 1; 596 | border-color: rgb(168 85 247 / var(--tw-border-opacity)); 597 | } 598 | .focus\:bg-white:focus { 599 | --tw-bg-opacity: 1; 600 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 601 | } 602 | .focus\:outline-none:focus { 603 | outline: 2px solid transparent; 604 | outline-offset: 2px; 605 | } 606 | @media (min-width: 768px) { 607 | 608 | .md\:mb-0 { 609 | margin-bottom: 0px; 610 | } 611 | 612 | .md\:flex { 613 | display: flex; 614 | } 615 | 616 | .md\:w-1\/6 { 617 | width: 16.666667%; 618 | } 619 | 620 | .md\:w-5\/6 { 621 | width: 83.333333%; 622 | } 623 | 624 | .md\:text-right { 625 | text-align: right; 626 | } 627 | } 628 | -------------------------------------------------------------------------------- /src/RecurCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const RecurCard = (props: { uuids: any[]; id: string; item: string }) => { 4 | const [uuids] = useState(props.uuids); 5 | const [settings, setSettings] = useState(logseq.settings); 6 | 7 | const deleteBlocks = async () => { 8 | // delete all blocks 9 | for (let u of uuids) { 10 | await logseq.Editor.removeBlock(u); 11 | } 12 | 13 | // delete entry from settings by matching the id from props and the dateAdded from settings 14 | const recurrencesClone: any[] = settings.recurrences; 15 | recurrencesClone.splice( 16 | recurrencesClone.findIndex((i) => i.dateAdded === props.id), 17 | 1 18 | ); 19 | setSettings((prevSettings) => ({ 20 | ...prevSettings, 21 | recurrences: recurrencesClone, 22 | })); 23 | logseq.hideMainUI(); 24 | logseq.App.showMsg("Blocks deleted successfully!"); 25 | }; 26 | 27 | return ( 28 |
29 |
30 |

{props.item}

31 |
32 |
33 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default RecurCard; 46 | -------------------------------------------------------------------------------- /src/Recurrence.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import DatePicker from "react-datepicker"; 3 | import "react-datepicker/dist/react-datepicker.css"; 4 | import { getDateForPageWithoutBrackets } from "logseq-dateutils"; 5 | import dayjs from "dayjs"; 6 | import { BlockEntity, PageEntity } from "@logseq/libs/dist/LSPlugin.user"; 7 | import RecurCard from "./RecurCard"; 8 | 9 | const Recurrence = () => { 10 | const [content, setContent] = useState(""); 11 | const [contentUUID, setContentUUID] = useState(""); 12 | const [journalDay, setJournalDay] = useState(""); 13 | 14 | const getCurrentBlock = async () => { 15 | const currBlock: BlockEntity = await logseq.Editor.getCurrentBlock(); 16 | const currPage: PageEntity = await logseq.Editor.getPage(currBlock.page.id); 17 | setJournalDay(currPage.journalDay.toString()); 18 | setContent(currBlock.content); 19 | setContentUUID(currBlock.uuid); 20 | }; 21 | 22 | useEffect(() => { 23 | getCurrentBlock(); 24 | }); 25 | 26 | const [recurrenceValues, setRecurrenceValues] = useState({ 27 | recurrencePattern: "", 28 | recurrenceType: "", 29 | options: { 30 | endAfter: "", 31 | endBy: null, 32 | }, 33 | }); 34 | 35 | const handleForm = (e: any) => { 36 | if (!e.type) { 37 | setRecurrenceValues((prevValues) => ({ 38 | ...prevValues, 39 | options: { 40 | endAfter: "", 41 | endBy: e, 42 | }, 43 | })); 44 | } else { 45 | const name = e.target.name.split("."); 46 | if (name.length > 1) { 47 | setRecurrenceValues((prevValues) => ({ 48 | ...prevValues, 49 | [name[0]]: { [name[1]]: e.target.value, endBy: "" }, 50 | })); 51 | } else { 52 | setRecurrenceValues((prevValues) => ({ 53 | ...prevValues, 54 | [name[0]]: e.target.value, 55 | })); 56 | } 57 | } 58 | }; 59 | 60 | const resetForm = () => { 61 | setRecurrenceValues({ 62 | recurrencePattern: "", 63 | recurrenceType: "", 64 | options: { 65 | endAfter: "", 66 | endBy: "", 67 | }, 68 | }); 69 | }; 70 | 71 | const createBlocks = async () => { 72 | if (content === "" || !content) { 73 | logseq.App.showMsg("You have no content to recur", "error"); 74 | return; 75 | } 76 | 77 | // Get basic settings 78 | const { recurrencePattern, recurrenceType, options } = recurrenceValues; 79 | const { preferredDateFormat } = logseq.settings; 80 | 81 | const d = new Date(); 82 | let dates = []; 83 | let settingsToBeSaved = { 84 | item: content, 85 | dateAdded: dayjs(d).unix(), 86 | uuids: [contentUUID], 87 | }; 88 | 89 | // Create blocks 90 | if (recurrenceType === "occurrences") { 91 | if (parseInt(options.endAfter) < 1) { 92 | logseq.App.showMsg( 93 | "You have indicated a negative or zero occurence.", 94 | "error" 95 | ); 96 | return; 97 | } 98 | 99 | for (let i = 0; i < parseInt(options.endAfter); i++) { 100 | if (recurrencePattern === "daily") { 101 | const payload = getDateForPageWithoutBrackets( 102 | dayjs(journalDay).add(i, "day").toDate(), 103 | preferredDateFormat 104 | ); 105 | dates.push(payload.toLowerCase()); 106 | } else if (recurrencePattern === "weekly") { 107 | const payload = getDateForPageWithoutBrackets( 108 | dayjs(journalDay).add(i, "week").toDate(), 109 | preferredDateFormat 110 | ); 111 | dates.push(payload.toLowerCase()); 112 | } else if (recurrencePattern === "monthly") { 113 | const payload = getDateForPageWithoutBrackets( 114 | dayjs(journalDay).add(i, "month").toDate(), 115 | preferredDateFormat 116 | ); 117 | dates.push(payload.toLowerCase()); 118 | } else if (recurrencePattern === "yearly") { 119 | const payload = getDateForPageWithoutBrackets( 120 | dayjs(journalDay).add(i, "year").toDate(), 121 | preferredDateFormat 122 | ); 123 | dates.push(payload.toLowerCase()); 124 | } 125 | } 126 | } else if (recurrenceType === "date") { 127 | const pushPayload = (d: Date) => { 128 | const payload = getDateForPageWithoutBrackets(d, preferredDateFormat); 129 | dates.push(payload.toLowerCase()); 130 | }; 131 | const endByDate = dayjs(new Date(options.endBy)).add(1, "day").toDate(); 132 | 133 | let i = 0; 134 | while (true) { 135 | if (recurrencePattern === "daily") { 136 | const d = dayjs(journalDay).add(i, "day").toDate(); 137 | 138 | if (d <= endByDate) { 139 | pushPayload(d); 140 | } else { 141 | break; 142 | } 143 | } else if (recurrencePattern === "weekly") { 144 | const d = dayjs(journalDay).add(i, "week").toDate(); 145 | 146 | if (d <= endByDate) { 147 | pushPayload(d); 148 | } else { 149 | break; 150 | } 151 | } else if (recurrencePattern === "monthly") { 152 | const d = dayjs(journalDay).add(i, "month").toDate(); 153 | 154 | if (d <= endByDate) { 155 | pushPayload(d); 156 | } else { 157 | break; 158 | } 159 | } else if (recurrencePattern === "yearly") { 160 | const d = dayjs(journalDay).add(i, "year").toDate(); 161 | 162 | if (d <= endByDate) { 163 | pushPayload(d); 164 | } else { 165 | break; 166 | } 167 | } 168 | i++; 169 | } 170 | } 171 | 172 | // Add blocks to the designated pages 173 | for (let j = 1; j < dates.length; j++) { 174 | const getPage = await logseq.Editor.getPage(dates[j]); 175 | 176 | if (getPage === null) { 177 | await logseq.Editor.createPage(dates[j], "", { 178 | redirect: false, 179 | createFirstBlock: false, 180 | format: "markdown", 181 | }); 182 | } 183 | 184 | const itemBlock = await logseq.Editor.insertBlock(dates[j], content, { 185 | isPageBlock: true, 186 | }); 187 | 188 | settingsToBeSaved.uuids.push(itemBlock.uuid); 189 | } 190 | 191 | // Clear forms 192 | resetForm(); 193 | 194 | logseq.App.showMsg("Blocks added successfully!"); 195 | 196 | logseq.hideMainUI(); 197 | 198 | // Save recurrences to settings so can delete them 199 | if ( 200 | !logseq.settings.recurrences || 201 | logseq.settings.recurrences.length === 0 202 | ) { 203 | console.log("Updating settings for the first time..."); 204 | logseq.updateSettings({ recurrences: [settingsToBeSaved] }); 205 | console.log(logseq.settings); 206 | } else { 207 | console.log("Updating settings..."); 208 | let existingSettings: any[] = logseq.settings.recurrences; 209 | existingSettings.push(settingsToBeSaved); 210 | logseq.updateSettings({ 211 | recurrences: existingSettings, 212 | }); 213 | console.log(logseq.settings); 214 | } 215 | }; 216 | 217 | return ( 218 |
219 |
220 |
221 | {content} 222 |
223 | 224 |
225 |
226 | 229 |
230 |
231 | 243 |
244 |
245 | 246 |
247 |
248 | 251 |
252 |
253 | 263 |
264 |
265 | 266 | {recurrenceValues.recurrenceType === "occurrences" && ( 267 |
268 |
269 | 272 |
273 |
274 | {" "} 284 | occurences 285 |
286 |
287 | )} 288 | 289 | {recurrenceValues.recurrenceType === "date" && ( 290 |
291 |
292 | 295 |
296 |
297 | 303 |
304 |
305 | )} 306 | 307 |
308 |
309 |
310 | 317 | 324 |
325 |
326 | 327 |
328 | {logseq.settings.recurrences.length > 0 && ( 329 |

Saved Recurrences

330 | )} 331 |
332 | {logseq.settings.recurrences && 333 | logseq.settings.recurrences.map( 334 | (r: { uuids: any[]; item: string; dateAdded: string }) => ( 335 | 336 | ) 337 | )} 338 |
339 |
340 | ); 341 | }; 342 | 343 | export default Recurrence; 344 | -------------------------------------------------------------------------------- /src/handleClosePopup.ts: -------------------------------------------------------------------------------- 1 | export const handleClosePopup = () => { 2 | //ESC 3 | document.addEventListener( 4 | 'keydown', 5 | function (e) { 6 | if (e.keyCode === 27) { 7 | logseq.hideMainUI({ restoreEditingCursor: true }); 8 | } 9 | e.stopPropagation(); 10 | }, 11 | false 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@logseq/libs'; 2 | import './App.css'; 3 | import { handleClosePopup } from './handleClosePopup'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import Recurrence from './Recurrence'; 7 | 8 | const main = () => { 9 | console.log('logseq-recurrence-plugin loaded'); 10 | 11 | window.setTimeout(async () => { 12 | const userConfigs = await logseq.App.getUserConfigs(); 13 | const preferredDateFormat: string = userConfigs.preferredDateFormat; 14 | logseq.updateSettings({ preferredDateFormat: preferredDateFormat }); 15 | console.log(`Settings updated to ${preferredDateFormat}`); 16 | }, 3000); 17 | 18 | if (!logseq.settings.recurrences) { 19 | logseq.updateSettings({ 20 | recurrences: [], 21 | }); 22 | } 23 | 24 | logseq.Editor.registerSlashCommand('Set Recurrence', async () => { 25 | ReactDOM.render( 26 | 27 | 28 | , 29 | document.getElementById('app') 30 | ); 31 | 32 | logseq.showMainUI(); 33 | }); 34 | 35 | handleClosePopup(); 36 | }; 37 | 38 | logseq.ready(main).catch(console.error); 39 | -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{vue,js,ts,jsx,tsx,hbs,html}'], 3 | darkMode: 'media', // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | spacing: { 7 | 100: '50rem', 8 | }, 9 | }, 10 | }, 11 | variants: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | }; 16 | --------------------------------------------------------------------------------