Squashed commit of the following:

commit f31e7ae2c1
Author: joenepraat <joenepraat@posteo.org>
Date:   Thu Mar 30 13:17:01 2023 +0000

    Translated using Weblate (Dutch)

    Currently translated at 94.5% (297 of 314 strings)

    Translation: Gancio/Web
    Translate-URL: https://hosted.weblate.org/projects/gancio/web/nl/

commit 96486b5945
Author: lesion <lesion@autistici.org>
Date:   Thu Mar 30 11:18:39 2023 +0200

    minor

commit 9524fd52f3
Author: lesion <lesion@autistici.org>
Date:   Thu Mar 30 00:08:27 2023 +0200

    update changelog, releasing v1.6.8

commit df4ec69128
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 29 12:41:52 2023 +0200

    force vuetify to not use google fonts

commit e35595df82
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 21:54:06 2023 +0200

    layout

commit ee8a9843b3
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 21:48:26 2023 +0200

    minor

commit 2608761a44
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 19:04:49 2023 +0200

    update deps

commit d7c8de7580
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 19:04:41 2023 +0200

    minor

commit 6b55ba1708
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 19:04:16 2023 +0200

    use new luxon in rss template, fixing locale and timezone #254

commit fc52107bd9
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 19:02:08 2023 +0200

    use luxon instead of dayjs server side too

commit f5604a03bc
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 18:55:57 2023 +0200

    unit test for recurrent events

commit 3e81d1dfb3
Merge: f960400 e750fc8
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 28 18:51:34 2023 +0200

    Merge remote-tracking branch 'weblate/master'

commit f960400085
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 27 17:19:27 2023 +0200

    improve index/tag/place layout

commit 0682feaaf8
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 27 17:18:57 2023 +0200

    minor with theme admin colors

commit e750fc8e81
Author: josé m <correoxm@disroot.org>
Date:   Sun Mar 26 07:48:06 2023 +0000

    Translated using Weblate (Galician)

    Currently translated at 100.0% (314 of 314 strings)

    Translation: Gancio/Web
    Translate-URL: https://hosted.weblate.org/projects/gancio/web/gl/

commit 4c74fd3227
Author: gallegonovato <fran-carro@hotmail.es>
Date:   Sat Mar 25 11:23:48 2023 +0000

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (314 of 314 strings)

    Translation: Gancio/Web
    Translate-URL: https://hosted.weblate.org/projects/gancio/web/es/

commit 428a94290f
Author: joenepraat <joenepraat@posteo.org>
Date:   Thu Mar 23 15:15:58 2023 +0000

    Translated using Weblate (Dutch)

    Currently translated at 95.5% (299 of 313 strings)

    Translation: Gancio/Web
    Translate-URL: https://hosted.weblate.org/projects/gancio/web/nl/

commit 373f78cd4e
Author: joenepraat <joenepraat@posteo.org>
Date:   Thu Mar 23 17:47:21 2023 +0000

    Translated using Weblate (Dutch)

    Currently translated at 7.6% (1 of 13 strings)

    Translation: Gancio/Email
    Translate-URL: https://hosted.weblate.org/projects/gancio/email/nl/

commit 2d11d88e8f
Merge: ea3066c e2fd5f8
Author: lesion <lesion@autistici.org>
Date:   Sat Mar 25 09:34:44 2023 +0100

    Merge remote-tracking branch 'weblate/master'

commit e2fd5f8b93
Author: joenepraat <joenepraat@posteo.org>
Date:   Thu Mar 23 15:15:58 2023 +0000

    Translated using Weblate (Dutch)

    Currently translated at 95.5% (299 of 313 strings)

    Translation: Gancio/Web
    Translate-URL: https://hosted.weblate.org/projects/gancio/web/nl/

commit 8280eb7c7a
Author: joenepraat <joenepraat@posteo.org>
Date:   Thu Mar 23 17:47:21 2023 +0000

    Translated using Weblate (Dutch)

    Currently translated at 7.6% (1 of 13 strings)

    Translation: Gancio/Email
    Translate-URL: https://hosted.weblate.org/projects/gancio/email/nl/

commit ea3066c34e
Author: lesion <lesion@autistici.org>
Date:   Fri Mar 24 16:03:08 2023 +0100

    people could choose custom colors

commit 8149ea23da
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 22 15:39:02 2023 +0100

    moving vuetify configuration to "middleware"

commit 7e6130155a
Author: lesion <lesion@autistici.org>
Date:   Fri Mar 10 21:58:02 2023 +0100

    start with custom color

commit e2b07a06bd
Author: lesion <lesion@autistici.org>
Date:   Thu Mar 23 13:11:15 2023 +0100

    release WPGancio 1.7

commit b91774ac39
Author: lesion <lesion@autistici.org>
Date:   Thu Mar 23 13:09:10 2023 +0100

    fix tags merge in wp plugin and end_datetime

commit c8493d0810
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 22 18:45:48 2023 +0100

    use new $time plugin instead of filters, fix #252

commit 780938ef91
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 22 16:51:02 2023 +0100

    v1.6.7

commit 442f88f322
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 22 16:49:37 2023 +0100

    minor

commit 9f90df2bfc
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 22 16:24:08 2023 +0100

    v.1.6.6

commit 71fdeb6ff8
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 22 16:17:02 2023 +0100

    some old event was flagged has multidate but without an end_datetime?

    fix #245

commit e6977368c5
Author: lesion <lesion@autistici.org>
Date:   Wed Mar 22 13:41:50 2023 +0100

    revert Intl, node is not ready, fix #250

commit eb2bf32162
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 21 16:47:47 2023 +0100

    release v1.6.5

commit 0ebb467630
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 21 01:15:35 2023 +0100

    minor

commit 934466b2ec
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 20 21:09:58 2023 +0100

    add modules in package

commit ec62ad9ba8
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 20 21:09:43 2023 +0100

    v-lazy in collection page too

commit d9f093fdbb
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 20 12:55:17 2023 +0100

    releasing v1.6.5

commit 2ffd2aff82
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 20 12:53:41 2023 +0100

    enable task manager in dev mode

commit 02f138e0f2
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 20 12:40:39 2023 +0100

    update CHANGELOG

commit b8e096ee39
Author: lesion <lesion@autistici.org>
Date:   Mon Mar 20 12:40:19 2023 +0100

    minor with 2w recurrent event frequency

commit 8f221fb69c
Author: lesion <lesion@autistici.org>
Date:   Sun Mar 19 23:33:55 2023 +0100

    minor

commit 079bcd4af2
Merge: 99d78e2 ae990fc
Author: lesion <lesion@autistici.org>
Date:   Sun Mar 19 23:28:44 2023 +0100

    Merge remote-tracking branch 'sedum/feat/ssr-proxy'

commit 99d78e2492
Author: lesion <lesion@autistici.org>
Date:   Sun Mar 19 23:26:57 2023 +0100

    Squashed commit of the following:

    commit 5c0d380740
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:22:25 2023 +0100

        update yarn.lock

    commit 909ee71ecb
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:22:09 2023 +0100

        Squashed commit of the following:

        commit fc8a9f4506
        Author: lesion <lesion@autistici.org>
        Date:   Tue Mar 14 16:42:24 2023 +0100

            address some issues with recurrent events, fix #247

        commit f7357666ca
        Author: lesion <lesion@autistici.org>
        Date:   Tue Mar 14 16:16:52 2023 +0100

            fix event import from URL

        commit e1bca6f46a
        Author: lesion <lesion@autistici.org>
        Date:   Tue Mar 14 16:15:42 2023 +0100

             add Duch (nl) locale (thanks @jeoenepraat)

        commit 5f8afdbc12
        Merge: 57a052a 92ca5ab
        Author: lesion <lesion@autistici.org>
        Date:   Tue Mar 14 11:39:50 2023 +0100

            Merge remote-tracking branch 'weblate/master'

        commit 57a052a7fa
        Merge: 63d1d2e 55137d2
        Author: lesion <lesion@autistici.org>
        Date:   Tue Mar 14 11:39:33 2023 +0100

            Merge commit '55137d2ac23549e633f36ad10139fd4168c2645f'

        commit 92ca5abf5e
        Author: joenepraat <joenepraat@posteo.org>
        Date:   Fri Mar 10 23:16:32 2023 +0000

            Translated using Weblate (Dutch)

            Currently translated at 68.3% (214 of 313 strings)

            Translation: Gancio/Web
            Translate-URL: https://hosted.weblate.org/projects/gancio/web/nl/

        commit 63d1d2ee53
        Author: lesion <lesion@autistici.org>
        Date:   Thu Mar 9 21:41:06 2023 +0100

            minor

        commit d2759a55a5
        Author: lesion <lesion@autistici.org>
        Date:   Thu Mar 9 21:38:39 2023 +0100

            wrong user / admin merge dark theme settings - fix #244

        commit b401d829db
        Author: lesion <lesion@autistici.org>
        Date:   Thu Mar 9 21:24:45 2023 +0100

            remove a small warning

        commit ccffe5f7b0
        Author: lesion <lesion@autistici.org>
        Date:   Fri Feb 24 11:40:36 2023 +0100

            push tags on release

        commit 55137d2ac2
        Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
        Date:   Thu Feb 23 23:56:16 2023 +0000

            Bump sequelize from 6.28.0 to 6.29.0

            Bumps [sequelize](https://github.com/sequelize/sequelize) from 6.28.0 to 6.29.0.
            - [Release notes](https://github.com/sequelize/sequelize/releases)
            - [Commits](https://github.com/sequelize/sequelize/compare/v6.28.0...v6.29.0)

            ---
            updated-dependencies:
            - dependency-name: sequelize
              dependency-type: direct:production
            ...

            Signed-off-by: dependabot[bot] <support@github.com>

        commit b654f29d8b
        Author: lesion <lesion@autistici.org>
        Date:   Wed Feb 22 13:21:17 2023 +0100

            update changelog

        commit 0cd1ee9d89
        Author: lesion <lesion@autistici.org>
        Date:   Wed Feb 22 13:17:29 2023 +0100

            increase rate limit max requests per minutes

        commit b6dafc082e
        Author: lesion <lesion@autistici.org>
        Date:   Wed Feb 22 08:45:39 2023 +0100

            minor

        commit 0fa7769844
        Author: lesion <lesion@autistici.org>
        Date:   Wed Feb 22 08:45:18 2023 +0100

            location saving is not working when geocoding is disabled, fix #238

        commit 07f9e2d9ee
        Author: lesion <lesion@autistici.org>
        Date:   Wed Feb 22 08:33:40 2023 +0100

            really fix #232

        commit bae930799e
        Author: lesion <lesion@autistici.org>
        Date:   Wed Feb 22 08:33:09 2023 +0100

            downgrade mariadb (sequelize is not ready)

        commit d733d7fef1
        Author: lesion <lesion@autistici.org>
        Date:   Wed Feb 22 00:16:28 2023 +0100

            aargh

        commit 98b22aad70
        Author: lesion <lesion@autistici.org>
        Date:   Tue Feb 21 00:56:06 2023 +0100

            minor

        commit fc098b603d
        Author: lesion <lesion@autistici.org>
        Date:   Tue Feb 21 00:55:44 2023 +0100

            missing i18n in setup, fix #239

        commit 3eaf72af19
        Merge: bba196b d6c6034
        Author: lesion <lesion@autistici.org>
        Date:   Mon Feb 20 21:17:37 2023 +0100

            Merge remote-tracking branch 'weblate/master'

        commit bba196b068
        Author: lesion <lesion@autistici.org>
        Date:   Sat Feb 18 00:05:52 2023 +0100

            update changelog, v1.6.3

        commit bb9f7cca47
        Author: lesion <lesion@autistici.org>
        Date:   Sat Feb 18 00:04:28 2023 +0100

            minor

        commit 80d2dbd06b
        Author: lesion <lesion@autistici.org>
        Date:   Fri Feb 17 23:40:28 2023 +0100

            minor

        commit d6c6034630
        Author: fadelkon <fadelkon@posteo.net>
        Date:   Thu Feb 16 22:09:23 2023 +0000

            Translated using Weblate (Catalan)

            Currently translated at 100.0% (313 of 313 strings)

            Translation: Gancio/Web
            Translate-URL: https://hosted.weblate.org/projects/gancio/web/ca/

        commit d125cf1506
        Author: lesion <lesion@autistici.org>
        Date:   Fri Feb 17 21:56:31 2023 +0100

            set a default user_locale path

        commit 4367960a62
        Merge: c8cc5c6 87dd179
        Author: lesion <lesion@autistici.org>
        Date:   Tue Feb 7 17:46:58 2023 +0100

            Merge branch 'master' into gh

        commit c8cc5c6c97
        Merge: 88e0c90 550e221
        Author: lesion <lesion@autistici.org>
        Date:   Mon Jan 9 17:15:21 2023 +0100

            Merge branch 'master' into gh

        commit 88e0c90a66
        Merge: 421aa12 f212ac1
        Author: lesion <lesion@autistici.org>
        Date:   Thu Dec 15 09:54:41 2022 +0100

            Merge branch 'master' into gh

        commit 421aa12781
        Merge: 5f6cc46 b3488e7
        Author: lesion <lesion@autistici.org>
        Date:   Wed Sep 28 12:26:08 2022 +0200

            Merge branch 'master' into gh

        commit 5f6cc46cdc
        Merge: b66feb9 171d968
        Author: lesion <lesion@autistici.org>
        Date:   Mon Aug 8 00:08:12 2022 +0200

            Merge branch 'master' into gh

        commit b66feb92e2
        Merge: 80c55d5 05d068f
        Author: lesion <lesion@autistici.org>
        Date:   Tue Jun 21 23:48:40 2022 +0200

            Merge branch 'master' into gh

        commit 80c55d5601
        Merge: 814090e a154fdf
        Author: lesion <lesion@autistici.org>
        Date:   Mon Jun 6 17:27:00 2022 +0200

            Merge branch 'master' into gh

        commit 814090e9b6
        Merge: 616c542 2e3aba9
        Author: lesion <lesion@autistici.org>
        Date:   Mon Jun 6 17:19:31 2022 +0200

            Merge branch 'master' into gh

        commit 616c54229a
        Merge: e4cb22e 82dcaf9
        Author: lesion <lesion@autistici.org>
        Date:   Mon Jun 6 16:57:05 2022 +0200

            Merge branch 'master' into gh

        commit e4cb22ee33
        Merge: 5dddfbd 8657937
        Author: lesion <lesion@autistici.org>
        Date:   Fri Mar 11 23:41:22 2022 +0100

            Merge branch 'master' into gh

        commit 5dddfbd29e
        Merge: 60e9d95 10c6b0d
        Author: lesion <lesion@autistici.org>
        Date:   Fri Mar 11 23:22:12 2022 +0100

            Merge branch 'master' into gh

        commit 60e9d95ba8
        Merge: 79445ca ad93f83
        Author: lesion <lesion@autistici.org>
        Date:   Tue Dec 7 01:35:18 2021 +0100

            Merge branch 'master' into gh

        commit 79445ca8a7
        Merge: 9472d8d cd313ef
        Author: les <lesion@autistici.org>
        Date:   Thu Jun 24 21:52:25 2021 +0200

            Merge branch 'master' into gh

        commit 9472d8d919
        Merge: f960149 9e9643e
        Author: les <lesion@autistici.org>
        Date:   Fri Mar 26 22:27:41 2021 +0100

            Merge branch 'dev' into gh

        commit f9601492dc
        Author: les <lesion@autistici.org>
        Date:   Fri Dec 6 11:30:41 2019 +0100

            update dependencies

        commit f8c7fa2b45
        Author: les <lesion@autistici.org>
        Date:   Fri Dec 6 11:41:13 2019 +0100

            minor

        commit 33ca266535
        Author: les <lesion@autistici.org>
        Date:   Fri Dec 6 11:38:15 2019 +0100

            prepare gh as a mirror

    commit 5c88754116
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:18:40 2023 +0100

        update deps

    commit 7eac4fce32
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:18:25 2023 +0100

        refactoring event detail page

    commit dc9ca88bc6
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:17:35 2023 +0100

        show hide boosts/bookmarks, fix #241

    commit d4a25b1dd0
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:13:58 2023 +0100

        minor with unixFormat

    commit 239d6bcab1
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:12:25 2023 +0100

        minor

    commit b149f980db
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:12:05 2023 +0100

        minor

    commit 6f2955c584
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:11:49 2023 +0100

        minor

    commit dd586c38c9
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:11:31 2023 +0100

        minor on style

    commit 544823717b
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:11:15 2023 +0100

        fix multidate issue, go to event on save

    commit 9ef0c75d03
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 23:09:47 2023 +0100

        use v-lazy, improve search, full tag/place events

    commit ac91072b79
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 22:47:51 2023 +0100

        increase DDOS limiter to 250 req/min

    commit d0ca92efb4
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 22:47:14 2023 +0100

        update changelog

    commit 2d54f19225
    Author: lesion <lesion@autistici.org>
    Date:   Sun Mar 19 22:46:51 2023 +0100

        use luxon instead of dayjs, new $time plugin

commit ae990fc370
Author: sedum <sedum@oziosi.org>
Date:   Thu Mar 16 17:29:48 2023 +0100

    add support for server side http proxy, close #240

commit fc8a9f4506
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 14 16:42:24 2023 +0100

    address some issues with recurrent events, fix #247

commit f7357666ca
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 14 16:16:52 2023 +0100

    fix event import from URL

commit e1bca6f46a
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 14 16:15:42 2023 +0100

     add Duch (nl) locale (thanks @jeoenepraat)

commit 5f8afdbc12
Merge: 57a052a 92ca5ab
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 14 11:39:50 2023 +0100

    Merge remote-tracking branch 'weblate/master'

commit 57a052a7fa
Merge: 63d1d2e 55137d2
Author: lesion <lesion@autistici.org>
Date:   Tue Mar 14 11:39:33 2023 +0100

    Merge commit '55137d2ac23549e633f36ad10139fd4168c2645f'

commit 92ca5abf5e
Author: joenepraat <joenepraat@posteo.org>
Date:   Fri Mar 10 23:16:32 2023 +0000

    Translated using Weblate (Dutch)

    Currently translated at 68.3% (214 of 313 strings)

    Translation: Gancio/Web
    Translate-URL: https://hosted.weblate.org/projects/gancio/web/nl/

commit 63d1d2ee53
Author: lesion <lesion@autistici.org>
Date:   Thu Mar 9 21:41:06 2023 +0100

    minor

commit d2759a55a5
Author: lesion <lesion@autistici.org>
Date:   Thu Mar 9 21:38:39 2023 +0100

    wrong user / admin merge dark theme settings - fix #244

commit b401d829db
Author: lesion <lesion@autistici.org>
Date:   Thu Mar 9 21:24:45 2023 +0100

    remove a small warning

commit 55137d2ac2
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Feb 23 23:56:16 2023 +0000

    Bump sequelize from 6.28.0 to 6.29.0

    Bumps [sequelize](https://github.com/sequelize/sequelize) from 6.28.0 to 6.29.0.
    - [Release notes](https://github.com/sequelize/sequelize/releases)
    - [Commits](https://github.com/sequelize/sequelize/compare/v6.28.0...v6.29.0)

    ---
    updated-dependencies:
    - dependency-name: sequelize
      dependency-type: direct:production
    ...

    Signed-off-by: dependabot[bot] <support@github.com>

commit 4367960a62
Merge: c8cc5c6 87dd179
Author: lesion <lesion@autistici.org>
Date:   Tue Feb 7 17:46:58 2023 +0100

    Merge branch 'master' into gh

commit c8cc5c6c97
Merge: 88e0c90 550e221
Author: lesion <lesion@autistici.org>
Date:   Mon Jan 9 17:15:21 2023 +0100

    Merge branch 'master' into gh

commit 88e0c90a66
Merge: 421aa12 f212ac1
Author: lesion <lesion@autistici.org>
Date:   Thu Dec 15 09:54:41 2022 +0100

    Merge branch 'master' into gh

commit 421aa12781
Merge: 5f6cc46 b3488e7
Author: lesion <lesion@autistici.org>
Date:   Wed Sep 28 12:26:08 2022 +0200

    Merge branch 'master' into gh

commit 5f6cc46cdc
Merge: b66feb9 171d968
Author: lesion <lesion@autistici.org>
Date:   Mon Aug 8 00:08:12 2022 +0200

    Merge branch 'master' into gh

commit b66feb92e2
Merge: 80c55d5 05d068f
Author: lesion <lesion@autistici.org>
Date:   Tue Jun 21 23:48:40 2022 +0200

    Merge branch 'master' into gh

commit 80c55d5601
Merge: 814090e a154fdf
Author: lesion <lesion@autistici.org>
Date:   Mon Jun 6 17:27:00 2022 +0200

    Merge branch 'master' into gh

commit 814090e9b6
Merge: 616c542 2e3aba9
Author: lesion <lesion@autistici.org>
Date:   Mon Jun 6 17:19:31 2022 +0200

    Merge branch 'master' into gh

commit 616c54229a
Merge: e4cb22e 82dcaf9
Author: lesion <lesion@autistici.org>
Date:   Mon Jun 6 16:57:05 2022 +0200

    Merge branch 'master' into gh

commit e4cb22ee33
Merge: 5dddfbd 8657937
Author: lesion <lesion@autistici.org>
Date:   Fri Mar 11 23:41:22 2022 +0100

    Merge branch 'master' into gh

commit 5dddfbd29e
Merge: 60e9d95 10c6b0d
Author: lesion <lesion@autistici.org>
Date:   Fri Mar 11 23:22:12 2022 +0100

    Merge branch 'master' into gh

commit 60e9d95ba8
Merge: 79445ca ad93f83
Author: lesion <lesion@autistici.org>
Date:   Tue Dec 7 01:35:18 2021 +0100

    Merge branch 'master' into gh

commit 79445ca8a7
Merge: 9472d8d cd313ef
Author: les <lesion@autistici.org>
Date:   Thu Jun 24 21:52:25 2021 +0200

    Merge branch 'master' into gh

commit 9472d8d919
Merge: f960149 9e9643e
Author: les <lesion@autistici.org>
Date:   Fri Mar 26 22:27:41 2021 +0100

    Merge branch 'dev' into gh

commit f9601492dc
Author: les <lesion@autistici.org>
Date:   Fri Dec 6 11:30:41 2019 +0100

    update dependencies

commit f8c7fa2b45
Author: les <lesion@autistici.org>
Date:   Fri Dec 6 11:41:13 2019 +0100

    minor

commit 33ca266535
Author: les <lesion@autistici.org>
Date:   Fri Dec 6 11:38:15 2019 +0100

    prepare gh as a mirror
This commit is contained in:
lesion 2023-04-03 16:39:26 +02:00
parent 4da715dd39
commit 08e4fe856c
No known key found for this signature in database
GPG key ID: 352918250B012177
72 changed files with 2912 additions and 2254 deletions

View file

@ -1,5 +1,33 @@
All notable changes to this project will be documented in this file.
### 1.6.8
- use new $time in admins table, fix #252
- fix some WPGancio issues
- new custom color feature!
- update locales (durch, spanish, galician)
- improve index, tag, place page layout
- use luxon instead of dayjs server side, fix #254
### 1.6.7 - 22 mar '23
- node is not Intl ready, fix #250
- fix old smultidate events without an end_datetime!
- fix a small issue during setup
### 1.6.5 - 21 mar '23
- optimize home page using lazy loading
- add support for server side http proxy (thanks @sedum)
- add Duch (nl) locale (thanks @jeoenepraat)
- fix #244, dark theme user / admin preference merge issue
- fix some issues with recurrent events, #247
- improve search flow (order by, press enter or icon...)
- filters / helpers refactoring
- tag and place pages list all events (past too)
- show boosts/bookmarks, fix #241
- go to event on save/update
- use luxon instead of dayjs, new $time plugin
- fix event import from URL
- remove an annoying warning from logs
### 1.6.4 - 22 feb '23
- add missing i18n during setup
- really fix #232

View file

@ -1,51 +0,0 @@
import dayjs from 'dayjs'
export function attributesFromEvents(_events) {
// merge events with same date
let attributes = []
const now = dayjs().unix()
for (let e of _events) {
const key = dayjs.unix(e.start_datetime).tz().format('MMDD') // Math.floor(e.start_datetime/(3600*24)) // dayjs.unix(e.start_datetime).tz().format('YYYYMMDD')
const c = (e.end_datetime || e.start_datetime) < now ? 'vc-past' : ''
if (e.multidate === true) {
attributes.push({
dates: { start: new Date(e.start_datetime * 1000), end: new Date(e.end_datetime * 1000) },
highlight: {
start: { fillMode: 'outline' },
base: { fillMode: 'light' },
end: { fillMode: 'outline' },
}
})
continue
}
const i = attributes.find(a => a.day === key)
if (!i) {
attributes.push({
day: key, key: e.id, n: 1, dates: new Date(e.start_datetime * 1000),
dot: { color: 'teal', class: c }
})
continue
}
i.n++
if (i.n >= 20) {
i.dot = { color: 'purple', class: c }
} else if (i.n >= 10) {
i.dot = { color: 'red', class: c }
} else if (i.n >= 5) {
i.dot = { color: 'orange', class: c }
} else if (i.n >= 3) {
i.dot = { color: 'yellow', class: c }
} else {
i.dot = { color: 'teal', class: c }
}
}
// add a bar to highlight today
attributes.push({ key: 'today', dates: new Date(), highlight: { color: 'green', fillMode: 'outline' } })
return attributes
}

View file

@ -108,7 +108,11 @@ li {
/* margin-top: 16px;
margin-right: 16px; */
transition: all .5s;
min-height: 288px;
overflow: hidden;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
border-radius: 4px;
}
.event .title {
@ -127,11 +131,10 @@ li {
flex: 1 1 auto;
}
.event .place span {
white-space: nowrap;
/* .event .place span {
overflow: hidden;
text-overflow: ellipsis;
}
} */
.event a {
text-decoration: none;
@ -148,6 +151,7 @@ li {
#event {
max-width: 1200px;
overflow-wrap: break-word;
}
/* .tags .v-chip .v-chip__content {

View file

@ -9,29 +9,26 @@
@input='click'
@update:from-page='updatePage'
:locale='$i18n.locale'
:popover="{ visibility: 'click' }"
:attributes='attributes'
transition='fade'
aria-label='Calendar'
is-expanded
is-inline)
.calh.d-flex.justify-center.align-center(slot='placeholder')
v-progress-circular(indeterminate)
.calh.text-center(slot='placeholder')
v-progress-circular.mt-5(indeterminate color='primary')
</template>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import dayjs from 'dayjs'
import { mdiChevronDown, mdiClose } from '@mdi/js'
import { attributesFromEvents } from '../assets/helper'
export default {
name: 'Calendar',
data () {
const month = dayjs.tz().month() + 1
const year = dayjs.tz().year()
data ({$time}) {
const month = $time.currentMonth()
const year = $time.currentYear()
return {
mdiChevronDown, mdiClose,
selectedDate: null,
@ -42,15 +39,15 @@ export default {
...mapState(['settings', 'events']),
...mapGetters(['is_dark']),
attributes () {
return attributesFromEvents(this.events)
return this.$time.attributesFromEvents(this.events)
}
},
methods: {
updatePage (page) {
if (page.month !== this.page.month || page.year !== this.page.year) {
this.$root.$emit('monthchange', page)
this.page.month = page.month
this.page.year = page.year
this.$root.$emit('monthchange', page)
}
},
click (day) {

View file

@ -1,7 +1,7 @@
<template lang="pug">
v-col(cols=12)
.text-center
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-model='type' color='primary' @change='type => change("type", type)')
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if="!event.parentId && !event.recurrent" v-model='type' color='primary' @change='type => change("type", type)')
v-btn(value='normal' label="normal") {{ $t('event.normal') }}
v-btn(v-if='settings.allow_multidate_event' value='multidate' label='multidate') {{ $t('event.multidate') }}
v-btn(v-if='settings.allow_recurrent_event' value='recurrent' label="recurrent") {{ $t('event.recurrent') }}
@ -10,7 +10,6 @@ v-col(cols=12)
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if='type === "recurrent"' color='primary' :value='value.recurrent.frequency' @change='fq => change("frequency", fq)')
v-btn(v-for='f in frequencies' :key='f.value' :value='f.value') {{ f.text }}
client-only
.datePicker.mt-3
v-input(:value='fromDate' :rules="[$validators.required('common.when')]")
@ -23,10 +22,9 @@ v-col(cols=12)
:is-dark="is_dark"
is-inline
is-expanded
:min-date='type !== "recurrent" && new Date()')
//- template(#placeholder)
.d-flex.calh.justify-center(slot='placeholder')
v-progress-circular(indeterminate)
:min-date='new Date()')
.calh.text-center(slot='placeholder')
v-progress-circular(indeterminate color='primary')
div.text-center.mb-2(v-if='type === "recurrent"')
span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{ whenPatterns }}
@ -94,10 +92,9 @@ v-col(cols=12)
</template>
<script>
import dayjs from 'dayjs'
import { DateTime } from 'luxon'
import { mapState, mapActions, mapGetters } from 'vuex'
import List from '@/components/List'
import { attributesFromEvents } from '../assets/helper'
import { mdiClockTimeFourOutline, mdiClockTimeEightOutline, mdiClose } from '@mdi/js'
export default {
@ -127,34 +124,33 @@ export default {
fromDate () {
if (this.value.from) {
if (this.value.multidate) {
return ({ start: dayjs(this.value.from).toDate(), end: dayjs(this.value.due).toDate() })
return ({ start: new Date(this.value.from), end: new Date(this.value.due) })
} else {
return new Date(this.value.from)
}
}
},
todayEvents() {
const start = dayjs.tz(this.value.from).startOf('day').unix()
const end = dayjs.tz(this.value.from).endOf('day').unix()
const start = this.$time.startOfDay(this.value.from)
const end = this.$time.endOfDay(this.value.from)
return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
},
attributes() {
return attributesFromEvents(this.events.filter(e => e.id !== this.event.id))
return this.$time.attributesFromEvents(this.events.filter(e => e.id !== this.event.id))
},
whenPatterns() {
if (!this.value.from) { return }
const date = dayjs(this.value.from)
const date = DateTime.fromJSDate(this.value.from)
const freq = this.value.recurrent.frequency
const weekDay = date.format('dddd')
const weekDay = date.toFormat('EEEE')
if (freq === '1w' || freq === '2w') {
return this.$t(`event.recurrent_${freq}_days`, { days: weekDay }).toUpperCase()
} else if (freq === '1m' || freq === '2m') {
const monthDay = date.format('D')
const n = Math.floor((monthDay - 1) / 7) + 1
const n = Math.floor((date.day) / 7) + 1
const patterns = [
{ label: this.$t(`event.recurrent_${freq}_days`, { days: monthDay }), key: 'ordinal' }
{ label: this.$t(`event.recurrent_${freq}_days`, { days: date.day }), key: 'ordinal' }
// { label: this.$tc(`event.recurrent_${freq}_ordinal`, { n, days: weekDay }), key: 'weekday' }
]
@ -168,7 +164,7 @@ export default {
}
// if selected day is in last week, propose also this type of selection
const lastWeek = date.daysInMonth() - monthDay < 7
const lastWeek = date.endOf('month').day - date.day < 7
if (lastWeek) {
patterns.push(
{
@ -215,13 +211,14 @@ export default {
if (from && from.start) {
from = from.start
}
let due = this.value.due
let due = this.value.multidate ? null : this.value.due
if (due && due.start) {
due = due.start
}
this.$emit('input', { ...this.value, from, due, recurrent: null, multidate: false })
}
} else if (what === 'frequency') {
if (typeof value === 'undefined') { value = '1w' }
this.$emit('input', { ...this.value, recurrent: { ...this.value.recurrent, frequency: value } })
} else if (what === 'recurrentType') {
this.$emit('input', { ...this.value, recurrent: { ...this.value.recurrent, type: value } })
@ -238,28 +235,22 @@ export default {
const [fromHour, fromMinute] = this.value.fromHour.split(':')
if (!this.value.multidate) {
if (hour < fromHour) {
this.value.due = dayjs(this.value.from).add(1, 'day').toDate()
this.value.due = DateTime.fromJSDate(this.value.from, {zone: this.settings.instance_timezone}).plus({day: 1}).toJSDate()
} else {
this.value.due = dayjs(this.value.from).toDate()
}
this.value.due = DateTime.fromJSDate(this.value.from, {zone: this.settings.instance_timezone}).toJSDate()
}
} else {
this.value.due = DateTime.fromJSDate(this.value.due, {zone: this.settings.instance_timezone}).set({ hour, minute }).toJSDate()
}
} else {
if (!this.value.multidate) {
this.value.due = null
} else {
this.value.due = DateTime.fromJSDate(this.value.due, {zone: this.settings.instance_timezone}).set({ hour: 23, minute:59 }).toJSDate()
}
}
this.$emit('input', { ...this.value, dueHour: value })
// if (value) {
// // const [hour, minute] = value.split(':')
// // let due = dayjs.tz(this.value.due || this.value.from).hour(Number(hour)).minute(Number(minute)).second(0)
// // add a day
// // if (dayjs(this.value.from).hour() > Number(hour) && !this.value.multidate) {
// // due = due.add(1, 'day')
// // }
// // due = due.hour(hour).minute(minute).second(0)
// } else {
// this.$emit('input', { ...this.value, dueHour: null })
// }
// change date in calendar (could be a range or a recurrent event...)
} else if (what === 'date') {
if (value === null) {

View file

@ -224,7 +224,7 @@ export default {
transition: opacity .5s;
opacity: 0;
visibility: hidden;
overflow: scroll;
overflow: auto;
// position: absolute;
}

View file

@ -1,13 +1,13 @@
<template lang="pug">
v-card.h-event.event.d-flex(itemscope itemtype="https://schema.org/Event")
div.h-event(itemscope itemtype="https://schema.org/Event")
nuxt-link(:to='`/event/${event.slug || event.id}`' itemprop="url")
MyPicture(v-if='!hide_thumbs' :event='event' thumb :lazy='lazy')
v-icon.float-right.mr-1(v-if='event.parentId' color='success' v-text='mdiRepeat')
.title.p-name(itemprop="name") {{ event.title }}
v-card-text.body.pt-0.pb-0
time.dt-start.subtitle-1(:datetime='event.start_datetime | unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime | unixFormat('YYYY-MM-DDTHH:mm')") <v-icon v-text='mdiCalendar'></v-icon> {{ event | when }}
.d-none.dt-end(v-if='event.end_datetime' itemprop="endDate" :content="event.end_datetime | unixFormat('YYYY-MM-DDTHH:mm')") {{ event.end_datetime | unixFormat('YYYY-MM-DD HH:mm') }}
time.dt-start.subtitle-1(:datetime='$time.unixFormat(event.start_datetime, "yyyy-MM-dd HH:mm")' itemprop="startDate" :content="$time.unixFormat(event.start_datetime, \"yyyy-MM-dd'T'HH:mm\")") <v-icon v-text='mdiCalendar'></v-icon> {{ $time.when(event) }}
.d-none.dt-end(v-if='event.end_datetime' itemprop="endDate" :content="$time.unixFormat(event.end_datetime,\"yyyy-MM-dd'T'HH:mm\")") {{ $time.unixFormat(event.end_datetime)}}
nuxt-link.place.d-block.p-location.pl-0(text :to='`/place/${encodeURIComponent(event.place.name)}`' itemprop="location" itemscope itemtype="https://schema.org/Place") <v-icon v-text='mdiMapMarker'></v-icon> <span itemprop='name'>{{ event.place.name }}</span>
.d-none(itemprop='address') {{ event.place.address }}
@ -22,7 +22,7 @@ import MyPicture from '~/components/MyPicture'
import { mdiRepeat, mdiCalendar, mdiMapMarker } from '@mdi/js'
export default {
data({ $store }) {
data() {
return { mdiRepeat, mdiMapMarker, mdiCalendar }
},
components: {

View file

@ -9,7 +9,7 @@ div#list
v-for='event in computedEvents'
:key='`${event.id}_${event.start_datetime}`' small)
v-list-item-content
v-list-item-subtitle <v-icon small color='success' v-if='event.parentId' v-text='mdiRepeat'></v-icon> {{event|when}}
v-list-item-subtitle <v-icon small color='success' v-if='event.parentId' v-text='mdiRepeat'></v-icon> {{$time.when(event)}}
span.primary--text.ml-1 @{{event.place.name}}
v-list-item-title(v-text='event.title')
</template>

View file

@ -5,11 +5,12 @@
client-only(v-if='showSearchBar')
v-menu(offset-y :close-on-content-click='false' tile)
template(v-slot:activator="{on ,attrs}")
v-text-field(hide-details outlined
:placeholder='$t("common.search")'
@input="v => setFilter(['query', v])" clearable :clear-icon='mdiClose')
template(v-slot:append v-if='settings.allow_recurrent_event || settings.allow_multidate_event')
v-icon(v-text='mdiCog' v-bind='attrs' v-on='on')
v-text-field(hide-details outlined v-model='query'
:placeholder='$t("common.search")' @click:clear="setFilter(['query', null])"
@keypress.enter="setFilter(['query', query])" clearable :clear-icon='mdiClose')
template(v-slot:append)
v-icon.mr-2(v-if='query' v-text='mdiMagnify' @click="setFilter(['query', query])")
v-icon(v-if='settings.allow_recurrent_event || settings.allow_multidate_event' v-text='mdiCog' v-bind='attrs' v-on='on')
v-card(outlined :rounded='"0"')
v-card-text
v-row(dense)
@ -36,12 +37,12 @@
<script>
import { mapState, mapActions } from 'vuex'
import Calendar from '@/components/Calendar'
import { mdiClose, mdiCog } from '@mdi/js'
import { mdiClose, mdiCog, mdiMagnify } from '@mdi/js'
export default {
data: ({ $store }) => ({
oldRoute: '',
mdiClose, mdiCog,
mdiClose, mdiCog, mdiMagnify,
collections: [],
show_recurrent: $store.state.settings.recurrent_event_visible,
show_multidate: true,

View file

@ -9,7 +9,7 @@ v-container
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:items='unconfirmedEvents'
:headers='headers')
template(v-slot:item.when='{ item }') {{item|when}}
template(v-slot:item.when='{ item }') {{$time.when(item)}}
template(v-slot:item.actions='{ item }')
v-btn(text small @click='confirm(item)' color='success') {{$t('common.confirm')}}
v-btn(text small :to='`/event/${item.slug || item.id}`' color='success') {{$t('common.preview')}}

View file

@ -41,7 +41,7 @@ v-container
template(v-slot:item.content='{ item }')
span(v-html='item.data.content')
template(v-slot:item.created='{ item }')
span {{item.created | dateFormat('lll')}}
span {{$time.format(item.created, 'ff')}}
template(v-slot:item.user='{ item }')
a(:href='item.ap_user.url || item.ap_user.ap_id' target='_blank') {{item.ap_user.preferredUsername}}
template(v-slot:item.event='{ item }')

View file

@ -28,7 +28,7 @@ v-container
:disable='!valid || loading') {{ $t('common.save') }}
v-card-text
v-card(v-for='plugin in plugins' :key='plugin.name' max-width="400" elevation='10' color='secondary' dark)
v-card(v-for='plugin in plugins' :key='plugin.name' max-width="400" elevation='10')
v-card-title {{ plugin.name }}
v-card-text
p {{ plugin.description }}

View file

@ -79,7 +79,7 @@ v-container
import SMTP from './SMTP.vue'
import Geolocation from './Geolocation.vue'
import { mapActions, mapState } from 'vuex'
import moment from 'dayjs'
import { DateTime } from 'luxon'
import tzNames from './tz.json'
import { mdiAlert, mdiArrowRight, mdiMap } from '@mdi/js'
const locales = require('../../locales/index')
@ -147,7 +147,7 @@ export default {
}
},
filteredTimezones () {
const current_timezone = moment.tz.guess()
const current_timezone = DateTime.local().zoneName
tzNames.unshift(current_timezone)
return tzNames
}

View file

@ -52,22 +52,39 @@ v-container
v-img.mt-2(:src='`/headerimage.png?${headerImageKey}`' max-height="150px" contain)
v-card-title {{$t('admin.colors')}}
//- choose theme colors
v-card-text
v-theme-provider(dark)
v-card(max-width='465')
v-card-text
span.mr-2(v-for='(color, i) in settings.dark_colors' :key='i')
v-menu(v-model='dark_menu[i]'
:close-on-content-click="false"
transition="slide-y-transition"
offset-y
top right
max-width="290px"
min-width="290px")
template(v-slot:activator='{ on }')
v-btn(:color='color' dark small v-on='on') {{i}}
v-color-picker(mode='hexa' :value='color' @update:color='c => updateColor("dark", i, c)')
v-theme-provider(light)
v-card.mt-4(max-width='465')
v-card-text
span.mr-2(v-for='(color, i) in settings.light_colors' :key='i')
v-menu(v-model='light_menu[i]'
:close-on-content-click="false"
transition="slide-y-transition"
offset-y
top right
max-width="290px"
min-width="290px")
template(v-slot:activator='{ on }')
v-btn(:color='color' small v-on='on') {{i}}
v-color-picker(mode='hexa' :value='color' @update:color='c => updateColor("light", i, c)')
//- TODO choose theme colors
//- v-row
//- v-col(v-for='(color, i) in colors' :key='i')
//- v-menu(v-model='menu[i]'
//- :close-on-content-click="false"
//- transition="slide-x-transition"
//- offset-y
//- absolute
//- bottom
//- max-width="290px"
//- min-width="290px")
//- template(v-slot:activator='{ on }')
//- v-btn(:color='i' small
//- v-on='on') {{i}}
//- v-color-picker(light @update:color='c => updateColor(i, c)')
v-dialog(v-model='linkModal' width='500' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card
@ -106,10 +123,11 @@ v-container
<script>
import { mapActions, mapState } from 'vuex'
import { mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp } from '@mdi/js'
import debounce from 'lodash/debounce'
export default {
name: 'Theme',
data () {
data ({ $store }) {
const t = new Date().getMilliseconds()
return {
mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp,
@ -118,24 +136,19 @@ export default {
fallbackImageKey: t,
headerImageKey: t,
link: { href: '', label: '' },
linkModal: false
// menu: [false, false, false, false]
// colors: { primary: '', secondary: '', accent: '', error: '', info: '', success: '', warning: '' }
// primary: {},
// secondary: {}
// }
linkModal: false,
dark_menu: [false, false, false, false],
light_menu: [false, false, false, false],
}
},
computed: {
...mapState(['settings']),
// 'colors.primary': this.color('primary'),
// 'colors.secondary': this.color('primary'),
// 'colors.tertiary': this.color('primary'),
is_dark: {
get () { return this.settings['theme.is_dark'] },
set (value) {
this.$vuetify.theme.dark = value
this.setSetting({ key: 'theme.is_dark', value })
this.setLocalSetting({ key: 'theme.is_dark', value })
}
},
hide_thumbs: {
@ -145,25 +158,10 @@ export default {
hide_calendar: {
get () { return this.settings.hide_calendar },
set (value) { this.setSetting({ key: 'hide_calendar', value }) }
},
// 'colors[0]': {
// get () {
// return this.settings['theme.colors'] || [0, 0]
// },
// set (value) {
// console.error(value)
// if (!value) { return }
// this.setSetting({ key: 'theme.primary', value })
// if (this.settings['theme.is_dark']) {
// this.$vuetify.theme.themes.dark.primary = value
// } else {
// this.$vuetify.theme.themes.light.primary = value
// }
// }
// }
}
},
methods: {
...mapActions(['setSetting']),
...mapActions(['setSetting', 'setLocalSetting']),
reset () {
this.setSetting({
key: 'footerLinks',
@ -197,12 +195,15 @@ export default {
.then(this.forceHeaderImageReload)
e.stopPropagation()
},
updateColor (i, v) {
this.colors[i] = v.hex
this.$vuetify.theme.themes.dark[i] = v.hex
updateSettingColor: debounce( async function (theme, color, value) {
const key = `${theme}_colors`
this.setSetting({ key, value: { ...this.settings[key], [color]: value.hex } })
}, 200),
updateColor (theme, color, value) {
this.$vuetify.theme.themes[theme][color] = value.hex
this.updateSettingColor(theme, color, value)
},
openLinkModal () {
// this.link = { href: '', label: '' }
this.linkModal = true
this.$nextTick(() => this.$refs.linkModalForm.reset())
},

View file

@ -12,7 +12,7 @@ v-container
//- ADD NEW USER
v-dialog(v-model='newUserDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card(color='secondary')
v-card
v-card-title {{$t('common.new_user')}}
v-card-text
v-form(v-model='valid' ref='user_form' lazy-validation @submit.prevent='createUser')

View file

@ -1,6 +1,6 @@
<template lang='pug'>
span
v-list(dense nav)
v-list(dense nav color='transparent')
v-list-group(:append-icon='mdiChevronUp' :value='true')
template(v-slot:activator)
v-list-item.text-overline {{$t('common.admin_actions')}}
@ -11,7 +11,7 @@ span
v-icon(v-if='event.is_visible' v-text='mdiEyeOff')
v-icon(v-else='event.is_visible' v-text='mdiEye')
v-list-item-content
v-list-item-title(v-text="$t(`common.${event.is_visible?'hide':'confirm'}`)")
v-list-item-title(v-text="$t(`common.${event.is_visible?(event.parentId?'skip':'hide'):'confirm'}`)")
//- Edit event
v-list-item(:to='`/add/${event.id}`')
@ -21,7 +21,7 @@ span
v-list-item-title(v-text="$t('common.edit')")
//- Remove
v-list-item(@click='remove(false)')
v-list-item(v-if='!event.parentId' @click='remove(false)')
v-list-item-icon
v-icon(v-text='mdiDelete')
v-list-item-content

2
docs/.bundle/config Normal file
View file

@ -0,0 +1,2 @@
---
BUNDLE_PATH: "vendor/bundle"

View file

@ -8,6 +8,35 @@ nav_order: 10
All notable changes to this project will be documented in this file.
### 1.6.8
- use new $time in admins table, fix #252
- fix some WPGancio issues
- new custom color feature!
- update locales (durch, spanish, galician)
- improve index, tag, place page layout
- use luxon instead of dayjs server side, fix #254
### 1.6.7 - 22 mar '23
- node is not Intl ready, fix #250
- fix old smultidate events without an end_datetime!
- fix a small issue during setup
### 1.6.5 - 21 mar '23
- optimize home page using lazy loading
- add support for server side http proxy (thanks @sedum)
- add Duch (nl) locale (thanks @jeoenepraat)
- fix #244, dark theme user / admin preference merge issue
- fix some issues with recurrent events, #247
- improve search flow (order by, press enter or icon...)
- filters / helpers refactoring
- tag and place pages list all events (past too)
- show boosts/bookmarks, fix #241
- go to event on save/update
- use luxon instead of dayjs, new $time plugin
- fix event import from URL
- remove an annoying warning from logs
### 1.6.4 - 22 feb '23
- add missing i18n during setup
- really fix #232

View file

@ -10,7 +10,7 @@ permalink: /
A shared agenda for local communities.
{: .fs-6 }
Last release **[1.6.3 - 17 Feb 2023](/changelog#163---17-feb-23)**
Last release **[1.6.8 - 30 Mar 2023](/changelog#168---30-mar-23)**
[Install]({% link install/install.md %}){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
[Demo](https://demo.gancio.org){: .btn .btn-green .fs-5 .mb-4 .mb-md-0 .mr-2 }

View file

@ -67,3 +67,24 @@ list of strings you can override.
> warning "Restart needed"
> Note that a restart is needed when you change user_locale's content.
### Proxy
Proxy outward request from gancio.
Look [here](https://www.npmjs.com/package/https-proxy-agent) for options.
Note: `hostname` option takes precedence over `host`.
```json
"proxy": {
"protocol": "http:",
"hostname": "",
"host": "127.0.0.1",
"port": "8118",
"auth": {
"username": "user",
"password": "password"
},
"headers": {
"X-Proxy-Header": "example"
}
}
```

View file

@ -1 +1,5 @@
{}
{
"register": {
"subject": "Registratieverzoek ontvangen"
}
}

View file

@ -162,8 +162,8 @@
"recurrent_2w_days": "A {days} every other",
"recurrent_1m_days": "The {days} of each month",
"recurrent_2m_days": "The {days} a month every other",
"recurrent_1m_ordinal": "The {n} {days} of each month",
"recurrent_2m_ordinal": "The {n} {days} a month every other",
"recurrent_1m_ordinal": "Each {n} {days} of the month",
"recurrent_2m_ordinal": "Each {n} {days} a month every other",
"each_week": "Each week",
"each_2w": "Every other weeks",
"each_month": "Each month",
@ -309,7 +309,8 @@
"tilelayer_test_error": "The tilelayer service is not reachable at {service_name}",
"geolocation": "Geolocation",
"allow_event_only_online": "Allow event only online",
"allow_event_also_online": "Allow event also online"
"allow_event_also_online": "Allow event also online",
"colors": "Colors"
},
"auth": {
"not_confirmed": "Not confirmed yet…",

View file

@ -156,8 +156,8 @@
"recurrent_2w_days": "Un {days} cada dos",
"recurrent_1m_days": "El día {days} de cada mes",
"recurrent_2m_days": "El día {days} cada dos meses",
"recurrent_1m_ordinal": "El {n} {days} de cada mes",
"recurrent_2m_ordinal": "El {n} {days} un mes sí y el otro no",
"recurrent_1m_ordinal": "Cada {n} {days} del mes",
"recurrent_2m_ordinal": "Cada {n} {días} un mes de cada dos",
"each_week": "Cada semana",
"each_2w": "Cada dos semanas",
"each_month": "Cada mes",
@ -301,7 +301,8 @@
"edit_tag": "Editar la etiqueta",
"edit_tag_help": "Puede cambiar la etiqueta sustituyéndola por una nueva o fusionándola con una existente. Los {n} eventos asociados también se modificarán.",
"geolocation_description": "<b>1. Defina un proveedor para el servicio de codificación geográfica</b>.<br>Actualmente, entre los enumerados en el <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki de OpenStreetMap </a>, hay soporte para el software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> y <a href=\"https://github.com/komoot /photon\">Photon</a>.<br>Puede usar una de las demostraciones oficiales relacionadas copiando el enlace en el campo 'Proveedor de codificación geográfica':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Términos de servicio</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\"> Condiciones de servicio</a>)</li></ul><br><b>2. Defina un proveedor para las capas del mapa.</b><br>Puede encontrar una lista de ellos aquí: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"geocoding_test_error": "No se puede acceder al servicio de geocodificación en {service_name}"
"geocoding_test_error": "No se puede acceder al servicio de geocodificación en {service_name}",
"colors": "Colores"
},
"auth": {
"not_confirmed": "Todavía no hemos confirmado este email…",

View file

@ -294,7 +294,8 @@
"edit_tag": "Editar etiqueta",
"edit_tag_help": "Podes cambiar a etiqueta substituíndoa por unha nova ou fusionándoa cunha existente. Tamén se cambiarán os {n} eventos asociados.",
"delete_tag_confirm": "Estás seguro de que queres eliminar a etiqueta \"{tag}\"? A etiqueta eliminarase de {n} eventos.",
"delete_collection_confirm": "Estás seguro de que queres eliminar a colección <u>{collection}</u>?"
"delete_collection_confirm": "Estás seguro de que queres eliminar a colección <u>{collection}</u>?",
"colors": "Cores"
},
"auth": {
"not_confirmed": "Aínda non foi confirmado…",

View file

@ -8,6 +8,7 @@ module.exports = {
gl: 'Galego',
it: 'Italiano',
nb: 'Norwegian Bokmål',
nl: 'Dutch',
pl: 'Polski',
pt: 'Português',
ru: 'Русский',

View file

@ -96,7 +96,9 @@
"getting_there": "Come arrivare",
"calendar": "Calendario",
"home": "Home",
"about": "Cos'è"
"about": "Cos'è",
"admin_actions": "Gestisci",
"recurring_event_actions": "Gestisci ricorrenza"
},
"login": {
"description": "Entrando puoi pubblicare nuovi eventi.",
@ -158,8 +160,8 @@
"recurrent_2w_days": "Un {days} ogni due",
"recurrent_1m_days": "Il giorno {days} di ogni mese",
"recurrent_2m_days": "Il giorno {days} ogni due mesi",
"recurrent_1m_ordinal": "Il {n} {days} di ogni mese",
"recurrent_2m_ordinal": "Il {n} {days} un mese sì e uno no",
"recurrent_1m_ordinal": "Ogni {n} {days} del mese",
"recurrent_2m_ordinal": "Ogni {n} {days} un mese sì e uno no",
"each_week": "Ogni settimana",
"each_2w": "Ogni due settimane",
"each_month": "Ogni mese",
@ -191,6 +193,7 @@
"allow_registration_description": "Vuoi abilitare la registrazione?",
"allow_anon_event": "Si possono inserire eventi anonimi (previa conferma)?",
"allow_recurrent_event": "Abilita eventi ricorrenti",
"allow_multidate_event": "Abilita eventi di più giorni",
"allow_geolocation": "Abilita la geolocalizzazione degli eventi",
"recurrent_event_visible": "Appuntamenti ricorrenti visibili di default",
"federation": "Federazione / ActivityPub",
@ -283,7 +286,8 @@
"tilelayer_test_button": "Test tilelayer",
"tilelayer_test_success": "Il servizio di tilelayer all'indirizzo {service_name} sta funzionando",
"tilelayer_test_error": "Il servizio non è raggiungibile all'indirizzo: {service_name}",
"geolocation": "Geo e mappe"
"geolocation": "Geo e mappe",
"colors": "Colori"
},
"auth": {
"not_confirmed": "Non ancora confermato…",

View file

@ -1 +1,332 @@
{}
{
"admin": {
"block_user": "Gebruiker verwijderen",
"filter_users": "Gebruikers filteren",
"add_link": "Link toevoegen",
"blocked": "Geblokkeerd",
"delete_user": "Verwijderen",
"remove_admin": "Beheerder verwijderen",
"user_remove_ok": "Gebruiker verwijderd",
"user_create_ok": "Gebruiker geregistreerd",
"announcement_remove_ok": "Mededeling verwijderd",
"instance_locale": "Standaardtaal",
"new_announcement": "Nieuwe mededeling",
"show_smtp_setup": "E-mailinstellingen",
"smtp_port": "SMTP-poort",
"config_plugin": "Pluginconfiguratie",
"event_remove_ok": "Evenement verwijderd",
"select_instance_timezone": "Tijdzone",
"add_instance": "Server toevoegen",
"hide_boost_bookmark": "Boost/favorieten verbergen",
"instance_name": "Servernaam",
"show_resource": "Reactie tonen",
"hide_resource": "Reactie verbergen",
"delete_resource": "Reactie verwijderen",
"filter_instances": "Servers filteren",
"is_dark": "Donker thema",
"footer_links": "Footerlinks",
"edit_place": "Locatie bewerken",
"smtp_hostname": "SMTP-server",
"domain": "Domein",
"tilelayer_provider_attribution": "Naamsvermelding",
"geolocation": "Geolocatie",
"trusted_instances_label_default": "Vriendschappelijke servers",
"edit_collection": "Collectie bewerken",
"new_collection": "Nieuwe collectie",
"smtp_use_sendmail": "Sendmail gebruiken",
"resources": "Reacties",
"favicon": "Logo",
"user_blocked": "Gebruiker {user} geblokkeerd",
"user_block_confirm": "Weet je zeker dat je de gebruiker {user} wilt verwijderen?",
"instance_block_confirm": "Weet je zeker dat je de server {instance} wilt verwijderen?",
"instance_place": "Naam van deze server",
"description_description": "Valt in de header, onder de naam van de server te lezen",
"smtp_test_button": "Test-e-mail verzenden",
"widget": "Widget",
"smtp_test_success": "Er is een test-e-mail naar {admin_email} verstuurd. Check je inbox!",
"event_confirm_description": "Je kunt hier anonieme evenementen beoordelen",
"allow_geolocation": "Geolocatie voor evenementen toestaan",
"allow_multidate_event": "Evenementen voor meerdere dagen toestaan",
"allow_recurrent_event": "Herhalende evenementen toestaan",
"enable_admin_user_confirm": "Weet je zeker dat je {user} beheerdersrechten wilt geven?",
"delete_tag_confirm": "Weet je zeker dat je de tag \"{tag}\" wilt verwijderen? De tag wordt uit {n} evenementen verwijderd.",
"delete_announcement_confirm": "Weet je zeker dat je de mededeling wilt verwijderen?",
"announcement_description": "Hier kun je mededelingen kwijt die op de startpagina worden getoond",
"add_trusted_instance": "Vriendschappelijke server toevoegen",
"trusted_instances_label_help": "Het standaardlabel is 'Vriendschappelijke servers'",
"edit_tag": "Tag bewerken",
"instance_place_help": "De naam van deze server die op andere servers wordt getoond",
"smtp_secure": "SMTP Secure (TLS of STARTTLS)",
"admin_email": "E-mailadres beheerder",
"delete_collection_confirm": "Weet je zeker dat je de collectie <u>{collection}</u> wilt verwijderen?",
"fallback_image": "Terugvalflyer",
"header_image": "Header",
"default_images_help": "Je moet de pagina <a href='/admin?tab=theme'>vernieuwen</a> om de veranderingen te kunnen zien.",
"known_users": "Bekende gebruikers",
"created_at": "Aangemaakt op",
"geocoding_test_button": "Geocoding testen",
"tilelayer_test_success": "De tilelayer-dienst op {service_name} werkt",
"tilelayer_test_error": "De tilelayer-dienst is niet bereikbaar op {service_name}",
"disable_user_confirm": "Weet je zeker dat je {user} wilt uitschakelen?",
"disable_admin_user_confirm": "Weet je zeker dat je de beheerdersrechten van {user} wilt verwijderen?",
"delete_user_confirm": "Weet je zeker dat je {user} wilt verwijderen?",
"allow_anon_event": "Anonieme evenementen toestaan (moeten worden beoordeeld)?",
"allow_registration_description": "Open registraties toestaan?",
"enable_federation_help": "Het is mogelijk om deze server vanaf de fediverse te volgen",
"federation": "Federatie / ActivityPub",
"enable_federation": "Federatie inschakelen",
"recurrent_event_visible": "Herhalende evenementen standaard tonen",
"enable_resources": "Reacties inschakelen",
"enable_resources_help": "Reacties op het evenement vanuit de fediverse toestaan",
"block": "Blokkeren",
"unblock": "Deblokkeren",
"user_add_help": "Een e-mail met instructies hoe het account te bevestigen en hoe een wachtwoord in te stellen, wordt naar de nieuwe gebruiker gestuurd",
"delete_resource_confirm": "Weet je zeker dat je deze reactie wilt verwijderen?",
"instance_name_help": "Account om via ActivityPub te volgen",
"trusted_instances_label": "Alternatief navigatielabel voor vriendschappelijke servers",
"enable_trusted_instances": "Vriendschappelijke servers inschakelen",
"trusted_instances_help": "De lijst met vriendschappelijke servers wordt onder een menu in de footer getoond",
"delete_trusted_instance_confirm": "Weet je zeker dat je dit item uit het menu met vriendschappelijke servers wilt verwijderen?",
"delete_footer_link_confirm": "Wil je deze link verwijderen?",
"hide_thumbs": "Thumbnails verbergen",
"default_images": "Standaardafbeeldingen",
"hide_calendar": "Agenda verbergen",
"geocoding_provider_type_help": "De standaardsoftware is Nominatim",
"geocoding_provider_type": "Geocodingsoftware",
"geocoding_provider_help": "De standaardprovider is Nominatim",
"geocoding_provider": "Geocodingprovider",
"geocoding_countrycodes": "Landcodes",
"geocoding_countrycodes_help": "Hiermee kun je alleen filteren op bepaalde landen",
"tilelayer_provider": "Tilelayer-provider",
"tilelayer_provider_help": "De standaardprovider is OpenStreetMap",
"geocoding_test_error": "De geocodingdienst is niet bereikbaar op {service_name}",
"geocoding_test_success": "De geocodingdienst op {service_name} werkt",
"tilelayer_test_button": "Tilelayer testen"
},
"ordinal": {
"4": "vierde",
"1": "eerste",
"3": "derde",
"-1": "laatste",
"2": "tweede",
"5": "vijfde"
},
"common": {
"plugins": "Plugins",
"help_translate": "Help vertalen",
"tag": "Tag",
"close": "Sluiten",
"show_map": "Kaart tonen",
"calendar": "Agenda",
"home": "Home",
"about": "Over",
"content": "Inhoud",
"recurring_event_actions": "Herhalende evenementacties",
"admin_actions": "Beheeracties",
"add_event": "Evenement toevoegen",
"next": "Volgende",
"export": "Exporteren",
"send": "Verzenden",
"where": "Waar",
"address": "Adres",
"when": "Wanneer",
"what": "Wat",
"media": "Media",
"login": "Inloggen",
"email": "E-mail",
"password": "Wachtwoord",
"register": "Registreren",
"description": "Omschrijving",
"remove": "Verwijderen",
"hide": "Verbergen",
"search": "Zoeken",
"edit": "Bewerken",
"info": "Info",
"confirm": "Goedkeuren",
"admin": "Beheerder",
"users": "Gebruikers",
"events": "Evenementen",
"places": "Locaties",
"settings": "Opties",
"actions": "Acties",
"deactivate": "Uitschakelen",
"remove_admin": "Beheerder verwijderen",
"activate": "Activeren",
"save": "Opslaan",
"preview": "Voorbeeld",
"logout": "Uitloggen",
"share": "Delen",
"name": "Naam",
"associate": "Associate",
"edit_event": "Evenement bewerken",
"related": "Gerelateerd",
"add": "Toevoegen",
"logout_ok": "Uitgelogd",
"copy": "Kopiëren",
"recover_password": "Wachtwoord herstellen",
"new_password": "Nieuw wachtwoord",
"new_user": "Nieuwe gebruiker",
"ok": "OK",
"cancel": "Annuleren",
"enable": "Inschakelen",
"disable": "Uitschakelen",
"me": "Jij",
"password_updated": "Wachtwoord veranderd.",
"resources": "Reacties",
"n_resources": "geen reactie|één reactie|{n} reacties",
"activate_user": "Goedgekeurd",
"displayname": "Weergavenaam",
"federation": "Federatie",
"set_password": "Wachtwoord instellen",
"copy_link": "Link kopiëren",
"send_via_mail": "E-mail verzenden",
"add_to_calendar": "Aan agenda toevoegen",
"instances": "Servers",
"copied": "Gekopieerd",
"embed": "Embedden",
"embed_title": "Embed dit evenement op jouw website",
"embed_help": "Kopieer en plak de volgende code op jouw website en het evenement wordt zoals hier daar getoond",
"max_events": "Max. aantal evenementen",
"feed": "RSS-feed",
"feed_url_copied": "Open de gekopieerde feed-URL in jouw RSS-feedreader",
"follow_me_title": "Volg updates op de fediverse",
"follow": "Volgen",
"moderation": "Moderatie",
"user": "Gebruiker",
"authorize": "Goedkeuren",
"title": "Naam",
"filter": "Filter",
"event": "Evenement",
"pause": "Pauze",
"start": "Start",
"fediverse": "Fediverse",
"skip": "Overslaan",
"delete": "Verwijderen",
"announcements": "Mededelingen",
"url": "URL",
"place": "Locatie",
"tags": "Tags",
"theme": "Thema",
"reset": "Resetten",
"import": "Importeren",
"label": "Label",
"collections": "Collecties"
},
"login": {
"not_registered": "Niet geregistreerd?",
"error": "Kon niet inloggen. Controleer jouw inloginformatie.",
"ok": "Ingelogd",
"insert_email": "Vul je e-mailadres in.",
"description": "Door in te loggen kun je nieuwe evenementen aanmaken.",
"check_email": "Controleer de inbox en spamfolder van jouw e-mail.",
"forgot_password": "Jouw wachtwoord vergeten?"
},
"recover": {
"not_valid_code": "Er ging iets fout."
},
"export": {
"email_description": "Je kunt evenementen die jou interesseren naar je laten e-mailen.",
"insert_your_address": "Vul je e-mailadres in",
"intro": "In tegenstelling tot asociale platformen die alles doen om gebruikers en hun gegevens voor zichzelf te houden, geloven wij dat informatie, zoals mensen, vrij moet zijn. Daarom kun je op te hoogte blijven van de door jou gewenste evenementen, zonder verplicht door deze website te hoeven ploegen.",
"feed_description": "Om updates te volgen met een computer of smartphone, zonder regelmatig deze website te hoeven bezoeken, kun je RSS-feeds gebruiken.</p>\n\n<p> Om RSS-feeds te kunnen bekijken gebruik je een speciale app om updates te ontvangen van websites die jou interesseren. Het is een goede manier om veel websites snel te kunnen volgen, zonder een account aan te hoeven maken of andere tijdrovende dingen te hoeven doen. </p>\n\n<li> Wanneer je Android gebruikt raden wij <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> of Feeder aan</li>\n<li> Voor iPhone / iPad kun je <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> gebruiken </li>\n<li> Voor een desktop / laptop raden wij Feedbro aan, die je dient toe te voegen aan <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> of <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nDoor het toevoegen van deze link aan jouw RSS-feedreader blijf je up-to-date.",
"ical_description": "Computers en smartphones hebben meestal een agenda-applicatie die in staat is om een externe agenda te importeren.",
"list_description": "Wanneer je een website hebt en je wilt een lijst met evenementen tonen, gebruik dan de volgende code"
},
"event": {
"recurrent": "Herhalend",
"only_future": "alleen toekomstige evenementen",
"recurrent_description": "Kies de frequentie en selecteer dagen",
"multidate": "Meer dagen",
"normal": "Normaal",
"import_ICS": "Vanuit ICS-bestand importeren",
"alt_text_description": "Omschrijving voor blinde of slechtziende mensen",
"what_description": "Titel",
"description_description": "Omschrijving",
"tag_description": "Tag",
"media_description": "Je kunt een flyer toevoegen (optioneel)",
"added": "Evenement toegevoegd",
"saved": "Evenement opgeslagen",
"added_anon": "Evenement toegevoegd, maar moet nog wel worden beoordeeld.",
"where_description": "Waar is het evenement? Je kunt de locatie aanmaken wanneer deze nog niet aanwezig is.",
"address_description": "Wat is het adres?",
"address_description_osm": "Wat is het adres? (door <a href='https://osm.org/copyright'>OpenStreetMap</a>-bijdragers)",
"confirmed": "Evenement goedgekeurd",
"not_found": "Kon het evenement niet vinden",
"remove_confirmation": "Weet je zeker dat je dit evenement wilt verwijderen?",
"edit_recurrent": "Herhalend evenement bewerken:",
"show_recurrent": "herhalende evenementen",
"show_past": "ook eerdere evenementen",
"ics": "ICS",
"anon_description": "Je kunt een evenement toevoegen zonder te registreren of in te loggen, maar je moet wel wachten dat iemand het leest\nen beoordeeld of het een geschikt evenement is. Het is niet mogelijk om het evenement daarna te wijzigen.<br/><br/>\nJe kunt in plaats daarvan ook <a href='/login'>inloggen</a> of <a href='/register'>registreren</a>. Ga anders gewoon je gang en krijg zo snel mogelijk een antwoord. ",
"show_multidate": "meerdaagse evenementen",
"multidate_description": "Is het een festival? Kies wanneer het begint en het eindigt",
"normal_description": "Kies de dag.",
"remove_recurrent_confirmation": "Weet je zeker dat je dit herhalende evenement wilt verwijderen?\nEerdere evenementen blijven bewaard, maar er worden geen nieuwe evenementen aangemaakt.",
"import_description": "Je kunt evenementen vanuit andere platformen en servers importeren met behulp van standaardformaten (ics en h-event)",
"choose_focal_point": "Kies het punt van aandacht",
"remove_media_confirmation": "Ga je akkoord met het verwijderen van de afbeelding?",
"download_flyer": "Flyer downloaden",
"same_day": "op dezelfde dag",
"anon": "Anoniem",
"updated": "Evenement bijgewerkt",
"recurrent_1w_days": "Elke {days}",
"recurrent_1m_days": "Dag {days} van elke maand",
"recurrent_1m_ordinal": "De {n} {days} van elke maand",
"recurrent_2m_days": "Dag {days} om de twee maanden",
"recurrent_2w_days": "Elke {days} om de twee weken",
"recurrent_2m_ordinal": "De {n} {days} om de twee maanden",
"each_week": "Elke week",
"each_2w": "Om de week",
"each_month": "Elke maand",
"due": "tot",
"from": "Van",
"image_too_big": "De afbeelding kan niet groter zijn dan 4MB",
"interact_with_me_at": "Heb interactie met mij op de fediverse via",
"interact_with_me": "Volg me",
"import_URL": "Vanuit URL importeren"
},
"confirm": {
"title": "Gebruiker beoordelen",
"not_valid": "Er ging iets mis.",
"valid": "Jouw account is goedgekeurd. Je kunt nu <a href=\"/login\">inloggen</a>"
},
"setup": {
"completed": "Instellen compleet",
"start": "Start",
"completed_description": "<p>Je kunt nu met de volgende gegevens inloggen:<br/><br/>Gebruiker: <b>{email}</b><br/>Wachtwoord: <b>{password}<b/></p>",
"copy_password_dialog": "Ja, je moet het wachtwoord kopiëren!"
},
"register": {
"complete": "Registraties moeten worden beoordeeld.",
"description": "Sociale bewegingen moeten zich organiseren en zichzelf financieren.<br/>\n<br/>Voordat je evenementen kunt aanmaken <strong>moet dit account worden goedgekeurd</strong>. Besef je dat <strong>achter deze website zich echt mensen bevinden</strong>, dus schrijf twee regels om ons te laten weten wat voor evenementen je wilt aanmaken.",
"error": "Fout: ",
"first_user": "Beheerder aangemaakt"
},
"settings": {
"danger_section": "Gevaarlijk onderdeel",
"password_updated": "Wachtwoord veranderd.",
"remove_account_confirm": "Je staat op het punt om je account permanent te verwijderen",
"update_confirm": "Wil je jouw wijziging opslaan?",
"change_password": "Jouw wachtwoord veranderen"
},
"auth": {
"fail": "Je kon niet inloggen. Weet je zeker dat je wachtwoord klopt?",
"not_confirmed": "Nog niet bevestigd…"
},
"validators": {
"required": "{fieldName} is vereist",
"email": "Een geldig e-mailadres invullen"
},
"about": "\n <p><a href='https://gancio.org'>Gancio</a> is een gedeelde agenda voor lokale community's.</p>\n ",
"oauth": {
"authorization_request": "De applicatie <code>{app}</code> vraagt om de volgende autorisatie voor <code>{instance_name}</code>:",
"scopes": {
"event:write": "Jouw evenementen toevoegen en bewerken"
},
"redirected_to": "Je wordt na bevestiging doorgestuurd naar <code>{url}</code>"
},
"error": {
"nick_taken": "Deze gebruikersnaam is al in gebruik.",
"email_taken": "Dit e-mailadres is al in gebruik."
}
}

26
modules/axios-proxy.js Normal file
View file

@ -0,0 +1,26 @@
const HttpsProxyAgent = require("https-proxy-agent"),
axios = require("axios"),
config = require('../server/config')
function axiosProxy (_moduleOptions) {
if (config.proxy && !(!config.proxy.hostname && !config.proxy.host)) {
const httpsAgent = new HttpsProxyAgent({
protocol: config.proxy.protocol,
hostname: config.proxy.hostname,
host: config.proxy.host,
port: config.proxy.port,
auth: config.proxy.auth.username + ':' + config.proxy.auth.password,
headers: config.proxy.headers
})
// Use axios as you normally would, but specify httpsAgent in the default configs
// https://github.com/nuxt-community/axios-module/pull/428#issuecomment-743313813
// Nuxt 2: https://github.com/axios/axios/issues/925#issuecomment-513028175
// Nuxt 3: https://github.com/unjs/ofetch#-adding-https-agent
axios.defaults.httpsAgent = httpsAgent
}
}
module.exports = axiosProxy

View file

@ -1,8 +1,6 @@
const config = require('./server/config.js')
const minifyTheme = require('minify-css-string').default
const locales = require('./locales/index')
import { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, ru, zhHans } from 'vuetify/lib/locale'
const isDev = (process.env.NODE_ENV !== 'production')
module.exports = {
@ -41,7 +39,8 @@ module.exports = {
** Plugins to load before mounting the App
*/
plugins: [
'@/plugins/filters', // text filters, datetime filters, generic transformation helpers etc.
'@/plugins/helpers',
'@/plugins/time', // datetime filters
'@/plugins/axios', // axios baseurl configuration
'@/plugins/validators', // inject validators
'@/plugins/api', // api helpers
@ -55,6 +54,7 @@ module.exports = {
modules: [
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/i18n',
'~/modules/axios-proxy.js', // Note: import this before @nuxtjs/axios to override defaults of both instances: `$axios` available in context, and `axios` used in controllers
'@nuxtjs/axios',
'@nuxtjs/auth',
'@nuxtjs/sitemap',
@ -140,27 +140,7 @@ module.exports = {
}
},
buildModules: ['@nuxtjs/vuetify'],
vuetify: {
lang: { locales: { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, ru, zhHans } },
treeShake: true,
theme: {
options: {
customProperties: false,
variations: false,
minifyTheme,
},
dark: true,
themes: {
dark: {
primary: '#FF6E40'
},
light: {
primary: '#FF4500'
}
}
},
defaultAssets: false
},
vuetify: { defaultAssets: false, optionsPath: './vuetify.options.js' },
build: {
extend(config, { isDev, isClient }) {
// ..

View file

@ -1,15 +1,15 @@
{
"name": "gancio",
"version": "1.6.4",
"version": "1.6.8",
"description": "A shared agenda for local communities",
"author": "lesion",
"scripts": {
"build": "nuxt build --modern",
"start:inspect": "NODE_ENV=production node --inspect node_modules/.bin/nuxt start --modern",
"dev": "nuxt dev",
"test-sqlite": "export NODE_ENV=test; export DB=sqlite; jest --testEnvironment=jest-environment-node --bail=1",
"test-mariadb": "export NODE_ENV=test; export DB=mariadb; jest --testEnvironment=jest-environment-node --bail=1",
"test-postgresql": "export NODE_ENV=test; export DB=postgresql; jest --testEnvironment=jest-environment-node --bail=1",
"test-sqlite": "export NODE_ENV=test; export DB=sqlite; jest --testEnvironment=jest-environment-node --forceExit --runInBand --bail=1",
"test-mariadb": "export NODE_ENV=test; export DB=mariadb; jest --testEnvironment=jest-environment-node --runInBand --bail=1",
"test-postgresql": "export NODE_ENV=test; export DB=postgresql; jest --testEnvironment=jest-environment-node --runInBand --bail=1",
"start": "nuxt start --modern",
"doc": "cd docs && bundle exec jekyll b",
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
@ -20,6 +20,7 @@
"files": [
"server/",
"assets/",
"modules/",
"nuxt.config.js",
"static/",
"views/",
@ -42,7 +43,7 @@
"accept-language": "^3.0.18",
"axios": "^0.27.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.0",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"cookie-universal-nuxt": "^2.2.2",
@ -55,20 +56,21 @@
"http-signature": "^1.3.6",
"https-proxy-agent": "^5.0.1",
"ical.js": "^1.5.0",
"ics": "^3.0.1",
"ics": "^3.1.0",
"jsdom": "^21.1.0",
"leaflet": "^1.9.2",
"linkify-html": "^4.0.2",
"linkifyjs": "4.1.0",
"linkify-html": "^4.1.1",
"linkifyjs": "4.1.1",
"lodash": "^4.17.21",
"luxon": "^3.3.0",
"mariadb": "^2.5.6",
"memory-cache": "^0.2.0",
"microformat-node": "^2.0.1",
"minify-css-string": "^1.0.0",
"mkdirp": "^2.1.3",
"mkdirp": "^2.1.6",
"multer": "^1.4.5-lts.1",
"mysql2": "^2.3.3",
"nuxt-edge": "2.17.0-27941778.c493723",
"nuxt-edge": "2.17.0-27999381.ff351a6",
"oauth2orize": "^1.11.1",
"passport": "^0.6.0",
"passport-anonymous": "^1.0.1",
@ -77,12 +79,12 @@
"passport-http-bearer": "^1.0.1",
"passport-oauth2-client-password": "^0.1.2",
"passport-oauth2-client-public": "^0.0.1",
"pg": "^8.9.0",
"sequelize": "^6.28.0",
"pg": "^8.10.0",
"sequelize": "^6.30.0",
"sequelize-slugify": "^1.6.2",
"sharp": "^0.27.2",
"sqlite3": "^5.1.4",
"telegraf": "^4.9.1",
"telegraf": "^4.12.2",
"tiptap": "^1.32.0",
"tiptap-extensions": "^1.35.0",
"umzug": "^2.3.0",
@ -91,26 +93,21 @@
"vuetify": "2.6.14",
"winston": "^3.8.2",
"winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.7.0"
"yargs": "^17.7.1"
},
"devDependencies": {
"@nuxtjs/vuetify": "^1.12.3",
"jest": "^29.4.2",
"jest-environment-node": "^29.4.2",
"prettier": "^2.8.1",
"jest": "^29.5.0",
"jest-environment-node": "^29.5.0",
"prettier": "^2.8.7",
"pug": "^3.0.2",
"pug-plain-loader": "^1.1.0",
"sass": "^1.56.2",
"sass": "^1.60.0",
"sequelize-cli": "^6.3.0",
"supertest": "^6.3.3",
"webpack": "4",
"webpack-cli": "^4.10.0"
},
"resolutions": {
"nth-check": "^2.0.1",
"glob-parent": "^5.1.2",
"moment": "^2.29.2"
},
"jest": {
"testEnvironment": "jsdom"
},

View file

@ -1,7 +1,7 @@
<template lang="pug">
v-container.container.pa-0.pa-md-3
v-card
v-alert(v-if='url!==settings.baseurl' outlined type='warning' color='red' show-icon :icon='mdiAlert')
v-alert(v-if='url!==settings.baseurl' outlined type='warning' show-icon :icon='mdiAlert')
span(v-html="$t('admin.wrong_domain_warning', { url, baseurl: settings.baseurl })")
v-tabs(v-model='selectedTab' show-arrows :next-icon='mdiChevronRight' :prev-icon='mdiChevronLeft')

View file

@ -34,6 +34,7 @@ v-container.container.pa-0.pa-md-3
//- When
DateInput(ref='when' v-model='date' :event='event')
//- Description
v-col.px-0(cols='12')
Editor.px-3.ma-0(
@ -70,7 +71,6 @@ v-container.container.pa-0.pa-md-3
<script>
import { mapState } from 'vuex'
import debounce from 'lodash/debounce'
import dayjs from 'dayjs'
import { mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle } from '@mdi/js'
@ -103,7 +103,7 @@ export default {
return true
},
async asyncData({ params, $axios, error, $auth, store }) {
async asyncData({ params, $axios, error, $auth, $time }) {
if (params.edit) {
const data = { event: { place: {}, media: [] } }
@ -123,15 +123,15 @@ export default {
data.event.place.name = event.place.name
data.event.place.address = event.place.address || ''
const from = dayjs.unix(event.start_datetime).tz()
const due = event.end_datetime && dayjs.unix(event.end_datetime).tz()
const from = $time.fromUnix(event.start_datetime)
const due = event.end_datetime && $time.fromUnix(event.end_datetime)
data.date = {
recurrent: event.recurrent,
from: from.toDate(),
due: due && due.toDate(),
from: from.toJSDate(),
due: due && due.toJSDate(),
multidate: event.multidate,
fromHour: from.format('HH:mm'),
dueHour: due && due.format('HH:mm')
fromHour: from.toFormat('HH:mm'),
dueHour: due && (due.toFormat('HH:mm') === '23:59' ? null : due.toFormat('HH:mm'))
}
data.event.title = event.title
@ -139,13 +139,15 @@ export default {
data.event.id = event.id
data.event.tags = event.tags
data.event.media = event.media || []
data.event.parentId = event.parentId
data.event.recurrent = event.recurrent
return data
}
return {}
},
data() {
const month = dayjs.tz().month() + 1
const year = dayjs.tz().year()
data({ $time }) {
const month = $time.currentMonth()
const year = $time.currentYear()
return {
mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle,
valid: false,
@ -189,15 +191,15 @@ export default {
this.event = Object.assign(this.event, event)
this.$refs.where.selectPlace({ name: event.place.name || event.place, address: event.place.address })
const from = dayjs.unix(this.event.start_datetime)
const due = this.event.end_datetime && dayjs.unix(this.event.end_datetime)
const from = this.$time.fromUnix(this.event.start_datetime)
const due = this.event.end_datetime && this.$time.fromUnix(this.event.end_datetime)
this.date = {
recurrent: this.event.recurrent || null,
from: from.toDate(),
due: due && due.toDate(),
from: from.toJSDate(),
due: due && due.toJSDate(),
multidate: event.multidate,
fromHour: from.format('HH:mm'),
dueHour: due && due.format('HH:mm')
fromHour: from.toFormat('HH:mm'),
dueHour: due && due.toFormat('HH:mm')
}
this.openImportDialog = false
},
@ -244,13 +246,11 @@ export default {
formData.append('description', this.event.description)
formData.append('multidate', !!this.date.multidate)
let [hour, minute] = this.date.fromHour.split(':')
formData.append('start_datetime', dayjs(this.date.from).tz().hour(Number(hour)).minute(Number(minute)).second(0).unix())
if (this.date.dueHour) {
[hour, minute] = this.date.dueHour.split(':')
formData.append('end_datetime', dayjs(this.date.due).tz().hour(Number(hour)).minute(Number(minute)).second(0).unix())
} else if (!!this.date.multidate) {
formData.append('end_datetime', dayjs(this.date.due).tz().hour(24).minute(0).second(0).unix())
formData.append('start_datetime', this.$time.fromDateInput(this.date.from, this.date.fromHour))
if (!!this.date.multidate) {
formData.append('end_datetime', this.$time.fromDateInput(this.date.due, this.date.dueHour || '23:59'))
} else if (this.date.dueHour) {
formData.append('end_datetime', this.$time.fromDateInput(this.date.from, this.date.dueHour))
}
if (this.edit) {
@ -258,12 +258,12 @@ export default {
}
if (this.event.tags) { this.event.tags.forEach(tag => formData.append('tags[]', tag.tag || tag)) }
try {
if (this.edit) {
await this.$axios.$put('/event', formData)
const ret = this.edit ? await this.$axios.$put('/event', formData) : await this.$axios.$post('/event', formData)
if (!this.date.recurrent) {
this.$router.push(`/event/${ret.slug}`)
} else {
await this.$axios.$post('/event', formData)
}
this.$router.push('/')
}
this.$nextTick(() => {
this.$root.$message(this.$auth.loggedIn ? (this.edit ? 'event.saved' : 'event.added') : 'event.added_anon', { color: 'success' })
})

View file

@ -5,7 +5,9 @@
<!-- Events -->
<div class='mb-2 mt-1 pl-1 pl-sm-2' id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id'></Event>
<v-lazy class='event' :value='idx<9' v-for='(event, idx) in events' :key='event.id' :min-height='hide_thumbs ? 105 : undefined' :options="{ threshold: .5, rootMargin: '500px' }">
<Event :event='event' :lazy='idx>9' />
</v-lazy>
</div>
</v-container>
</template>

View file

@ -2,10 +2,10 @@
nuxt-link.embed_event(:to='`/event/${event.slug || event.id}`' target='_blank' :class='{ withImg: event.media }')
//- image
img.float-left(:src='event | mediaURL("thumb")')
img.float-left(:src='$helper.mediaURL(event, "thumb")')
.event-info
//- title
.date {{event|when}}<br/>
.date {{$time.when(event)}}<br/>
h4 {{event.title}}
//- date / place

View file

@ -1,11 +1,9 @@
<template lang="pug">
v-container#event.pa-0.pa-sm-2
v-container#event.pa-2.pa-sm-2(itemscope itemtype="https://schema.org/Event" v-touch="{ left: goNext, right: goPrev }")
//- EVENT PAGE
//- gancio supports microformats (http://microformats.org/wiki/h-event)
//- and microdata https://schema.org/Event
v-card.h-event(itemscope itemtype="https://schema.org/Event" v-touch="{ left: goNext, right: goPrev }")
v-card-text
v-row
v-col.col-12.col-md-8
MyPicture(v-if='hasMedia' :event='event')
@ -18,21 +16,21 @@ v-container#event.pa-0.pa-sm-2
.title.text-h5
strong.p-name.text--primary(itemprop="name") {{event.title}}
v-divider
v-card-text
time.dt-start.text-button(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')")
v-container.eventDetails
time.dt-start(:datetime='$time.unixFormat(event.start_datetime, "yyyy-MM-dd HH:mm")' itemprop="startDate" :content="$time.unixFormat(event.start_datetime, \"yyyy-MM-dd'T'HH:mm\")")
v-icon(v-text='mdiCalendar' small)
strong.ml-2 {{event|when}}
.d-none.dt-end(itemprop="endDate" :content="event.end_datetime|unixFormat('YYYY-MM-DDTHH:mm')") {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}}
div.text-caption.mb-3 {{event.start_datetime|from}}
small(v-if='event.parentId') ({{event|recurrentDetail}})
strong.ml-2.text-uppercase {{$time.when(event)}}
.d-none.dt-end(v-if='event.end_datetime' itemprop="endDate" :content="$time.unixFormat(event.end_datetime,\"yyyy-MM-dd'T'HH:mm\")") {{$time.unixFormat(event.end_datetime,"yyyy-MM-dd'T'HH:mm")}}
div.font-weight-light.mb-3 {{$time.from(event.start_datetime)}}
small(v-if='event.parentId') ({{$time.recurrentDetail(event)}})
.text-h6.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place")
.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place")
v-icon(v-text='mdiMapMarker' small)
nuxt-link.vcard.ml-2.p-name.text-decoration-none.text-button(itemprop="name" :to='`/place/${encodeURIComponent(event.place.name)}`') {{event.place && event.place.name}}
.text-caption.p-street-address(itemprop='address') {{event.place && event.place.address}}
nuxt-link.vcard.ml-2.p-name.text-decoration-none.text-uppercase(itemprop="name" :to='`/place/${encodeURIComponent(event.place.name)}`') {{event.place && event.place.name}}
.font-weight-light.p-street-address(itemprop='address') {{event.place && event.place.address}}
//- tags, hashtags
v-card-text.pt-0(v-if='event.tags && event.tags.length')
v-container.pt-0(v-if='event.tags && event.tags.length')
v-chip.p-category.ml-1.mt-1(v-for='tag in event.tags' small label color='primary'
outlined :key='tag' :to='`/tag/${encodeURIComponent(tag)}`') {{tag}}
@ -54,7 +52,7 @@ v-container#event.pa-0.pa-sm-2
v-divider
//- info & actions
v-list(dense nav)
v-list(dense nav color='transparent')
//- v-list-group(:append-icon='mdiChevronUp' :value='true')
//- template(v-slot:activator)
//- v-list-item.text-overline {{$t('common.actions')}}
@ -88,7 +86,7 @@ v-container#event.pa-0.pa-sm-2
v-list-item-title(v-text="$t('common.add_to_calendar')")
//- download flyer
v-list-item(v-if='hasMedia' :href='event | mediaURL("download")')
v-list-item(v-if='hasMedia' :href='$helper.mediaURL(event, "download")')
v-list-item-icon
v-icon(v-text='mdiFileDownloadOutline')
v-list-item-content
@ -104,9 +102,23 @@ v-container#event.pa-0.pa-sm-2
//- resources from fediverse
#resources.mt-1(v-if='settings.enable_federation')
//- div.float-right(v-if='settings.hide_boosts')
//- small.mr-3 🔖 {{event.likes.length}}
//- small {{event.boost.length}}<br/>
div.mb-3(v-if='!settings.hide_boosts && (event.boost?.length || event.likes?.length) ')
client-only
v-menu(open-on-hover top offset-y)
template( v-slot:activator="{ on, attrs }")
span.mr-3(v-bind='attrs' v-on='on') <v-icon color='primary' v-text='mdiBookmark' /> {{event.likes.length}}
v-list
v-list-item(v-for='(like, idx) in event.likes' :key='idx')
v-list-item-title(v-text='like')
v-menu(open-on-hover top offset-y)
template( v-slot:activator="{ on, attrs }")
span(v-bind='attrs' v-on='on') <v-icon v-text='mdiShareAll' /> {{event.boost.length}}
v-list
v-list-item(v-for='(boost, idx) in event.boost' :key='idx')
v-list-item-title(v-text='boost')
template(slot='placeholder')
span.mr-3 <v-icon color='primary' v-text='mdiBookmark' /> {{event.likes.length}}
span <v-icon v-text='mdiShareAll' /> {{event.boost.length}}
v-dialog(v-model='showResources' max-width="900" width="900" :fullscreen='$vuetify.breakpoint.xsOnly'
destroy-on-close)
@ -145,7 +157,7 @@ v-container#event.pa-0.pa-sm-2
v-icon.mr-1(v-show='resource.hidden' v-text='mdiEyeOff')
a(:href='resource.data.url || resource.data.context')
small {{resource.data.published|dateFormat('ddd, D MMMM HH:mm')}}
small {{$time.format(resource.data.published,'ff')}}
v-card-text
@ -178,7 +190,7 @@ v-container#event.pa-0.pa-sm-2
<script>
import { mapState } from 'vuex'
import get from 'lodash/get'
import moment from 'dayjs'
import { DateTime } from 'luxon'
import clipboard from '../../assets/clipboard'
import MyPicture from '~/components/MyPicture'
import EventAdmin from '@/components/eventAdmin'
@ -187,8 +199,8 @@ import EmbedEvent from '@/components/embedEvent'
const { htmlToText } = require('html-to-text')
import { mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiClose, mdiMap,
mdiEye, mdiEyeOff, mdiDelete, mdiRepeat, mdiLock, mdiFileDownloadOutline,
mdiCalendarExport, mdiCalendar, mdiContentCopy, mdiMapMarker, mdiChevronUp, mdiMonitorAccount } from '@mdi/js'
mdiEye, mdiEyeOff, mdiDelete, mdiRepeat, mdiLock, mdiFileDownloadOutline, mdiShareAll,
mdiCalendarExport, mdiCalendar, mdiContentCopy, mdiMapMarker, mdiChevronUp, mdiMonitorAccount, mdiBookmark } from '@mdi/js'
export default {
name: 'Event',
@ -210,7 +222,7 @@ export default {
data ({$store}) {
return {
mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiCalendarExport, mdiCalendar, mdiFileDownloadOutline,
mdiMapMarker, mdiContentCopy, mdiClose, mdiDelete, mdiEye, mdiEyeOff, mdiRepeat, mdiLock, mdiMap, mdiChevronUp, mdiMonitorAccount,
mdiMapMarker, mdiContentCopy, mdiClose, mdiDelete, mdiEye, mdiEyeOff, mdiRepeat, mdiLock, mdiMap, mdiChevronUp, mdiMonitorAccount, mdiBookmark, mdiShareAll,
currentAttachment: 0,
event: {},
diocane: '',
@ -261,23 +273,23 @@ export default {
{ property: 'og:type', content: 'event' },
{
property: 'og:image',
content: this.$options.filters.mediaURL(this.event)
content: this.$helper.mediaURL(this.event)
},
{ property: 'og:site_name', content: this.settings.title },
{
property: 'og:updated_time',
content: moment.unix(this.event.start_datetime).format()
content: DateTime.fromSeconds(this.event.start_datetime, { zone: this.settings.instance_timezone }).toISO()
},
{
property: 'article:published_time',
content: moment.unix(this.event.start_datetime).format()
content: DateTime.fromSeconds(this.event.start_datetime, { zone: this.settings.instance_timezone }).toISO()
},
{ property: 'article:section', content: 'event' },
{ property: 'twitter:card', content: 'summary' },
{ property: 'twitter:title', content: this.event.title },
{
property: 'twitter:image',
content: this.$options.filters.mediaURL(this.event)
content: this.$helper.mediaURL(this.event)
},
{
property: 'twitter:description',
@ -285,7 +297,7 @@ export default {
}
],
link: [
{ rel: 'image_src', href: this.$options.filters.mediaURL(this.event) },
{ rel: 'image_src', href: this.$helper.mediaURL(this.event) },
{
rel: 'alternate',
type: 'application/rss+xml',
@ -320,7 +332,7 @@ export default {
mounted () {
window.addEventListener('keydown', this.keyDown)
},
destroyed () {
beforeDestroy () {
window.removeEventListener('keydown', this.keyDown)
},
methods: {

View file

@ -81,7 +81,6 @@ v-container.pa-0.pa-md-3
</template>
<script>
import dayjs from 'dayjs'
import { mapState } from 'vuex'
import FollowMe from '../components/FollowMe'
import Search from '@/components/Search'
@ -95,9 +94,9 @@ export default {
Search
},
mixins: [clipboard],
async asyncData ({ $axios, params, store, $api }) {
async asyncData ({ $axios, params, store, $api, $time }) {
const events = await $api.getEvents({
start: dayjs().unix(),
start: $time.currentTimestamp(),
show_recurrent: false
})
return { events }

View file

@ -1,5 +1,5 @@
<template lang="pug">
v-container.px-2.px-sm-6.pt-0
v-container.px-2.px-sm-6.pt-0#home
//- View
#themeview.mt-sm-4.mt-2
@ -10,14 +10,17 @@ v-container.px-2.px-sm-6.pt-0
Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement')
//- Events
#events.mt-sm-4.mt-2
Event(:event='event' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id')
#events.mt-sm-4.mt-2(v-if='!$fetchState.pending')
v-lazy.event.v-card(:value='idx<9' v-for='(event, idx) in visibleEvents' :key='event.id' :min-height='hide_thumbs ? 105 : undefined' :options="{ threshold: .5, rootMargin: '500px' }" :class="{ 'theme--dark': is_dark }")
Event(:event='event' :lazy='idx>9')
.text-center(v-else)
v-progress-circular.justify-center.align-center(color='primary' indeterminate model-value='20')
</template>
<script>
import { mapState, mapActions } from 'vuex'
import debounce from 'lodash/debounce'
import dayjs from 'dayjs'
import { mapState, mapActions, mapGetters } from 'vuex'
import { DateTime } from 'luxon'
import Event from '@/components/Event'
import Announcement from '@/components/Announcement'
import ThemeView from '@/components/ThemeView'
@ -28,22 +31,24 @@ export default {
components: { Event, Announcement, ThemeView },
middleware: 'setup',
fetch () {
if (this.filter.query) {
return this.getEvents({
query: this.filter.query,
older: true
})
} else {
return this.getEvents({
start: this.start,
end: this.end
end: this.end,
})
},
activated() {
if (this.$fetchState.timestamp <= Date.now() - 60000) {
this.$fetch()
}
},
data ({ $store }) {
data ({ $time }) {
return {
mdiMagnify, mdiCloseCircle,
isCurrentMonth: true,
now: dayjs().unix(),
start: dayjs().startOf('month').unix(),
now: $time.nowUnix(),
start: $time.startMonth(),
end: null,
tmpEvents: [],
selectedDay: null,
@ -70,16 +75,14 @@ export default {
},
computed: {
...mapState(['settings', 'announcements', 'events', 'filter']),
...mapGetters(['hide_thumbs', 'is_dark']),
visibleEvents () {
if (this.filter.query && this.filter.query.length > 2) {
return this.tmpEvents
}
const now = dayjs().unix()
const now = this.$time.nowUnix()
if (this.selectedDay) {
const min = dayjs.tz(this.selectedDay).startOf('day').unix()
const max = dayjs.tz(this.selectedDay).endOf('day').unix()
return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.filter.show_recurrent || !e.parentId))
} else if (this.isCurrentMonth) {
const min = this.selectedDay.startOf('day').toUnixInteger()
const max = this.selectedDay.endOf('day').toUnixInteger()
return this.events.filter(e => (e.start_datetime < max && (e.end_datetime || e.start_datetime) > min) && (this.filter.show_recurrent || !e.parentId))
} else if (this.isCurrentMonth && !this.filter.query) {
return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.filter.show_recurrent || !e.parentId)))
} else {
return this.events.filter(e => this.filter.show_recurrent || !e.parentId)
@ -89,16 +92,11 @@ export default {
created () {
this.$root.$on('dayclick', this.dayChange)
this.$root.$on('monthchange', this.monthChange)
if (process.client) {
this.storeUnsubscribe = this.$store.subscribeAction( { after: (action, state) => {
if (action.type === 'setFilter') {
if (this.filter.query && this.filter.query.length > 2) {
this.search()
} else {
this.tmpEvents = []
this.$fetch()
}
}
if (action.type === 'setFilter') { this.$fetch() }
}})
}
},
destroyed () {
this.$root.$off('dayclick')
@ -109,37 +107,36 @@ export default {
},
methods: {
...mapActions(['getEvents']),
search: debounce(async function() {
this.tmpEvents = await this.$api.getEvents({
start: 0,
show_recurrent: this.filter.show_recurrent,
show_multidate: this.filter.show_multidate,
query: this.filter.query
})
}, 200),
async monthChange ({ year, month }) {
if (this.filter.query) return
this.$nuxt.$loading.start()
let isCurrentMonth
// unselect current selected day
this.selectedDay = null
const now = DateTime.local({zone: this.settings.instance_timezone})
// check if current month is selected
if (month - 1 === dayjs.tz().month() && year === dayjs.tz().year()) {
if (month === now.month && year === now.year) {
isCurrentMonth = true
this.start = dayjs().startOf('month').unix()
this.start = now.startOf('month').toUnixInteger()
this.end = null
} else {
isCurrentMonth = false
this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix()
this.start = DateTime.local(year, month, { zone: this.settings.instance_timezone }).toUnixInteger()
this.end = DateTime.local(year, month, { zone: this.settings.instance_timezone }).plus({ month: !this.$vuetify.breakpoint.smAndDown ? 1 : 0 }).endOf('month').toUnixInteger() // .endOf('week').unix()
}
this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix()
await this.$fetch()
this.$nuxt.$loading.finish()
this.$nextTick( () => this.isCurrentMonth = isCurrentMonth)
},
dayChange (day) {
this.selectedDay = day ? dayjs.tz(day).format('YYYY-MM-DD') : null
if (!day) {
this.selectedDay = null
return
}
const date = DateTime.fromJSDate(day)
this.selectedDay = day ? DateTime.local({ zone: this.settings.instance_timezone }).set({ year: date.year, month: date.month, day: date.day}) : null
}
}
}

View file

@ -1,19 +1,21 @@
<template>
<v-container class='px-0' fluid>
<v-container id='home' class='px-2 px-sm-6 pt-0'>
<h1 class='d-block text-h4 font-weight-black text-center text-uppercase mt-10 mx-auto w-100 text-underline'>
<u>{{ place.name }}</u>
</h1>
<span class="d-block text-subtitle text-center w-100 mb-14">{{ place.address }}</span>
<!-- Events -->
<div class="mb-2 mt-1 pl-1 pl-sm-2" id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx > 2' :key='event.id'></Event>
<div id="events">
<v-lazy class='event v-card' :value='idx<9' v-for='(event, idx) in events' :key='event.id' :min-height='hide_thumbs ? 105 : undefined' :options="{ threshold: .5, rootMargin: '500px' }" :class="{ 'theme--dark': is_dark }">
<Event :event='event' :lazy='idx > 9' />
</v-lazy>
</div>
</v-container>
</template>
<script>
import { mapState } from 'vuex'
import { mapState, mapGetters } from 'vuex'
import Event from '@/components/Event'
export default {
@ -29,7 +31,10 @@ export default {
]
}
},
computed: mapState(['settings']),
computed: {
...mapState(['settings']),
...mapGetters(['hide_thumbs', 'is_dark']),
},
asyncData({ $axios, params, error }) {
try {
const place = params.place

View file

@ -11,7 +11,6 @@ v-container#home(fluid)
<script>
import { mapState } from 'vuex'
import dayjs from 'dayjs'
import Event from '@/components/Event'
import Announcement from '@/components/Announcement'
import Calendar from '@/components/Calendar'
@ -20,11 +19,11 @@ import { mdiMagnify } from '@mdi/js'
export default {
name: 'Index',
components: { Event, Announcement, Calendar },
data () {
data ({ $time }) {
return {
mdiMagnify,
events: [],
start: dayjs().startOf('month').unix(),
start: $time.startMonth(),
end: null,
}
},

View file

@ -1,17 +1,19 @@
<template>
<v-container class='px-0' fluid>
<v-container id='home' class='px-2 px-sm-6 pt-0' fluid>
<h1 class='d-block text-h3 font-weight-black text-center text-uppercase mt-10 mb-16 mx-auto w-100 text-underline'><u>{{tag}}</u></h1>
<!-- Events -->
<div class="mb-2 mt-1 pl-1 pl-sm-2" id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id'></Event>
<v-lazy class='event v-card' :value='idx<9' v-for='(event, idx) in events' :key='event.id' :min-height='hide_thumbs ? 105 : undefined' :options="{ threshold: .5, rootMargin: '500px' }" :class="{ 'theme--dark': is_dark }">
<Event :event='event' :lazy='idx>9' />
</v-lazy>
</div>
</v-container>
</template>
<script>
import { mapState } from 'vuex'
import { mapState, mapGetters } from 'vuex'
import Event from '@/components/Event'
export default {
@ -28,7 +30,10 @@ export default {
]
}
},
computed: mapState(['settings']),
computed: {
...mapState(['settings']),
...mapGetters(['hide_thumbs', 'is_dark']),
},
async asyncData ({ $axios, params, error }) {
try {
const tag = params.tag

View file

@ -1,92 +0,0 @@
import Vue from 'vue'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import 'dayjs/locale/it'
import 'dayjs/locale/en'
import 'dayjs/locale/es'
import 'dayjs/locale/ca'
import 'dayjs/locale/pl'
import 'dayjs/locale/eu'
import 'dayjs/locale/nb'
import 'dayjs/locale/fr'
import 'dayjs/locale/de'
import 'dayjs/locale/gl'
import 'dayjs/locale/sk'
import 'dayjs/locale/ru'
import 'dayjs/locale/pt'
import 'dayjs/locale/zh'
dayjs.extend(relativeTime)
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(localizedFormat)
export default ({ app, store }) => {
// set timezone to instance_timezone!!
// to show local time relative to event's place
// not where in the world I'm looking at the page from
app.i18n.defaultLocale = store.state.settings.instance_locale
const instance_timezone = store.state.settings.instance_timezone
dayjs.tz.setDefault(instance_timezone)
dayjs.locale(app.i18n.locale || store.state.settings.instance_locale)
// replace links with anchors
// TODO: remove fb tracking id?
Vue.filter('linkify', value => value.replace(/(https?:\/\/([^\s]+))/g, '<a href="$1">$2</a>'))
Vue.filter('url2host', url => url.match(/^https?:\/\/(.[^/:]+)/i)[1])
Vue.filter('datetime', value => dayjs.tz(value).locale(app.i18n.locale || store.state.settings.instance_locale).format('ddd, D MMMM HH:mm'))
Vue.filter('dateFormat', (value, format) => dayjs.tz(value).format(format))
Vue.filter('unixFormat', (timestamp, format) => dayjs.unix(timestamp).tz().format(format))
// shown in mobile homepage
Vue.filter('day', value => dayjs.unix(value).tz().locale(app.i18n.locale || store.state.settings.instance_locale).format('dddd, D MMM'))
Vue.filter('mediaURL', (event, type, format = '.jpg') => {
const mediaPath = type === 'download' ? '/download/' : '/media/'
if (event.media && event.media.length) {
if (type === 'alt') {
return event.media[0].name
} else {
return store.state.settings.baseurl + mediaPath + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url.replace(/.jpg$/, format)
}
} else if (type !== 'alt') {
return store.state.settings.baseurl + mediaPath + (type === 'thumb' ? 'thumb/' : '') + 'logo.svg'
}
return ''
})
Vue.filter('from', timestamp => dayjs.unix(timestamp).tz().locale(app.i18n.locale || store.state.settings.instance_locale).fromNow())
Vue.filter('recurrentDetail', event => {
const parent = event.parent
if (!parent.recurrent || !parent.recurrent.frequency) return 'error!'
const { frequency, type } = parent.recurrent
let recurrent
if (frequency === '1w' || frequency === '2w') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: dayjs.unix(parent.start_datetime).tz().format('dddd') })
} else if (frequency === '1m' || frequency === '2m') {
const d = type === 'ordinal' ? dayjs.unix(parent.start_datetime).date() : dayjs.unix(parent.start_datetime).tz().format('dddd')
if (type === 'ordinal') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: d })
} else {
recurrent = app.i18n.t(`event.recurrent_${frequency}_ordinal`,
{ n: app.i18n.t('ordinal.' + type), days: d })
}
}
return recurrent
})
Vue.filter('when', (event) => {
const start = dayjs.unix(event.start_datetime).tz().locale(app.i18n.locale || store.state.settings.instance_locale)
const end = event.end_datetime && dayjs.unix(event.end_datetime).tz().locale(app.i18n.locale || store.state.settings.instance_locale)
let time = start.format('dddd D MMMM HH:mm')
if (end) {
time += event.multidate ? `${end.format('dddd D MMMM HH:mm')}` : `-${end.format('HH:mm')}`
}
return time
})
}

21
plugins/helpers.js Normal file
View file

@ -0,0 +1,21 @@
export default ({ store }, inject) => {
const helper = {
mediaURL (event, type, format = '.jpg') {
const mediaPath = type === 'download' ? '/download/' : '/media/'
if (event.media && event.media.length) {
if (type === 'alt') {
return event.media[0].name
} else {
return store.state.settings.baseurl + mediaPath + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url.replace(/.jpg$/, format)
}
} else if (type !== 'alt') {
return store.state.settings.baseurl + mediaPath + (type === 'thumb' ? 'thumb/' : '') + 'logo.svg'
}
return ''
}
}
inject('helper', helper)
}

170
plugins/time.js Normal file
View file

@ -0,0 +1,170 @@
import { DateTime, Settings } from 'luxon'
export default ({ app, store }, inject) => {
const zone = Settings.defaultZoneName = store.state.settings.instance_timezone
Settings.defaultLocale = app.i18n.locale || store.state.settings.instance_locale
const time = {
format (date, format) {
return DateTime.fromISO(date, {
zone: store.state.settings.instance_timezone,
locale: app.i18n.locale || store.state.settings.instance_locale
}).toFormat(format || '')
},
unixFormat (timestamp, format='EEEE d MMMM HH:mm') {
return DateTime.fromSeconds(timestamp, {
zone: store.state.settings.instance_timezone,
locale: app.i18n.locale || store.state.settings.instance_locale
}).toFormat(format)
},
fromUnix (timestamp) {
return DateTime.fromSeconds(timestamp, { zone })
},
fromDateInput (date, time) {
const [hour, minute] = time.split(':')
return DateTime.fromJSDate(date, { zone })
.set({ hour: Number(hour), minute: Number(minute), second: 0 })
.toUnixInteger()
},
currentMonth () {
return DateTime.local({ zone }).month
},
currentYear () {
return DateTime.local({ zone }).year
},
when (event) {
const currentYear = app.$time.currentYear()
const opt = {
zone: store.state.settings.instance_timezone,
locale: app.i18n.locale || store.state.settings.instance_locale
}
const start = DateTime.fromSeconds(event.start_datetime, opt)
let time = start.toFormat('EEEE d MMMM HH:mm')
const end = event.end_datetime && DateTime.fromSeconds(event.end_datetime, opt)
if (end) {
time += event.multidate ? `${end.toFormat('EEEE d MMMM')}` : `-${end.toFormat('HH:mm')}`
}
if (currentYear !== start.year) {
time += ` (${start.year})`
}
return time
},
nowUnix () {
const opt = {
zone,
locale: app.i18n.locale || store.state.settings.instance_locale
}
return DateTime.local(opt).toUnixInteger()
},
startMonth () { return DateTime.local({ zone }).startOf('month').toUnixInteger() },
startOfDay (date) { return DateTime.fromJSDate(date, { zone }).startOf('day').toUnixInteger()},
endOfDay (date) { return DateTime.fromJSDate(date, { zone }).endOf('day').toUnixInteger()},
recurrentDetail (event) {
const parent = event.parent
if (!parent.recurrent || !parent.recurrent.frequency) return 'error!'
const { frequency, type } = parent.recurrent
let recurrent
if (frequency === '1w' || frequency === '2w') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: DateTime.fromSeconds(parent.start_datetime).toFormat('EEEE') })
} else if (frequency === '1m' || frequency === '2m') {
const d = type === 'ordinal' ? DateTime.fromSeconds(parent.start_datetime).day : DateTime.fromSeconds(parent.start_datetime).toFormat('EEEE')
if (type === 'ordinal') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: d })
} else {
recurrent = app.i18n.t(`event.recurrent_${frequency}_ordinal`,
{ n: app.i18n.t('ordinal.' + type), days: d })
}
}
return recurrent
},
currentTimestamp () {
return DateTime.local().toUnixInteger()
},
from (timestamp) {
const opt = {
zone: store.state.settings.instance_timezone,
locale: app.i18n.locale || store.state.settings.instance_locale
}
return DateTime.fromSeconds(timestamp, opt).toRelative()
},
/**
* @description build v-calendar attributes
* @link https://vcalendar.io/attributes.html
*/
attributesFromEvents(events) {
const attributes = []
const opt = {
zone: store.state.settings.instance_timezone,
locale: app.i18n.locale || store.state.settings.instance_locale
}
const now = DateTime.local(opt).toUnixInteger()
for (const e of events) {
const tmp = DateTime.fromSeconds(e.start_datetime, opt)
const start = DateTime.local().set({ year: tmp.year, month: tmp.month, day: tmp.day })
// merge events with same date
const key = `${start.month}${start.day}`
const c = (e.end_datetime || e.start_datetime) < now ? 'vc-past' : ''
if (e.multidate === true && e.end_datetime) {
attributes.push({
dates: { start: start.toJSDate(), end: DateTime.fromSeconds(e.end_datetime).toJSDate() },
highlight: {
start: { fillMode: 'outline' },
base: { fillMode: 'light' },
end: { fillMode: 'outline' },
}
})
continue
}
const i = attributes.find(a => a.day === key)
if (!i) {
attributes.push({
day: key,
key: e.id, n: 1,
dates: start.toJSDate(),
dot: { color: 'teal', class: c }
})
continue
}
i.n++
if (i.n >= 20) {
i.dot = { color: 'purple', class: c }
} else if (i.n >= 10) {
i.dot = { color: 'red', class: c }
} else if (i.n >= 5) {
i.dot = { color: 'orange', class: c }
} else if (i.n >= 3) {
i.dot = { color: 'yellow', class: c }
} else {
i.dot = { color: 'teal', class: c }
}
}
// add a bar to highlight today
attributes.push({ key: 'today', dates: new Date(), highlight: { color: 'green', fillMode: 'outline' }})
return attributes
}
}
inject('time', time)
}

View file

@ -1,9 +1,3 @@
import Vue from 'vue'
import VCalendar from 'v-calendar'
export default () => {
Vue.use(VCalendar, {
componentPrefix: 'vc',
// why is that ?!
// firstDayOfWeek: 2
})
}
Vue.use(VCalendar, { componentPrefix: 'vc' })

View file

@ -1,7 +1,7 @@
const { Collection, Filter, Event, Tag, Place } = require('../models/models')
const log = require('../../log')
const dayjs = require('dayjs')
const { DateTime } = require('luxon')
const { col: Col } = require('../../helpers')
const { Op, Sequelize } = require('sequelize')
@ -21,7 +21,6 @@ const collectionController = {
// return events from collection
async getEvents (req, res) {
const format = req.params.format || 'json'
const name = req.params.name
const collection = await Collection.findOne({ where: { name } })
@ -33,7 +32,7 @@ const collectionController = {
if (!filters.length) {
return res.json([])
}
const start = dayjs().unix()
const start = DateTime.local().toUnixInteger()
const where = {
// do not include parent recurrent event
recurrent: null,

View file

@ -1,11 +1,11 @@
const crypto = require('crypto')
const path = require('path')
const config = require('../../config')
const fs = require('fs')
const fs = require('fs/promises')
const { Op } = require('sequelize')
const linkifyHtml = require('linkify-html')
const Sequelize = require('sequelize')
const dayjs = require('dayjs')
const { DateTime } = require('luxon')
const helpers = require('../../helpers')
const Col = helpers.col
const notifier = require('../../notifier')
@ -85,73 +85,6 @@ const eventController = {
return res.json(ret)
},
// async search(req, res) {
// const search = req.query.search.trim().toLocaleLowerCase()
// const show_recurrent = req.query.show_recurrent || false
// const end = req.query.end
// const replacements = []
// const where = {
// // do not include parent recurrent event
// recurrent: null,
// // confirmed event only
// is_visible: true,
// }
// if (!show_recurrent) {
// where.parentId = null
// }
// if (end) {
// where.start_datetime = { [Op.lte]: end }
// }
// if (search) {
// replacements.push(search)
// where[Op.or] =
// [
// { title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') },
// Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'),
// Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
// ]
// }
// const events = await Event.findAll({
// where,
// attributes: {
// exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
// },
// order: [['start_datetime', 'DESC']],
// include: [
// {
// model: Tag,
// // order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
// attributes: ['tag'],
// through: { attributes: [] }
// },
// { model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
// ],
// replacements,
// limit: 30,
// }).catch(e => {
// log.error('[EVENT]', e)
// return res.json([])
// })
// const ret = events.map(e => {
// e = e.get()
// e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
// return e
// })
// return res.json(ret)
// },
async _get(slug) {
// retrocompatibility, old events URL does not use slug, use id as fallback
const id = Number(slug) || -1
@ -319,7 +252,7 @@ const eventController = {
where: {
parentId: null,
is_visible: false,
start_datetime: { [Op.gt]: dayjs().unix() }
start_datetime: { [Op.gt]: DateTime.local().toUnixInteger() }
},
order: [['start_datetime', 'ASC']],
include: [{ model: Tag, required: false }, Place]
@ -496,8 +429,8 @@ const eventController = {
try {
const old_path = path.resolve(config.upload_path, event.media[0].url)
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url)
fs.unlinkSync(old_path)
fs.unlinkSync(old_thumb_path)
await fs.unlink(old_path)
await fs.unlink(old_thumb_path)
} catch (e) {
log.info(e.toString())
}
@ -575,8 +508,8 @@ const eventController = {
try {
const old_path = path.join(config.upload_path, event.media[0].url)
const old_thumb_path = path.join(config.upload_path, 'thumb', event.media[0].url)
fs.unlinkSync(old_thumb_path)
fs.unlinkSync(old_path)
await fs.unlink(old_thumb_path)
await fs.unlink(old_path)
} catch (e) {
log.info(e.toString())
}
@ -601,7 +534,7 @@ const eventController = {
* @returns
*/
async _select({
start = dayjs().unix(),
start = DateTime.local().toUnixInteger(),
end,
query,
tags,
@ -610,7 +543,8 @@ const eventController = {
show_multidate,
limit,
page,
older }) {
older,
reverse }) {
const where = {
// do not include _parent_ recurrent event
@ -680,7 +614,7 @@ const eventController = {
attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'description', 'resources', 'recurrent', 'placeId', 'image_path']
},
order: [['start_datetime', older ? 'DESC' : 'ASC']],
order: [['start_datetime', reverse ? 'DESC' : 'ASC']],
include: [
{
model: Tag,
@ -709,7 +643,7 @@ const eventController = {
*/
async select(req, res) {
const settings = res.locals.settings
const start = req.query.start || dayjs().unix()
const start = req.query.start || DateTime.local().toUnixInteger()
const end = req.query.end
const query = req.query.query
const tags = req.query.tags
@ -730,10 +664,12 @@ const eventController = {
},
/**
* Ensure we have the next instance of a recurrent event
* Ensure we have the next occurrence of a recurrent event
*/
async _createRecurrentOccurrence(e, startAt) {
async _createRecurrentOccurrence(e, startAt = DateTime.local(), firstOccurrence = true) {
log.debug(`Create recurrent event [${e.id}] ${e.title}"`)
// prepare the new event occurrence copying the parent's properties
const event = {
parentId: e.id,
title: e.title,
@ -744,55 +680,67 @@ const eventController = {
placeId: e.placeId
}
const recurrent = e.recurrent
const start_date = dayjs.unix(e.start_datetime)
let cursor = start_date > startAt ? start_date : startAt
const recurrentDetails = e.recurrent
const parentStartDatetime = DateTime.fromSeconds(e.start_datetime)
// cursor is when start to count
// sets it to
let cursor = parentStartDatetime > startAt ? parentStartDatetime : startAt
startAt = cursor
const duration = dayjs.unix(e.end_datetime).diff(start_date, 's')
const frequency = recurrent.frequency
const type = recurrent.type
cursor = cursor.hour(start_date.hour()).minute(start_date.minute()).second(0)
const duration = e.end_datetime ? e.end_datetime-e.start_datetime : 0
const frequency = recurrentDetails.frequency
const type = recurrentDetails.type
if (!frequency) {
log.warn(`Recurrent event ${e.id} - ${e.title} does not have a frequency specified`)
return
}
if (!frequency) { return }
cursor = cursor.set({ hour: parentStartDatetime.hour, minute: parentStartDatetime.minute, second: 0 })
// each week or 2
if (frequency[1] === 'w') {
cursor = cursor.day(start_date.day())
if (cursor.isBefore(startAt)) {
cursor = cursor.add(7, 'day')
}
if (frequency[0] === '2') {
cursor = cursor.add(7, 'day')
cursor = cursor.set({ weekday: parentStartDatetime.weekday }) //day(parentStartDatetime.day())
if (cursor < startAt) {
cursor = cursor.plus({ days: 7 * Number(frequency[0]) })
}
} else if (frequency === '1m') {
if (type === 'ordinal') {
cursor = cursor.date(start_date.date())
cursor = cursor.set({ day: parentStartDatetime.day })
if (cursor.isBefore(startAt)) {
cursor = cursor.add(1, 'month')
if (cursor< startAt) {
cursor = cursor.plus({ months: 1 })
}
} else { // weekday
// get weekday
// get recurrent freq details
cursor = helpers.getWeekdayN(cursor, type, start_date.day())
if (cursor.isBefore(startAt)) {
cursor = cursor.add(4, 'week')
cursor = helpers.getWeekdayN(cursor, type, start_date.day())
cursor = helpers.getWeekdayN(cursor, type, parentStartDatetime.weekday)
if (cursor< startAt) {
cursor = cursor.plus({ months: 1 })
cursor = helpers.getWeekdayN(cursor, type, parentStartDatetime.weekday)
}
}
}
log.debug(cursor)
event.start_datetime = cursor.unix()
event.end_datetime = event.start_datetime + duration
event.start_datetime = cursor.toUnixInteger()
event.end_datetime = e.end_datetime ? event.start_datetime + duration : null
try {
const newEvent = await Event.create(event)
if (e.tags) {
return newEvent.addTags(e.tags)
} else {
return newEvent
}
} catch (e) {
console.error(event)
log.error('[RECURRENT EVENT]', e)
}
},
/**
* Create instances of recurrent events
*/
async _createRecurrent(start_datetime = dayjs().unix()) {
async _createRecurrent(start_datetime = DateTime.local().toUnixInteger()) {
// select recurrent events and its childs
const events = await Event.findAll({
where: { is_visible: true, recurrent: { [Op.ne]: null } },
@ -805,9 +753,9 @@ const eventController = {
const creations = events.map(e => {
if (e.child.length) {
if (e.child.find(c => c.is_visible)) return
return eventController._createRecurrentOccurrence(e, dayjs.unix(e.child[0].start_datetime + 1))
return eventController._createRecurrentOccurrence(e, DateTime.fromSeconds(e.child[0].start_datetime + 1), false)
}
return eventController._createRecurrentOccurrence(e, dayjs())
return eventController._createRecurrentOccurrence(e, DateTime.local(), true)
})
return Promise.all(creations)

View file

@ -2,7 +2,7 @@ const { Event, Place, Tag } = require('../models/models')
const { htmlToText } = require('html-to-text')
const { Op, literal } = require('sequelize')
const moment = require('dayjs')
const { DateTime } = require('luxon')
const ics = require('ics')
const exportController = {
@ -13,8 +13,13 @@ const exportController = {
const places = req.query.places
const show_recurrent = !!req.query.show_recurrent
const opt = {
zone: res.locals.settings.instance_timezone,
locale: res.locals.settings.instance_locale
}
const where = {}
const yesterday = moment().subtract('1', 'day').unix()
const yesterday = DateTime.local(opt).minus({day: 1}).toUnixInteger()
if (tags && places) {
@ -69,8 +74,18 @@ const exportController = {
feed (_req, res, events, title = res.locals.settings.title, link = `${res.locals.settings.baseurl}/feed/rss`) {
const settings = res.locals.settings
const opt = {
zone: settings.instance_timezone,
locale: settings.instance_locale
}
function unixFormat (timestamp, format='EEEE d MMMM HH:mm') {
return DateTime.fromSeconds(timestamp, opt).toFormat(format)
}
res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', { events, settings, moment, title, link })
res.render('feed/rss.pug', { events, settings, unixFormat, title, link })
},
/**
@ -82,8 +97,8 @@ const exportController = {
const settings = res.locals.settings
const eventsMap = events.map(e => {
const tmpStart = moment.unix(e.start_datetime)
const start = tmpStart.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
const tmpStart = DateTime.fromSeconds(e.start_datetime)
const start = [ tmpStart.year, tmpStart.month, tmpStart.day, tmpStart.hour, tmpStart.minute ]
const ret = {
uid: `${e.id}@${settings.hostname}`,
@ -99,8 +114,8 @@ const exportController = {
}
if (e.end_datetime) {
const tmpEnd = moment.unix(e.end_datetime)
const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
const tmpEnd = DateTime.fromSeconds(e.end_datetime)
const end = [ tmpEnd.year, tmpEnd.month, tmpEnd.day, tmpEnd.hour, tmpEnd.minute ]
ret.end = end
}

View file

@ -56,11 +56,11 @@ const geocodingController = {
},
async nominatimRateLimit(req, res, next) {
geocodingController.providerRateLimit(req, res, next, nominatim.cache)
process.env.NODE_ENV === 'test' ? next() : geocodingController.providerRateLimit(req, res, next, nominatim.cache)
},
async photonRateLimit(req, res, next) {
geocodingController.providerRateLimit(req, res, next, photon.cache)
process.env.NODE_ENV === 'test' ? next() : geocodingController.providerRateLimit(req, res, next, photon.cache)
},
async checkInCache (req, res, details, providerCache) {

View file

@ -18,7 +18,8 @@ const localeController = {
// check if we have a user custom messages
let customLocaleMessages = {}
const customLocalePath = path.resolve(config.user_locale, `${locale}.json`)
const customLocalePath = config.user_locale && path.resolve(config.user_locale, `${locale}.json`)
if (config.user_locale && fs.existsSync(customLocalePath)) {
try {
customLocaleMessages = require(customLocalePath)

View file

@ -18,7 +18,7 @@ module.exports = {
const format = req.params.format || 'json'
log.debug(`Events for place: ${placeName}`)
const events = await eventController._select({ places: String(place.id), show_recurrent: true })
const events = await eventController._select({ places: String(place.id), show_recurrent: true, reverse: true, start_datetime: 0 })
switch (format) {
case 'rss':

View file

@ -88,7 +88,7 @@ const pluginController = {
try {
const plugin = require(pluginFile)
const name = plugin.configuration.name
console.log(`Found plugin '${name}'`)
log.info(`Found plugin '${name}'`)
pluginController.plugins.push(plugin)
if (settingsController.settings['plugin_' + name]) {
const pluginSetting = settingsController.settings['plugin_' + name]

View file

@ -6,7 +6,6 @@ const sharp = require('sharp')
const config = require('../../config')
const generateKeyPair = promisify(crypto.generateKeyPair)
const log = require('../../log')
// const locales = require('../../../locales/index')
const escape = require('lodash/escape')
const DB = require('../models/models')
@ -43,7 +42,8 @@ const defaultSettings = {
enable_trusted_instances: true,
trusted_instances: [],
'theme.is_dark': true,
'theme.primary': '#FF4500',
dark_colors: { primary: '#FF6E40', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FB8C00' },
light_colors: { primary: '#FF4500', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FB8C00' },
trusted_instances_label: '',
hide_thumbs: false,
hide_calendar: false,
@ -104,19 +104,6 @@ const settingsController = {
await settingsController.set('privateKey', privateKey, true)
}
// initialize user_locale
// if (config.user_locale && fs.existsSync(path.resolve(config.user_locale))) {
// const user_locales_files = fs.readdirSync(path.resolve(config.user_locale))
// user_locales_files.forEach( f => {
// const locale = path.basename(f ,'.json')
// if (locales[locale]) {
// log.info(`Adding custom locale ${locale}`)
// settingsController.user_locale[locale] = require(path.resolve(config.user_locale, f)).default
// } else {
// log.warning(`Unknown custom user locale: ${locale} [valid locales are ${locales}]`)
// }
// })
// }
const pluginController = require('./plugins')
pluginController._load()
},

View file

@ -33,7 +33,7 @@ module.exports = {
const eventController = require('./event')
const format = req.params.format || 'json'
const tags = req.params.tag
const events = await eventController._select({ tags: tags.toLocaleLowerCase(), show_recurrent: true })
const events = await eventController._select({ tags: tags.toLocaleLowerCase(), show_recurrent: true, start_datetime: 0, reverse: true })
switch (format) {
case 'rss':
return exportController.feed(req, res, events,

View file

@ -7,11 +7,11 @@ const instanceApiRateLimiter = {
DDOSProtectionApiRateLimiter: (process.env.NODE_ENV === 'test' ? next : rateLimit({
windowMs: 60 * 1000, // 1 minutes
max: 150, // Limit each IP to 150 requests per `window`
max: 250, // Limit each IP to 150 requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: (request, response, next, options) => {
log.warn(`DDOS protection api rate limiter: > 150req/minute/ip ${request.ip}`)
log.warn(`DDOS protection api rate limiter: > 250req/minute/ip ${request.ip}`)
return response.status(options.statusCode).send(options.message)
}
})),

View file

@ -1,6 +1,6 @@
const Email = require('email-templates')
const path = require('path')
const moment = require('dayjs')
const { DateTime } = require('luxon')
const settingsController = require('./controller/settings')
const log = require('../log')
const { Task, TaskManager } = require('../taskManager')
@ -52,6 +52,11 @@ const mail = {
transport: settings.smtp || {}
})
const opt = {
zone: settings.instance_timezone,
locale
}
const msg = {
template,
message: {
@ -61,7 +66,7 @@ const mail = {
...locals,
locale,
config: { title: settings.title, baseurl: settings.baseurl, description: settings.description, admin_email: settings.admin_email },
datetime: datetime => moment.unix(datetime).tz().locale(locale).format('ddd, D MMMM HH:mm')
datetime: timestamp => DateTime.fromSeconds(timestamp, opt).toFormat('EEEE, d MMMM HH:mm')
}
}
return email.send(msg)

View file

@ -1,13 +1,7 @@
const config = require('../../config')
const { htmlToText } = require('html-to-text')
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
const { DateTime } = require('luxon')
dayjs.extend(utc)
dayjs.extend(timezone)
// class Event extends Model {}
module.exports = (sequelize, DataTypes) => {
const Event = sequelize.define('event', {
id: {
@ -41,12 +35,18 @@ module.exports = (sequelize, DataTypes) => {
online_locations: { type: DataTypes.JSON, defaultValue: [] }
})
Event.prototype.toAP = function (username, locale, to = []) {
Event.prototype.toAP = function (settings, to = []) {
const username = settings.instance_name
const opt = {
zone: settings.instance_timezone,
locale: settings.instance_locale
}
const tags = this.tags && this.tags.map(t => t.tag.replace(/[ #]/g, '_'))
const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000))
const content = `
📍 ${this.place && this.place.name}
📅 ${dayjs.unix(this.start_datetime).tz().locale(locale).format('dddd, D MMMM (HH:mm)')}
📅 ${DateTime.fromSeconds(this.start_datetime).toFormat('EEEE, d MMMM (HH:mm)')}
${plainDescription}
`
@ -69,8 +69,8 @@ module.exports = (sequelize, DataTypes) => {
name: this.title,
url: `${config.baseurl}/event/${this.slug || this.id}`,
type: 'Event',
startTime: dayjs.unix(this.start_datetime).tz().locale(locale).format(),
...( this.end_datetime ? { endTime : dayjs.unix(this.end_datetime).tz().locale(locale).format() } : {} ),
startTime: DateTime.fromSeconds(this.start_datetime, opt).toISO(),
...( this.end_datetime ? { endTime : DateTime.fromSeconds(this.end_datetime, opt).toISO() } : {} ),
location: {
name: this.place.name,
address: this.place.address,
@ -83,7 +83,7 @@ module.exports = (sequelize, DataTypes) => {
name: '#' + tag,
href: `${config.baseurl}/tag/${tag}`
})),
published: dayjs(this.createdAt).utc().format(),
published: this.createdAt,
attributedTo: `${config.baseurl}/federation/u/${username}`,
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [`${config.baseurl}/federation/u/${username}/followers`],

View file

@ -15,6 +15,17 @@ let config = {
db: {},
user_locale: path.resolve(process.env.cwd || '', 'user_locale'),
upload_path: path.resolve(process.env.cwd || '', 'uploads'),
proxy: {
protocol: process.env.GANCIO_PROXY_PROTOCOL || '',
hostname: process.env.GANCIO_PROXY_HOSTNAME || '',
host: process.env.GANCIO_PROXY_HOST || '',
port: process.env.GANCIO_PROXY_PORT || '',
auth: {
username: process.env.GANCIO_PROXY_USERNAME || '',
password: process.env.GANCIO_PROXY_PASSWORD || ''
},
headers: process.env.GANCIO_PROXY_HEADERS && JSON.parse(process.env.GANCIO_PROXY_HEADERS) || {}
},
write (config_path= process.env.config_path || './config.json') {
delete config.status
return fs.writeFileSync(config_path, JSON.stringify(config, null, 2))

View file

@ -89,9 +89,7 @@ const Helpers = {
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [...recipients[sharedInbox], `${config.baseurl}/federation/u/${settingsController.settings.instance_name}/followers`],
actor: `${config.baseurl}/federation/u/${settingsController.settings.instance_name}`,
object: event.toAP(settingsController.settings.instance_name,
settingsController.settings.instance_locale,
recipients[sharedInbox])
object: event.toAP(settingsController.settings, recipients[sharedInbox])
}
body['@context'] = [
'https://www.w3.org/ns/activitystreams',

View file

@ -34,7 +34,7 @@ router.get('/m/:event_id', async (req, res) => {
const event = await Event.findByPk(req.params.event_id, { include: [User, Tag, Place] })
if (!event) { return res.status(404).send('Not found') }
const eventAp = event.toAP(settingsController.settings.instance_name, settingsController.settings.instance_locale)
const eventAp = event.toAP(settingsController.settings)
eventAp['@context'] = [
"https://www.w3.org/ns/activitystreams"
]

View file

@ -3,10 +3,8 @@ const { Event, Place, APUser, Tag } = require('../api/models/models')
const escape = require('lodash/escape')
const config = require('../config')
const log = require('../log')
const utc = require('dayjs/plugin/utc')
const dayjs = require('dayjs')
const settingsController = require('../api/controller/settings')
dayjs.extend(utc)
const { DateTime } = require('luxon')
module.exports = {
get (req, res) {
@ -132,9 +130,9 @@ module.exports = {
type: 'Create',
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [`${settings.baseurl}/federation/u/${name}/followers`],
published: dayjs(e.createdAt).utc().format(),
published: e.createdAt,
actor: `${settings.baseurl}/federation/u/${name}`,
object: e.toAP(name, settings.instance_locale)
object: e.toAP(settings)
}))
}
})

View file

@ -87,7 +87,8 @@ module.exports = {
trusted_instances: settings.trusted_instances,
trusted_instances_label: settings.trusted_instances_label,
'theme.is_dark': settings['theme.is_dark'],
'theme.primary': settings['theme.primary'],
dark_colors: settings.dark_colors,
light_colors: settings.light_colors,
hide_thumbs: settings.hide_thumbs,
hide_calendar: settings.hide_calendar,
allow_geolocation: settings.allow_geolocation,
@ -101,9 +102,6 @@ module.exports = {
allow_event_only_online: settings.allow_event_only_online,
allow_event_also_online: settings.allow_event_also_online
}
// set user locale
// res.locals.user_locale = settingsController.user_locale[res.locals.acceptedLocale]
dayjs.tz.setDefault(res.locals.settings.instance_timezone)
next()
},
@ -199,9 +197,9 @@ module.exports = {
* It does supports ICS and H-EVENT
*/
async importURL(req, res) {
const URL = req.query.URL
const url = req.query.URL
try {
const response = await axios.get(URL)
const response = await axios.get(url)
const contentType = response.headers['content-type']
if (contentType.includes('text/html')) {
@ -213,7 +211,7 @@ module.exports = {
const props = e.properties
let media = get(props, 'featured[0]')
if (media) {
media = url.resolve(URL, media)
media = URL.resolve(url, media)
}
return {
title: get(props, 'name[0]', ''),
@ -255,16 +253,17 @@ module.exports = {
let cursor
if (n === -1) {
cursor = date.endOf('month')
cursor = cursor.day(weekday)
if (cursor.month() !== date.month()) {
cursor = cursor.subtract(1, 'week')
cursor = cursor.set({ weekday })
if (cursor.month !== date.month) {
cursor = cursor.minus({ days: 7 })
}
} else {
cursor = date.startOf('month')
cursor = cursor.add(cursor.day() <= date.day() ? n - 1 : n, 'week')
cursor = cursor.day(weekday)
cursor = cursor.plus({ days: cursor.weekday <= date.weekday ? (n-1) * 7 : n * 7})
cursor = cursor.set({ weekday })
}
cursor = cursor.hour(date.hour()).minute(date.minute()).second(0)
cursor = cursor.set({ hour: date.hour, minute: date.minute, second: 0 })
log.debug(cursor)
return cursor
},

View file

@ -1,12 +1,12 @@
const config = require('../server/config')
const db = require('./api/models/index')
const log = require('../server/log')
const { Settings } = require('luxon')
db.initialize()
const settingsController = require('./api/controller/settings')
const initialize = {
// close connections/port/unix socket
async shutdown (exit = true) {
@ -26,9 +26,6 @@ const initialize = {
},
async start () {
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(timezone)
if (config.status == 'CONFIGURED') {
await db.sequelize.authenticate()
log.debug('Running migrations')
@ -57,10 +54,11 @@ const initialize = {
await settingsController.load()
}
dayjs.tz.setDefault(settingsController.settings.instance_timezone)
Settings.defaultLocale = settingsController.settings.instance_locale
Settings.defaultZone = settingsController.settings.instance_timezone
let TaskManager
if (config.status === 'READY' && process.env.NODE_ENV == 'production') {
if (config.status === 'READY' && process.env.NODE_ENV != 'test') {
TaskManager = require('../server/taskManager').TaskManager
TaskManager.start()
}

View file

@ -7,8 +7,13 @@ const helpers = require('./helpers')
const api = require('./api')
async function main () {
const log = require('./log')
try {
await initialize.start()
} catch (e) {
log.error('[ERROR]' + e)
}
app.use([
helpers.initSettings,
@ -19,7 +24,6 @@ async function main () {
// const promBundle = require('express-prom-bundle')
// const metricsMiddleware = promBundle({ includeMethod: true })
const log = require('./log')
app.enable('trust proxy')

View file

@ -1,5 +1,3 @@
import dayjs from 'dayjs'
export const state = () => ({
localSettings : {
hide_thumbs: null,
@ -43,10 +41,10 @@ export const state = () => ({
export const getters = {
hide_thumbs (state) {
return (state.localSettings['hide_thumbs'] === null) ? state.settings.hide_thumbs : state.localSettings.hide_thumbs
return (![true, false].includes(state.localSettings['hide_thumbs'])) ? state.settings.hide_thumbs : state.localSettings.hide_thumbs
},
is_dark (state) {
return (state.localSettings['theme.is_dark'] === null) ? state.settings['theme.is_dark'] : state.localSettings['theme.is_dark']
return (![true, false].includes(state.localSettings['theme.is_dark'])) ? state.settings['theme.is_dark'] : state.localSettings['theme.is_dark']
}
}
@ -75,12 +73,11 @@ export const actions = {
// this method is called server side only for each request for nuxt
// we use it to get configuration from db, set locale, etc...
nuxtServerInit ({ commit }, { res, app }) {
if (res.locals && res.locals.settings) {
commit('setSettings', res.locals.settings)
}
commit('setFilter', { type: 'show_recurrent',
value: res.locals.settings.allow_recurrent_event && res.locals.settings.recurrent_event_visible })
}
commit('setLocalSetting', { key: 'hide_thumbs', value: app.$cookies.get('hide_thumbs') })
commit('setLocalSetting', { key: 'theme.is_dark', value: app.$cookies.get('theme.is_dark') })
@ -112,10 +109,12 @@ export const actions = {
},
async getEvents ({ commit, state }, params = {}) {
const events = await this.$api.getEvents({
start: params.start || dayjs().startOf('month').unix(),
start: params.start || this.$time.startMonth(),
end: params.end || null,
show_recurrent: state.filter.show_recurrent,
show_multidate: state.filter.show_multidate
show_multidate: state.filter.show_multidate,
...( params.query && { query: params.query }),
...( params.older && { older: params.older })
})
commit('setEvents', events)
}

View file

@ -419,11 +419,6 @@ describe('Collection', () => {
describe('Geocoding', () => {
test('should not be enabled by default', async () => {
await request(app)
.post('/api/settings')
.send({ key: 'allow_geolocation', value: false })
.auth(token.access_token, { type: 'bearer' })
.expect(200)
const response = await request(app).get('/api/placeOSM/Nominatim/test')
.expect(403)

184
tests/recurrent.test.js Normal file
View file

@ -0,0 +1,184 @@
const path = require('path')
const { DateTime } = require('luxon')
beforeAll(async () => {
switch (process.env.DB) {
case 'mariadb':
process.env.config_path = path.resolve(__dirname, './seeds/config.mariadb.json')
break
case 'postgresql':
process.env.config_path = path.resolve(__dirname, './seeds/config.postgres.json')
break
case 'sqlite':
default:
process.env.config_path = path.resolve(__dirname, './seeds/config.sqlite.json')
}
try {
await require('../server/initialize.server.js').start()
const { sequelize } = require('../server/api/models/index')
await sequelize.query('DELETE FROM settings')
await sequelize.query('DELETE FROM events')
await sequelize.query('DELETE FROM user_followers')
await sequelize.query('DELETE FROM users')
await sequelize.query('DELETE FROM ap_users')
await sequelize.query('DELETE FROM tags')
await sequelize.query('DELETE FROM places')
await sequelize.query('DELETE FROM filters')
await sequelize.query('DELETE FROM collections')
} catch (e) {
console.error(e)
}
})
afterAll(async () => {
await require('../server/initialize.server.js').shutdown(false)
})
describe('Recurrent events', () => {
test('shoud create an occurrence in future when start date time is in past', async () => {
const eventController = require('../server/api/controller/event')
const { Event } = require('../server/api/models/models')
// each week starting from past
let ret = await Event.create({
title: 'each week starting from past',
is_visible: true,
recurrent: { frequency: '1w' },
start_datetime: DateTime.local(2023, 3, 27, 8).toUnixInteger(),
})
// 27 March 2023 08:00 -> 1w -> 3 April 2023 08:00
let ev = await eventController._createRecurrentOccurrence(ret)
expect(ev.start_datetime).toBe(DateTime.local(2023, 4, 3, 8).toUnixInteger())
// 3 April 2023 08:00 -> 1w -> 10 April 2023 08:00
ev = await eventController._createRecurrentOccurrence(ret, DateTime.fromSeconds(ev.start_datetime+1), false)
expect(ev.start_datetime).toBe(DateTime.local(2023, 4, 10, 8).toUnixInteger())
// weekly test
// data di inizio prima di oggi
// data di inizio dopo di oggi
// test creazione evento successivo (quando quello prima e' skipped)
// stessa cosa per il bisettimanale
// stessa cosa per il mensile (primo, secondo, ultimo mercoldi' del mese)
// stessa cosa per il mensile ordinal (il 4 di ogni mese)
})
test('shoud create an occurrence when start date time is in future', async () => {
const eventController = require('../server/api/controller/event')
const { Event } = require('../server/api/models/models')
// each week starting from future
ret = await Event.create({
title: 'each week starting from future',
is_visible: true,
recurrent: { frequency: '1w' },
start_datetime: DateTime.local(2033, 3, 27, 8).toUnixInteger(),
})
// 27 March 2033 08:00 -> 1w -> 27 March 2033 08:00
ev = await eventController._createRecurrentOccurrence(ret)
expect(ev.start_datetime).toBe(DateTime.local(2033, 3, 27, 8).toUnixInteger())
// 27 March 2033 08:00 -> 1w -> 3 April 2023 08:00
ev = await eventController._createRecurrentOccurrence(ret,DateTime.fromSeconds(ev.start_datetime+1), false)
expect(ev.start_datetime).toBe(DateTime.local(2033, 4, 3, 8).toUnixInteger())
})
test('shoud create a 2 week occurrence in future when start date time is in past', async () => {
const eventController = require('../server/api/controller/event')
const { Event } = require('../server/api/models/models')
// each week starting from past
let ret = await Event.create({
title: 'each 2 weeks starting from past',
is_visible: true,
recurrent: { frequency: '2w' },
start_datetime: DateTime.local(2023, 3, 27, 8).toUnixInteger(),
})
// 27 March 2023 08:00 -> 2w -> 10 April 2023 08:00
let ev = await eventController._createRecurrentOccurrence(ret)
expect(ev.start_datetime).toBe(DateTime.local(2023, 4, 10, 8).toUnixInteger())
// 10 April 2023 08:00 -> 2w -> 24 April 2023 08:00
ev = await eventController._createRecurrentOccurrence(ret, DateTime.fromSeconds(ev.start_datetime+1), false)
expect(ev.start_datetime).toBe(DateTime.local(2023, 4, 24, 8).toUnixInteger())
})
test('shoud create a 2 week occurrence in future when start date time is in future', async () => {
const eventController = require('../server/api/controller/event')
const { Event } = require('../server/api/models/models')
// each week starting from past
let ret = await Event.create({
title: 'each 2 weeks starting from future',
is_visible: true,
recurrent: { frequency: '2w' },
start_datetime: DateTime.local(2033, 3, 27, 8).toUnixInteger(),
})
// 27 March 2023 08:00 -> 2w -> 10 April 2023 08:00
let ev = await eventController._createRecurrentOccurrence(ret)
expect(ev.start_datetime).toBe(DateTime.local(2033, 3, 27, 8).toUnixInteger())
// 27 March 2033 08:00 -> 2w -> 10 April 2033 08:00
ev = await eventController._createRecurrentOccurrence(ret, DateTime.fromSeconds(ev.start_datetime+1), false)
expect(ev.start_datetime).toBe(DateTime.local(2033, 4, 10, 8).toUnixInteger())
})
test('shoud create an occurrence each month in future when start date time is in past', async () => {
const eventController = require('../server/api/controller/event')
const { Event } = require('../server/api/models/models')
// each week starting from past
let ret = await Event.create({
title: 'each month starting from future',
is_visible: true,
recurrent: { frequency: '1m', type: 'ordinal' },
start_datetime: DateTime.local(2033, 3, 27, 8).toUnixInteger(),
})
// 27 March 2023 08:00 -> 2w -> 27 March 2023 08:00
let ev = await eventController._createRecurrentOccurrence(ret)
expect(ev.start_datetime).toBe(DateTime.local(2033, 3, 27, 8).toUnixInteger())
// 27 March 2033 08:00 -> 1m -> 27 April 2033 08:00
ev = await eventController._createRecurrentOccurrence(ret, DateTime.fromSeconds(ev.start_datetime+1), false)
expect(ev.start_datetime).toBe(DateTime.local(2033, 4, 27, 8).toUnixInteger())
})
test('shoud create an occurrence each last monday', async () => {
const eventController = require('../server/api/controller/event')
const { Event } = require('../server/api/models/models')
// each week starting from past
let ret = await Event.create({
title: 'each last monday starting from past',
is_visible: true,
recurrent: { frequency: '1m', type: -1 },
start_datetime: DateTime.local(2023, 3, 27, 8).toUnixInteger(),
})
// 27 March 2033 08:00 -> 1m -> 24 April 2033 08:00
ev = await eventController._createRecurrentOccurrence(ret)
expect(ev.start_datetime).toBe(DateTime.local(2023, 4, 24, 8).toUnixInteger())
// 24 April 2033 08:00 -> 1m -> 29 May 2033 08:00
ev = await eventController._createRecurrentOccurrence(ret, DateTime.fromSeconds(ev.start_datetime+1), false)
expect(ev.start_datetime).toBe(DateTime.local(2023, 5, 29, 8).toUnixInteger())
})
})

View file

@ -9,7 +9,7 @@ rss(version='2.0' xmlns:atom="http://www.w3.org/2005/Atom")
item
if (event.media && event.media.length)
<enclosure url="#{settings.baseurl}/media/#{event.media[0].url}" type='image/jpeg' length="#{event.media[0].size||1}"></enclosure>
title [#{moment.unix(event.start_datetime).format("YY-MM-DD")}] #{event.title} @ #{event.place.name}
title [#{unixFormat(event.start_datetime,"yy-MM-dd")}] #{event.title} @ #{event.place.name}
link #{settings.baseurl}/event/#{event.slug || event.id}
each tag in event.tags
category #{tag.tag}
@ -17,7 +17,7 @@ rss(version='2.0' xmlns:atom="http://www.w3.org/2005/Atom")
| <![CDATA[
| <h4>#{event.title}</h4>
| <strong>#{event.place.name} - #{event.place.address}</strong>
| <small>(#{moment.unix(event.start_datetime).format("dddd, D MMMM HH:mm")})</small><br/>
| <small>(#{unixFormat(event.start_datetime,"EEEE, d MMMM HH:mm")})</small><br/>
if (event.media && event.media.length)
| <img alt="#{event.media[0].name || ''}" src="#{settings.baseurl}/media/#{event.media[0].url}"/>
| !{event.description}

25
vuetify.options.js Normal file
View file

@ -0,0 +1,25 @@
const minifyTheme = require('minify-css-string').default
import { ca, de, en, es, eu, fr, gl, it, nb, nl, pl, pt, sk, ru, zhHans } from 'vuetify/es5/locale'
export default ({ res, nuxtState }) => {
const settings = process.server ? res.locals.settings : nuxtState.state.settings
return {
lang: { locales: { ca, de, en, es, eu, fr, gl, it, nb, nl, pl, pt, sk, ru, zhHans } },
treeShake: true,
theme: {
options: {
customProperties: false,
variations: false,
minifyTheme,
},
dark: settings['theme.is_dark'],
themes: {
dark: settings.dark_colors,
light: settings.light_colors
}
},
defaultAssets: false
}
}

View file

@ -3,7 +3,7 @@
Plugin Name: WPGancio
Plugin URI: https://gancio.org
Description: Connects an user of a gancio instance to a Wordpress user so that published events are automatically pushed with Gancio API.
Version: 1.5
Version: 1.7
Author: Gancio
License: AGPL 3.0
@ -35,9 +35,3 @@ require_once(WPGANCIO_DIR . 'oauth.php');
* It requires an event manager plugin, only Event Organiser (https://wp-event-organiser.com/) is
* supported until now but to add another plugin it's easy.
*/
/**
* - Add Gancio Settings page to select an instance URL and...
* - start an OAuth 2.0 authentication flow with the selected instance
* - Send each new / updated events to the selected instance via Gancio API
*/

View file

@ -35,9 +35,10 @@ function wpgancio_save_event ($post_id) {
return sanitize_title($tag->name);
}
// TODO: merge event tags with post tags
$tmp_tags = get_the_terms( $event, 'event-tag' );
if ($tmp_tags) {
$tags = array_map('tagName', $tmp_tags);
}
$gancio_id = get_post_meta($post_id, 'wpgancio_gancio_id', TRUE);
@ -55,9 +56,8 @@ function wpgancio_save_event ($post_id) {
'tags' => $tags,
'description' => $event->post_content,
'start_datetime' => intval($start_datetime),
'end_datetime' => intval($end_datetime),
'place_name' => $place_name,
'place_address' => "${place_address['address']}, ${place_address['city']}"
'place_address' => $place_address['address'] . ", " . $place_address['city']
);
// add image if specified

View file

@ -4,7 +4,7 @@ Donate link: https://gancio.org
Tags: events, gancio, fediverse, AP, activity pub
Requires at least: 4.7
Tested up to: 6.0
Stable tag: 1.6
Stable tag: 1.7
Requires PHP: 7.0
License: AGPLv3 or later
License URI: https://www.gnu.org/licenses/agpl-3.0.html
@ -18,6 +18,9 @@ for this to work an event manager plugin is required, only [Event Organiser](htt
== Changelog ==
= 1.7 =
* Fix merge event tags while posting to an instance
= 1.6 =
* Support MU installation

2924
yarn.lock

File diff suppressed because it is too large Load diff