diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 66a805a23..739e569f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -127,14 +127,14 @@ exunit: - test-junit-report.xml expire_in: 30 days -jest: +vitest: stage: test needs: - lint-front before_script: - yarn --cwd "js" install --frozen-lockfile script: - - yarn --cwd "js" run test:unit --no-color --ci --reporters=default --reporters=jest-junit + - yarn --cwd "js" run coverage --reporter=default --reporter=junit --outputFile.junit=./junit.xml artifacts: when: always paths: diff --git a/.tool-versions b/.tool-versions index 3162337da..73654c14d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.13.4-otp-24 -erlang 24.3.3 +elixir 1.14.0-otp-25 +erlang 25.0.4 diff --git a/config/config.exs b/config/config.exs index f5dc26cb5..1ebf3e65a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -54,7 +54,7 @@ config :mobilizon, Mobilizon.Web.Endpoint, secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM", render_errors: [view: Mobilizon.Web.ErrorView, accepts: ~w(html json)], pubsub_server: Mobilizon.PubSub, - cache_static_manifest: "priv/static/manifest.json", + cache_static_manifest: "priv/static/cache_manifest.json", has_reverse_proxy: true config :mime, :types, %{ @@ -123,6 +123,18 @@ config :mobilizon, Mobilizon.Web.Email.Mailer, # can be `true` no_mx_lookups: false +config :vite_phx, + release_app: :mobilizon, + # to tell prod and dev env appart + environment: config_env(), + # this manifest is different from the Phoenix "cache_manifest.json"! + # optional + vite_manifest: "priv/static/manifest.json", + # optional + phx_manifest: "priv/static/cache_manifest.json", + # optional + dev_server_address: "http://localhost:5173" + # Configures Elixir's Logger config :logger, :console, backends: [:console], @@ -347,6 +359,23 @@ config :mobilizon, :exports, config :mobilizon, :analytics, providers: [] +config :mobilizon, Mobilizon.Service.Pictures, service: Mobilizon.Service.Pictures.Unsplash + +config :mobilizon, Mobilizon.Service.Pictures.Unsplash, + app_name: "Mobilizon", + access_key: nil + +config :mobilizon, :search, global: [is_default_search: false, is_enabled: true] + +config :mobilizon, Mobilizon.Service.GlobalSearch, + service: Mobilizon.Service.GlobalSearch.SearchMobilizon + +config :mobilizon, Mobilizon.Service.GlobalSearch.SearchMobilizon, + endpoint: "https://search.joinmobilizon.org", + csp_policy: [ + img_src: "search.joinmobilizon.org" + ] + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index 9a37eef89..77c55e544 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -15,13 +15,7 @@ config :mobilizon, Mobilizon.Web.Endpoint, check_origin: false, watchers: [ node: [ - "node_modules/webpack/bin/webpack.js", - "--mode", - "development", - "--watch", - "--watch-options-stdin", - "--config", - "node_modules/@vue/cli-service/webpack.config.js", + "node_modules/.bin/vite", cd: Path.expand("../js", __DIR__) ] ] @@ -102,3 +96,5 @@ config :mobilizon, :anonymous, reports: [ allowed: true ] + +config :unplug, :init_mode, :runtime diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile index 4c87c709b..6342d6847 100644 --- a/docker/tests/Dockerfile +++ b/docker/tests/Dockerfile @@ -1,7 +1,7 @@ FROM elixir:latest LABEL maintainer="Thomas Citharel " -ENV REFRESHED_AT=2022-04-06 +ENV REFRESHED_AT=2022-09-20 RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq RUN npm install -g yarn wait-on diff --git a/js/.eslintrc.js b/js/.eslintrc.js index 10dcd95e4..101b36075 100644 --- a/js/.eslintrc.js +++ b/js/.eslintrc.js @@ -1,3 +1,6 @@ +/* eslint-env node */ +require("@rushstack/eslint-patch/modern-module-resolution"); + module.exports = { root: true, @@ -6,10 +9,11 @@ module.exports = { }, extends: [ - "plugin:vue/essential", "eslint:recommended", - "@vue/typescript/recommended", + "plugin:vue/vue3-essential", + "@vue/eslint-config-typescript/recommended", "plugin:prettier/recommended", + "@vue/eslint-config-prettier", ], plugins: ["prettier"], @@ -20,12 +24,11 @@ module.exports = { }, rules: { - "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "no-underscore-dangle": [ "error", { - allow: ["__typename"], + allow: ["__typename", "__schema"], }, ], "@typescript-eslint/no-explicit-any": "off", @@ -50,4 +53,7 @@ module.exports = { }, ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"], + globals: { + GeolocationPositionError: true, + }, }; diff --git a/js/.gitignore b/js/.gitignore index b3a5de1e2..5c5176c48 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -5,6 +5,7 @@ node_modules /tests/e2e/videos/ /tests/e2e/screenshots/ /coverage +stats.html # local env files .env.local @@ -23,3 +24,6 @@ yarn-error.log* *.njsproj *.sln *.sw? +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/js/babel.config.js b/js/babel.config.js deleted file mode 100644 index 162a3ea97..000000000 --- a/js/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ["@vue/cli-plugin-babel/preset"], -}; diff --git a/js/env.d.ts b/js/env.d.ts new file mode 100644 index 000000000..f284c30a9 --- /dev/null +++ b/js/env.d.ts @@ -0,0 +1,12 @@ +/// + +/// + +interface ImportMetaEnv { + readonly VITE_SERVER_URL: string; + readonly VITE_HISTOIRE_ENV: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/js/get_union_json.ts b/js/get_union_json.ts index 287cc9982..9a36881d1 100644 --- a/js/get_union_json.ts +++ b/js/get_union_json.ts @@ -1,5 +1,5 @@ -const fetch = require("node-fetch"); -const fs = require("fs"); +import fetch from "node-fetch"; +import fs from "fs"; fetch(`http://localhost:4000/api`, { method: "POST", diff --git a/js/histoire.config.ts b/js/histoire.config.ts new file mode 100644 index 000000000..349d77575 --- /dev/null +++ b/js/histoire.config.ts @@ -0,0 +1,51 @@ +/// + +import { defineConfig } from "histoire"; +import { HstVue } from "@histoire/plugin-vue"; +import path from "path"; + +export default defineConfig({ + plugins: [HstVue()], + setupFile: path.resolve(__dirname, "./src/histoire.setup.ts"), + viteNodeInlineDeps: [/date-fns/], + tree: { + groups: [ + { + title: "Actors", + include: (file) => /^src\/components\/Account/.test(file.path), + }, + { + title: "Address", + include: (file) => /^src\/components\/Address/.test(file.path), + }, + { + title: "Comments", + include: (file) => /^src\/components\/Comment/.test(file.path), + }, + { + title: "Discussion", + include: (file) => /^src\/components\/Discussion/.test(file.path), + }, + { + title: "Events", + include: (file) => /^src\/components\/Event/.test(file.path), + }, + { + title: "Groups", + include: (file) => /^src\/components\/Group/.test(file.path), + }, + { + title: "Home", + include: (file) => /^src\/components\/Home/.test(file.path), + }, + { + title: "Posts", + include: (file) => /^src\/components\/Post/.test(file.path), + }, + { + title: "Others", + include: () => true, + }, + ], + }, +}); diff --git a/js/jest.config.js b/js/jest.config.js deleted file mode 100644 index a87688fe7..000000000 --- a/js/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel", - collectCoverage: true, - collectCoverageFrom: [ - "**/*.{vue,ts}", - "!**/node_modules/**", - "!get_union_json.ts", - ], - coverageReporters: ["html", "text", "text-summary"], - reporters: ["default", "jest-junit"], - // The following should fix the issue with svgs and ?inline loader (see Logo.vue), but doesn't work - // - // transform: { - // "^.+\\.svg$": "/tests/unit/svgTransform.js", - // }, - // moduleNameMapper: { - // "^@/(.*svg)(\\?inline)$": "/src/$1", - // "^@/(.*)$": "/src/$1", - // }, -}; diff --git a/js/package.json b/js/package.json index 79073992b..4287e202f 100644 --- a/js/package.json +++ b/js/package.json @@ -3,19 +3,25 @@ "version": "2.1.0", "private": true, "scripts": { - "serve": "vue-cli-service serve", + "dev": "vite", + "preview": "vite preview", "build": "yarn run build:assets && yarn run build:pictures", - "test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=UTC vue-cli-service test:unit", - "test:e2e": "vue-cli-service test:e2e", - "lint": "vue-cli-service lint", - "build:assets": "vue-cli-service build --report", - "build:pictures": "bash ./scripts/build/pictures.sh" + "lint": "eslint --ext .ts,.vue --ignore-path .gitignore --fix src", + "format": "prettier . --write", + "build:assets": "vite build", + "build:pictures": "bash ./scripts/build/pictures.sh", + "story:dev": "histoire dev", + "story:build": "histoire build", + "story:preview": "histoire preview", + "test": "vitest", + "coverage": "vitest run --coverage" }, "dependencies": { "@absinthe/socket": "^0.2.1", "@absinthe/socket-apollo-link": "^0.2.1", "@apollo/client": "^3.3.16", - "@mdi/font": "^6.1.95", + "@headlessui/vue": "^1.6.7", + "@oruga-ui/oruga-next": "^0.5.5", "@sentry/tracing": "^7.1", "@sentry/vue": "^7.1", "@tailwindcss/line-clamp": "^0.4.0", @@ -39,24 +45,32 @@ "@tiptap/extension-strike": "^2.0.0-beta.26", "@tiptap/extension-text": "^2.0.0-beta.15", "@tiptap/extension-underline": "^2.0.0-beta.7", - "@tiptap/vue-2": "^2.0.0-beta.21", + "@tiptap/suggestion": "^2.0.0-beta.195", + "@tiptap/vue-3": "^2.0.0-beta.96", "@vue-a11y/announcer": "^2.1.0", "@vue-a11y/skip-to": "^2.1.2", - "@vue/apollo-option": "4.0.0-alpha.11", + "@vue-leaflet/vue-leaflet": "^0.6.1", + "@vue/apollo-composable": "^4.0.0-alpha.17", + "@vue/compiler-sfc": "^3.2.37", + "@vueuse/core": "^9.1.0", + "@vueuse/head": "^0.7.9", + "@vueuse/router": "^9.0.2", "apollo-absinthe-upload-link": "^1.5.0", "autoprefixer": "^10", - "blurhash": "^1.1.3", - "buefy": "^0.9.0", + "blurhash": "^2.0.0", + "bulma": "^0.9.4", "bulma-divider": "^0.2.0", - "core-js": "^3.6.4", "date-fns": "^2.16.0", "date-fns-tz": "^1.1.6", - "graphql": "^16.0.0", + "floating-vue": "^2.0.0-beta.17", + "graphql": "^15.8.0", "graphql-tag": "^2.10.3", + "hammerjs": "^2.0.8", "intersection-observer": "^0.12.0", "jwt-decode": "^3.1.2", "leaflet": "^1.4.0", "leaflet.locatecontrol": "^0.76.0", + "leaflet.markercluster": "^1.5.3", "lodash": "^4.17.11", "ngeohash": "^0.6.3", "p-debounce": "^4.0.0", @@ -67,24 +81,28 @@ "tailwindcss": "^3", "tippy.js": "^6.2.3", "unfetch": "^4.2.0", - "v-tooltip": "^2.1.3", - "vue": "^2.6.11", - "vue-class-component": "^7.2.3", - "vue-i18n": "^8.14.0", + "vue": "^3.2.37", + "vue-i18n": "9", + "vue-material-design-icons": "^5.1.2", "vue-matomo": "^4.1.0", "vue-meta": "^2.3.1", "vue-plausible": "^1.3.1", - "vue-property-decorator": "^9.0.0", - "vue-router": "^3.1.6", + "vue-router": "4", "vue-scrollto": "^2.17.1", - "vue2-leaflet": "^2.0.3", - "vuedraggable": "^2.24.3" + "vue-use-route-query": "^1.1.0", + "vuedraggable": "^4.1.0" }, "devDependencies": { - "@rushstack/eslint-patch": "^1.1.0", - "@types/jest": "^28.0.0", + "@histoire/plugin-vue": "^0.10.0", + "@intlify/vite-plugin-vue-i18n": "^6.0.0", + "@playwright/test": "^1.25.1", + "@rushstack/eslint-patch": "^1.1.4", + "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/typography": "^0.5.4", + "@types/hammerjs": "^2.0.41", "@types/leaflet": "^1.5.2", "@types/leaflet.locatecontrol": "^0.74", + "@types/leaflet.markercluster": "^1.5.1", "@types/lodash": "^4.14.141", "@types/ngeohash": "^0.6.2", "@types/phoenix": "^1.5.2", @@ -93,37 +111,29 @@ "@types/prosemirror-state": "^1.2.4", "@types/prosemirror-view": "^1.11.4", "@types/sanitize-html": "^2.5.0", - "@typescript-eslint/eslint-plugin": "^5.3.0", - "@typescript-eslint/parser": "^5.3.0", - "@vue/cli-plugin-babel": "~5.0.6", - "@vue/cli-plugin-eslint": "~5.0.6", - "@vue/cli-plugin-pwa": "~5.0.6", - "@vue/cli-plugin-router": "~5.0.6", - "@vue/cli-plugin-typescript": "~5.0.6", - "@vue/cli-plugin-unit-jest": "~5.0.6", - "@vue/cli-service": "~5.0.6", + "@vitejs/plugin-vue": "^3.0.3", + "@vitest/coverage-c8": "^0.23.4", + "@vitest/ui": "^0.23.4", + "@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.0", - "@vue/test-utils": "^1.1.0", - "@vue/vue2-jest": "^28.0.0", - "babel-jest": "^28.1.1", - "eslint": "^8.2.0", + "@vue/test-utils": "^2.0.2", + "eslint": "^8.21.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.20.2", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-vue": "^9.1.1", + "eslint-plugin-vue": "^9.3.0", "flush-promises": "^1.0.2", - "jest": "^28.1.1", - "jest-junit": "^13.0.0", + "histoire": "^0.10.4", + "jsdom": "^20.0.0", "mock-apollo-client": "^1.1.0", "prettier": "^2.2.1", "prettier-eslint": "^15.0.1", + "rollup-plugin-visualizer": "^5.7.1", "sass": "^1.34.1", - "sass-loader": "^13.0.0", - "ts-jest": "28", - "typescript": "~4.5.5", - "vue-cli-plugin-tailwind": "~3.0.0", - "vue-i18n-extract": "^2.0.4", - "vue-template-compiler": "^2.6.11", - "webpack-cli": "^4.7.0" + "typescript": "~4.8.3", + "vite": "^3.0.9", + "vite-plugin-pwa": "^0.13.0", + "vitest": "^0.23.3", + "vue-i18n-extract": "^2.0.4" } } diff --git a/js/playwright.config.ts b/js/playwright.config.ts new file mode 100644 index 000000000..c3da6c26e --- /dev/null +++ b/js/playwright.config.ts @@ -0,0 +1,107 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; +import { devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: "./tests/e2e", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:4005", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + }, + }, + + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + }, + }, + + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/js/public/img/categories/arts-small.webp b/js/public/img/categories/arts-small.webp new file mode 100644 index 000000000..2d5f4b411 Binary files /dev/null and b/js/public/img/categories/arts-small.webp differ diff --git a/js/public/img/categories/arts.webp b/js/public/img/categories/arts.webp new file mode 100644 index 000000000..a9fab2781 Binary files /dev/null and b/js/public/img/categories/arts.webp differ diff --git a/js/public/img/categories/business-small.webp b/js/public/img/categories/business-small.webp new file mode 100644 index 000000000..967246ba0 Binary files /dev/null and b/js/public/img/categories/business-small.webp differ diff --git a/js/public/img/categories/business.webp b/js/public/img/categories/business.webp new file mode 100644 index 000000000..06d4d2c9d Binary files /dev/null and b/js/public/img/categories/business.webp differ diff --git a/js/public/img/categories/crafts-small.webp b/js/public/img/categories/crafts-small.webp new file mode 100644 index 000000000..97c743d60 Binary files /dev/null and b/js/public/img/categories/crafts-small.webp differ diff --git a/js/public/img/categories/crafts.webp b/js/public/img/categories/crafts.webp new file mode 100644 index 000000000..6dbe7a670 Binary files /dev/null and b/js/public/img/categories/crafts.webp differ diff --git a/js/public/img/categories/film_media-small.webp b/js/public/img/categories/film_media-small.webp new file mode 100644 index 000000000..deafb6b03 Binary files /dev/null and b/js/public/img/categories/film_media-small.webp differ diff --git a/js/public/img/categories/film_media.webp b/js/public/img/categories/film_media.webp new file mode 100644 index 000000000..c50ef4338 Binary files /dev/null and b/js/public/img/categories/film_media.webp differ diff --git a/js/public/img/categories/food_drink-small.webp b/js/public/img/categories/food_drink-small.webp new file mode 100644 index 000000000..71d984907 Binary files /dev/null and b/js/public/img/categories/food_drink-small.webp differ diff --git a/js/public/img/categories/food_drink.webp b/js/public/img/categories/food_drink.webp new file mode 100644 index 000000000..0225ca512 Binary files /dev/null and b/js/public/img/categories/food_drink.webp differ diff --git a/js/public/img/categories/games-small.webp b/js/public/img/categories/games-small.webp new file mode 100644 index 000000000..8095816a1 Binary files /dev/null and b/js/public/img/categories/games-small.webp differ diff --git a/js/public/img/categories/games.webp b/js/public/img/categories/games.webp new file mode 100644 index 000000000..56a069448 Binary files /dev/null and b/js/public/img/categories/games.webp differ diff --git a/js/public/img/categories/health-small.webp b/js/public/img/categories/health-small.webp new file mode 100644 index 000000000..1514b3ee3 Binary files /dev/null and b/js/public/img/categories/health-small.webp differ diff --git a/js/public/img/categories/health.webp b/js/public/img/categories/health.webp new file mode 100644 index 000000000..fcec8cea3 Binary files /dev/null and b/js/public/img/categories/health.webp differ diff --git a/js/public/img/categories/lgbtq-small.webp b/js/public/img/categories/lgbtq-small.webp new file mode 100644 index 000000000..d3f70c9b3 Binary files /dev/null and b/js/public/img/categories/lgbtq-small.webp differ diff --git a/js/public/img/categories/lgbtq.webp b/js/public/img/categories/lgbtq.webp new file mode 100644 index 000000000..72b460152 Binary files /dev/null and b/js/public/img/categories/lgbtq.webp differ diff --git a/js/public/img/categories/movements_politics-small.webp b/js/public/img/categories/movements_politics-small.webp new file mode 100644 index 000000000..e7c47b4cb Binary files /dev/null and b/js/public/img/categories/movements_politics-small.webp differ diff --git a/js/public/img/categories/movements_politics.webp b/js/public/img/categories/movements_politics.webp new file mode 100644 index 000000000..987dbbddf Binary files /dev/null and b/js/public/img/categories/movements_politics.webp differ diff --git a/js/public/img/categories/music-small.webp b/js/public/img/categories/music-small.webp new file mode 100644 index 000000000..856ec7184 Binary files /dev/null and b/js/public/img/categories/music-small.webp differ diff --git a/js/public/img/categories/music.webp b/js/public/img/categories/music.webp new file mode 100644 index 000000000..4efa3fef2 Binary files /dev/null and b/js/public/img/categories/music.webp differ diff --git a/js/public/img/categories/outdoors_adventure-small.webp b/js/public/img/categories/outdoors_adventure-small.webp new file mode 100644 index 000000000..79a2027c0 Binary files /dev/null and b/js/public/img/categories/outdoors_adventure-small.webp differ diff --git a/js/public/img/categories/outdoors_adventure.webp b/js/public/img/categories/outdoors_adventure.webp new file mode 100644 index 000000000..1eac571b7 Binary files /dev/null and b/js/public/img/categories/outdoors_adventure.webp differ diff --git a/js/public/img/categories/party-small.webp b/js/public/img/categories/party-small.webp new file mode 100644 index 000000000..a52d1c406 Binary files /dev/null and b/js/public/img/categories/party-small.webp differ diff --git a/js/public/img/categories/party.webp b/js/public/img/categories/party.webp new file mode 100644 index 000000000..8e3bc6b3a Binary files /dev/null and b/js/public/img/categories/party.webp differ diff --git a/js/public/img/categories/photography-small.webp b/js/public/img/categories/photography-small.webp new file mode 100644 index 000000000..0ac0863bf Binary files /dev/null and b/js/public/img/categories/photography-small.webp differ diff --git a/js/public/img/categories/photography.webp b/js/public/img/categories/photography.webp new file mode 100644 index 000000000..082401eda Binary files /dev/null and b/js/public/img/categories/photography.webp differ diff --git a/js/public/img/categories/spirituality_religion_beliefs-small.webp b/js/public/img/categories/spirituality_religion_beliefs-small.webp new file mode 100644 index 000000000..bff9c3798 Binary files /dev/null and b/js/public/img/categories/spirituality_religion_beliefs-small.webp differ diff --git a/js/public/img/categories/spirituality_religion_beliefs.webp b/js/public/img/categories/spirituality_religion_beliefs.webp new file mode 100644 index 000000000..3022135b8 Binary files /dev/null and b/js/public/img/categories/spirituality_religion_beliefs.webp differ diff --git a/js/public/img/categories/sports-small.webp b/js/public/img/categories/sports-small.webp new file mode 100644 index 000000000..0f6367432 Binary files /dev/null and b/js/public/img/categories/sports-small.webp differ diff --git a/js/public/img/categories/sports.webp b/js/public/img/categories/sports.webp new file mode 100644 index 000000000..304eff1cf Binary files /dev/null and b/js/public/img/categories/sports.webp differ diff --git a/js/public/img/categories/theatre-small.webp b/js/public/img/categories/theatre-small.webp new file mode 100644 index 000000000..ff0c6575f Binary files /dev/null and b/js/public/img/categories/theatre-small.webp differ diff --git a/js/public/img/categories/theatre.webp b/js/public/img/categories/theatre.webp new file mode 100644 index 000000000..766e608c7 Binary files /dev/null and b/js/public/img/categories/theatre.webp differ diff --git a/js/public/img/koena-a11y.svg b/js/public/img/koena-a11y.svg deleted file mode 100644 index 4ef920d2f..000000000 --- a/js/public/img/koena-a11y.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/js/src/assets/logo.svg b/js/public/img/logo.svg similarity index 100% rename from js/src/assets/logo.svg rename to js/public/img/logo.svg diff --git a/js/public/img/online-event.webp b/js/public/img/online-event.webp new file mode 100644 index 000000000..c20e97658 Binary files /dev/null and b/js/public/img/online-event.webp differ diff --git a/js/public/img/pics/error.jpg b/js/public/img/pics/error.jpg deleted file mode 100644 index 46752640e..000000000 Binary files a/js/public/img/pics/error.jpg and /dev/null differ diff --git a/js/public/img/pics/error.webp b/js/public/img/pics/error.webp new file mode 100644 index 000000000..d807645e5 Binary files /dev/null and b/js/public/img/pics/error.webp differ diff --git a/js/public/img/pics/event_creation-1024w.jpg b/js/public/img/pics/event_creation-1024w.jpg deleted file mode 100644 index 57c879268..000000000 Binary files a/js/public/img/pics/event_creation-1024w.jpg and /dev/null differ diff --git a/js/public/img/pics/event_creation-1024w.webp b/js/public/img/pics/event_creation-1024w.webp index 887137e16..96f31769c 100644 Binary files a/js/public/img/pics/event_creation-1024w.webp and b/js/public/img/pics/event_creation-1024w.webp differ diff --git a/js/public/img/pics/event_creation-480w.jpg b/js/public/img/pics/event_creation-480w.jpg deleted file mode 100644 index 004c872c0..000000000 Binary files a/js/public/img/pics/event_creation-480w.jpg and /dev/null differ diff --git a/js/public/img/pics/event_creation-480w.webp b/js/public/img/pics/event_creation-480w.webp index 7b6c6fda8..15f143247 100644 Binary files a/js/public/img/pics/event_creation-480w.webp and b/js/public/img/pics/event_creation-480w.webp differ diff --git a/js/public/img/pics/event_creation.jpg b/js/public/img/pics/event_creation.jpg deleted file mode 100644 index 227c4affc..000000000 Binary files a/js/public/img/pics/event_creation.jpg and /dev/null differ diff --git a/js/public/img/pics/event_creation.webp b/js/public/img/pics/event_creation.webp new file mode 100644 index 000000000..7406af41d Binary files /dev/null and b/js/public/img/pics/event_creation.webp differ diff --git a/js/public/img/pics/footer_1.jpg b/js/public/img/pics/footer_1.jpg deleted file mode 100644 index 15a7a887e..000000000 Binary files a/js/public/img/pics/footer_1.jpg and /dev/null differ diff --git a/js/public/img/pics/footer_1.webp b/js/public/img/pics/footer_1.webp new file mode 100644 index 000000000..e6685dec6 Binary files /dev/null and b/js/public/img/pics/footer_1.webp differ diff --git a/js/public/img/pics/footer_2.jpg b/js/public/img/pics/footer_2.jpg deleted file mode 100644 index 389a1c88d..000000000 Binary files a/js/public/img/pics/footer_2.jpg and /dev/null differ diff --git a/js/public/img/pics/footer_2.webp b/js/public/img/pics/footer_2.webp new file mode 100644 index 000000000..e438b2cb9 Binary files /dev/null and b/js/public/img/pics/footer_2.webp differ diff --git a/js/public/img/pics/footer_3.jpg b/js/public/img/pics/footer_3.jpg deleted file mode 100644 index d126878b4..000000000 Binary files a/js/public/img/pics/footer_3.jpg and /dev/null differ diff --git a/js/public/img/pics/footer_3.webp b/js/public/img/pics/footer_3.webp new file mode 100644 index 000000000..be647fe19 Binary files /dev/null and b/js/public/img/pics/footer_3.webp differ diff --git a/js/public/img/pics/footer_4.jpg b/js/public/img/pics/footer_4.jpg deleted file mode 100644 index 50a35c9ac..000000000 Binary files a/js/public/img/pics/footer_4.jpg and /dev/null differ diff --git a/js/public/img/pics/footer_4.webp b/js/public/img/pics/footer_4.webp new file mode 100644 index 000000000..1d498275b Binary files /dev/null and b/js/public/img/pics/footer_4.webp differ diff --git a/js/public/img/pics/footer_5.jpg b/js/public/img/pics/footer_5.jpg deleted file mode 100644 index e67a5bf66..000000000 Binary files a/js/public/img/pics/footer_5.jpg and /dev/null differ diff --git a/js/public/img/pics/footer_5.webp b/js/public/img/pics/footer_5.webp new file mode 100644 index 000000000..3fd2a6b90 Binary files /dev/null and b/js/public/img/pics/footer_5.webp differ diff --git a/js/public/img/pics/group-1024w.jpg b/js/public/img/pics/group-1024w.jpg deleted file mode 100644 index 5e7e9c329..000000000 Binary files a/js/public/img/pics/group-1024w.jpg and /dev/null differ diff --git a/js/public/img/pics/group-1024w.webp b/js/public/img/pics/group-1024w.webp index 3e9463f90..fea1fde5d 100644 Binary files a/js/public/img/pics/group-1024w.webp and b/js/public/img/pics/group-1024w.webp differ diff --git a/js/public/img/pics/group-480w.jpg b/js/public/img/pics/group-480w.jpg deleted file mode 100644 index b003dd502..000000000 Binary files a/js/public/img/pics/group-480w.jpg and /dev/null differ diff --git a/js/public/img/pics/group-480w.webp b/js/public/img/pics/group-480w.webp index 8e8380625..319d1257f 100644 Binary files a/js/public/img/pics/group-480w.webp and b/js/public/img/pics/group-480w.webp differ diff --git a/js/public/img/pics/group.jpg b/js/public/img/pics/group.jpg deleted file mode 100644 index 039149cd2..000000000 Binary files a/js/public/img/pics/group.jpg and /dev/null differ diff --git a/js/public/img/pics/group.webp b/js/public/img/pics/group.webp new file mode 100644 index 000000000..26ef07440 Binary files /dev/null and b/js/public/img/pics/group.webp differ diff --git a/js/public/img/pics/homepage.jpg b/js/public/img/pics/homepage.jpg deleted file mode 100644 index 8fd9d5835..000000000 Binary files a/js/public/img/pics/homepage.jpg and /dev/null differ diff --git a/js/public/img/pics/homepage.webp b/js/public/img/pics/homepage.webp new file mode 100644 index 000000000..0d0791a50 Binary files /dev/null and b/js/public/img/pics/homepage.webp differ diff --git a/js/public/img/pics/homepage_background-1024w.webp b/js/public/img/pics/homepage_background-1024w.webp deleted file mode 100644 index 9f63b55e5..000000000 Binary files a/js/public/img/pics/homepage_background-1024w.webp and /dev/null differ diff --git a/js/public/img/pics/realisation.jpg b/js/public/img/pics/realisation.jpg deleted file mode 100644 index ecc4610f0..000000000 Binary files a/js/public/img/pics/realisation.jpg and /dev/null differ diff --git a/js/public/img/pics/realisation.webp b/js/public/img/pics/realisation.webp new file mode 100644 index 000000000..739b6df84 Binary files /dev/null and b/js/public/img/pics/realisation.webp differ diff --git a/js/public/img/pics/rose.jpg b/js/public/img/pics/rose.jpg deleted file mode 100644 index 1689c50ec..000000000 Binary files a/js/public/img/pics/rose.jpg and /dev/null differ diff --git a/js/public/img/pics/rose.webp b/js/public/img/pics/rose.webp new file mode 100644 index 000000000..9d8328860 Binary files /dev/null and b/js/public/img/pics/rose.webp differ diff --git a/js/public/img/shape-1.svg b/js/public/img/shape-1.svg new file mode 100644 index 000000000..c8a8128df --- /dev/null +++ b/js/public/img/shape-1.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/js/public/img/shape-2.svg b/js/public/img/shape-2.svg new file mode 100644 index 000000000..8c0b87491 --- /dev/null +++ b/js/public/img/shape-2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/js/public/img/shape-3.svg b/js/public/img/shape-3.svg new file mode 100644 index 000000000..dba085f3c --- /dev/null +++ b/js/public/img/shape-3.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/js/public/index.html b/js/public/index.html deleted file mode 100644 index 083951877..000000000 --- a/js/public/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - -
- - - diff --git a/js/scripts/build/pictures.sh b/js/scripts/build/pictures.sh index d19fec20a..999d136ef 100755 --- a/js/scripts/build/pictures.sh +++ b/js/scripts/build/pictures.sh @@ -30,11 +30,6 @@ convert_image () { convert -geometry "$resolution"x $file $output } -produce_webp () { - name=$(file_name) - output="$output_dir/$name.webp" - cwebp $file -quiet -o $output -} progress() { local w=80 p=$1; shift @@ -68,23 +63,3 @@ do fi done echo -e "\nDone!" - -echo "Generating optimized versions of the pictures…" - -if ! command -v cwebp &> /dev/null -then - echo "$(tput setaf 1)ERROR: The cwebp command could not be found. You need to install webp.$(tput sgr 0)" - exit 1 -fi - -nb_files=$( shopt -s nullglob ; set -- $output_dir/* ; echo $#) -i=1 -for file in $output_dir/* -do - if [[ -f $file ]]; then - produce_webp - progress $(($i*100/$nb_files)) still working... - i=$((i+1)) - fi -done -echo -e "\nDone!" \ No newline at end of file diff --git a/js/src/App.vue b/js/src/App.vue index 309331248..175d22d41 100644 --- a/js/src/App.vue +++ b/js/src/App.vue @@ -1,267 +1,277 @@ - diff --git a/js/src/components/Account/ActorInline.story.vue b/js/src/components/Account/ActorInline.story.vue new file mode 100644 index 000000000..40195eb80 --- /dev/null +++ b/js/src/components/Account/ActorInline.story.vue @@ -0,0 +1,52 @@ + + diff --git a/js/src/components/Account/ActorInline.vue b/js/src/components/Account/ActorInline.vue index 4857b8607..a6e712a84 100644 --- a/js/src/components/Account/ActorInline.vue +++ b/js/src/components/Account/ActorInline.vue @@ -1,34 +1,37 @@ - diff --git a/js/src/components/Account/PopoverActorCard.story.vue b/js/src/components/Account/PopoverActorCard.story.vue new file mode 100644 index 000000000..4f06faf1c --- /dev/null +++ b/js/src/components/Account/PopoverActorCard.story.vue @@ -0,0 +1,59 @@ + + + diff --git a/js/src/components/Account/PopoverActorCard.vue b/js/src/components/Account/PopoverActorCard.vue index 4f75672b8..b649075e2 100644 --- a/js/src/components/Account/PopoverActorCard.vue +++ b/js/src/components/Account/PopoverActorCard.vue @@ -1,44 +1,28 @@ - diff --git a/js/src/components/Error.vue b/js/src/components/Error.vue deleted file mode 100644 index e10a22775..000000000 --- a/js/src/components/Error.vue +++ /dev/null @@ -1,345 +0,0 @@ - - - diff --git a/js/src/components/ErrorComponent.vue b/js/src/components/ErrorComponent.vue new file mode 100644 index 000000000..7f755fd49 --- /dev/null +++ b/js/src/components/ErrorComponent.vue @@ -0,0 +1,211 @@ + + + diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue deleted file mode 100644 index 71c18fc64..000000000 --- a/js/src/components/Event/AddressAutoComplete.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - diff --git a/js/src/components/Event/DateCalendarIcon.story.vue b/js/src/components/Event/DateCalendarIcon.story.vue new file mode 100644 index 000000000..e6b55dcb3 --- /dev/null +++ b/js/src/components/Event/DateCalendarIcon.story.vue @@ -0,0 +1,14 @@ + + + diff --git a/js/src/components/Event/DateCalendarIcon.vue b/js/src/components/Event/DateCalendarIcon.vue index 59a55e3fc..ff450dc8b 100644 --- a/js/src/components/Event/DateCalendarIcon.vue +++ b/js/src/components/Event/DateCalendarIcon.vue @@ -1,71 +1,51 @@ - -### Example -```vue - -``` - -```vue - -``` - - - diff --git a/js/src/components/Event/EventCard.story.vue b/js/src/components/Event/EventCard.story.vue new file mode 100644 index 000000000..82020d4c8 --- /dev/null +++ b/js/src/components/Event/EventCard.story.vue @@ -0,0 +1,148 @@ + + + diff --git a/js/src/components/Event/EventCard.vue b/js/src/components/Event/EventCard.vue index 8ef70a2f3..bcf92c9b4 100644 --- a/js/src/components/Event/EventCard.vue +++ b/js/src/components/Event/EventCard.vue @@ -1,272 +1,219 @@ - - - diff --git a/js/src/components/Event/EventFullDate.vue b/js/src/components/Event/EventFullDate.vue index 4ca50ee96..1958f5792 100644 --- a/js/src/components/Event/EventFullDate.vue +++ b/js/src/components/Event/EventFullDate.vue @@ -1,91 +1,69 @@ - -#### Give a translated and localized text that give the starting and ending datetime for an event. - -##### Start date with no ending -```vue - -``` - -##### Start date with an ending the same day -```vue - -``` - -##### Start date with an ending on a different day -```vue - -``` - - - diff --git a/js/src/components/Event/EventListViewCard.story.vue b/js/src/components/Event/EventListViewCard.story.vue new file mode 100644 index 000000000..2ecca3b8f --- /dev/null +++ b/js/src/components/Event/EventListViewCard.story.vue @@ -0,0 +1,143 @@ + + + diff --git a/js/src/components/Event/EventListViewCard.vue b/js/src/components/Event/EventListViewCard.vue index 9a890f7fd..0924b6af3 100644 --- a/js/src/components/Event/EventListViewCard.vue +++ b/js/src/components/Event/EventListViewCard.vue @@ -1,169 +1,83 @@ - - - diff --git a/js/src/components/Event/EventMap.vue b/js/src/components/Event/EventMap.vue index adb332f4f..0a3d050b9 100644 --- a/js/src/components/Event/EventMap.vue +++ b/js/src/components/Event/EventMap.vue @@ -6,6 +6,7 @@ - diff --git a/js/src/components/Event/EventMetadataList.vue b/js/src/components/Event/EventMetadataList.vue index 313fda303..d3fb4abd0 100644 --- a/js/src/components/Event/EventMetadataList.vue +++ b/js/src/components/Event/EventMetadataList.vue @@ -3,18 +3,18 @@
- - - - diff --git a/js/src/components/Event/FullAddressAutoComplete.vue b/js/src/components/Event/FullAddressAutoComplete.vue index e42fe2d30..640e3ce64 100644 --- a/js/src/components/Event/FullAddressAutoComplete.vue +++ b/js/src/components/Event/FullAddressAutoComplete.vue @@ -1,56 +1,62 @@ - diff --git a/js/src/components/Event/ParticipationButton.story.vue b/js/src/components/Event/ParticipationButton.story.vue new file mode 100644 index 000000000..e1e16b511 --- /dev/null +++ b/js/src/components/Event/ParticipationButton.story.vue @@ -0,0 +1,114 @@ + + + diff --git a/js/src/components/Event/ParticipationButton.vue b/js/src/components/Event/ParticipationButton.vue index 48fd0b93a..198c96731 100644 --- a/js/src/components/Event/ParticipationButton.vue +++ b/js/src/components/Event/ParticipationButton.vue @@ -1,90 +1,61 @@ -import {EventJoinOptions} from "@/types/event.model"; - -A button to set your participation - -##### If the participant has been confirmed -```vue - -``` - -##### If the participant has not being approved yet -```vue - -``` - -##### If the participant has been rejected -```vue - -``` - -##### If the participant doesn't exist yet -```vue - -``` - - - - - diff --git a/js/src/components/Event/RecentEventCardWrapper.vue b/js/src/components/Event/RecentEventCardWrapper.vue index a2d2c6e1d..59b1f47aa 100644 --- a/js/src/components/Event/RecentEventCardWrapper.vue +++ b/js/src/components/Event/RecentEventCardWrapper.vue @@ -2,8 +2,8 @@

{{ - formatDistanceToNow(new Date(event.publishAt || event.insertedAt), { - locale: $dateFnsLocale, + formatDistanceToNow(new Date(event.publishAt), { + locale: dateFnsLocale, addSuffix: true, }) || $t("Right now") }} @@ -11,25 +11,15 @@

- - diff --git a/js/src/components/Event/ShareEventModal.story.vue b/js/src/components/Event/ShareEventModal.story.vue new file mode 100644 index 000000000..c153fdc5a --- /dev/null +++ b/js/src/components/Event/ShareEventModal.story.vue @@ -0,0 +1,29 @@ + + + diff --git a/js/src/components/Event/ShareEventModal.vue b/js/src/components/Event/ShareEventModal.vue index c6afd9317..bec5c4a95 100644 --- a/js/src/components/Event/ShareEventModal.vue +++ b/js/src/components/Event/ShareEventModal.vue @@ -1,220 +1,49 @@ - - diff --git a/js/src/components/Event/SkeletonEventResult.story.vue b/js/src/components/Event/SkeletonEventResult.story.vue new file mode 100644 index 000000000..079d01ba7 --- /dev/null +++ b/js/src/components/Event/SkeletonEventResult.story.vue @@ -0,0 +1,17 @@ + + + diff --git a/js/src/components/Event/SkeletonEventResult.vue b/js/src/components/Event/SkeletonEventResult.vue new file mode 100644 index 000000000..d29223e85 --- /dev/null +++ b/js/src/components/Event/SkeletonEventResult.vue @@ -0,0 +1,51 @@ + + + diff --git a/js/src/components/Event/TagInput.story.vue b/js/src/components/Event/TagInput.story.vue new file mode 100644 index 000000000..ed30e63e4 --- /dev/null +++ b/js/src/components/Event/TagInput.story.vue @@ -0,0 +1,23 @@ + + + diff --git a/js/src/components/Event/TagInput.vue b/js/src/components/Event/TagInput.vue index 9d4a6671c..dc88546e5 100644 --- a/js/src/components/Event/TagInput.vue +++ b/js/src/components/Event/TagInput.vue @@ -1,104 +1,91 @@ - diff --git a/js/src/components/Group/MultiGroupCard.vue b/js/src/components/Group/MultiGroupCard.vue index 392581c5f..4d3a7087d 100644 --- a/js/src/components/Group/MultiGroupCard.vue +++ b/js/src/components/Group/MultiGroupCard.vue @@ -8,20 +8,13 @@ /> - diff --git a/js/src/components/Home/UnloggedIntroduction.story.vue b/js/src/components/Home/UnloggedIntroduction.story.vue new file mode 100644 index 000000000..4958f4e22 --- /dev/null +++ b/js/src/components/Home/UnloggedIntroduction.story.vue @@ -0,0 +1,23 @@ + + diff --git a/js/src/components/Home/UnloggedIntroduction.vue b/js/src/components/Home/UnloggedIntroduction.vue new file mode 100644 index 000000000..bd95e33c9 --- /dev/null +++ b/js/src/components/Home/UnloggedIntroduction.vue @@ -0,0 +1,52 @@ + + diff --git a/js/src/components/Image/BlurhashImg.vue b/js/src/components/Image/BlurhashImg.vue index a4017a5fc..fd53a33ee 100644 --- a/js/src/components/Image/BlurhashImg.vue +++ b/js/src/components/Image/BlurhashImg.vue @@ -2,37 +2,33 @@ - - diff --git a/js/src/components/Image/LazyImage.vue b/js/src/components/Image/LazyImage.vue index 9bfcbf85b..0a2a40da1 100644 --- a/js/src/components/Image/LazyImage.vue +++ b/js/src/components/Image/LazyImage.vue @@ -1,22 +1,20 @@ - - diff --git a/js/src/components/Image/LazyImageWrapper.vue b/js/src/components/Image/LazyImageWrapper.vue index 038207644..b30a027ae 100644 --- a/js/src/components/Image/LazyImageWrapper.vue +++ b/js/src/components/Image/LazyImageWrapper.vue @@ -8,10 +8,9 @@ :rounded="rounded" /> - diff --git a/js/src/components/Image/test.html b/js/src/components/Image/test.html new file mode 100644 index 000000000..6d63a3297 --- /dev/null +++ b/js/src/components/Image/test.html @@ -0,0 +1,67 @@ + + + + + + + + + + + Tailwind CSS CDN + + +
+ +
+
+
+
+

+ + + + Members only +

+
+ Best Mountain Trails 2020 +
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. + Voluptatibus quia, Nonea! Maiores et perferendis eaque, + exercitationem praesentium nihil. +

+
+
+ Avatar of Writer +
+

John Smith

+

Aug 18

+
+
+
+
+
+ + diff --git a/js/src/components/LeafletMap.vue b/js/src/components/LeafletMap.vue new file mode 100644 index 000000000..0426739ab --- /dev/null +++ b/js/src/components/LeafletMap.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/js/src/components/Local/CloseContent.vue b/js/src/components/Local/CloseContent.vue new file mode 100644 index 000000000..0d614d003 --- /dev/null +++ b/js/src/components/Local/CloseContent.vue @@ -0,0 +1,112 @@ + + + diff --git a/js/src/components/Local/CloseEvents.vue b/js/src/components/Local/CloseEvents.vue new file mode 100644 index 000000000..bd6808b46 --- /dev/null +++ b/js/src/components/Local/CloseEvents.vue @@ -0,0 +1,107 @@ + + + diff --git a/js/src/components/Local/CloseGroups.vue b/js/src/components/Local/CloseGroups.vue new file mode 100644 index 000000000..f11bcbfc0 --- /dev/null +++ b/js/src/components/Local/CloseGroups.vue @@ -0,0 +1,102 @@ + + + diff --git a/js/src/components/Local/LastEvents.vue b/js/src/components/Local/LastEvents.vue new file mode 100644 index 000000000..c2b936231 --- /dev/null +++ b/js/src/components/Local/LastEvents.vue @@ -0,0 +1,78 @@ + + + diff --git a/js/src/components/Local/MoreContent.vue b/js/src/components/Local/MoreContent.vue new file mode 100644 index 000000000..734e892a5 --- /dev/null +++ b/js/src/components/Local/MoreContent.vue @@ -0,0 +1,90 @@ + + + diff --git a/js/src/components/Local/OnlineEvents.vue b/js/src/components/Local/OnlineEvents.vue new file mode 100644 index 000000000..fe3a0a56d --- /dev/null +++ b/js/src/components/Local/OnlineEvents.vue @@ -0,0 +1,77 @@ + + + diff --git a/js/src/components/Map.vue b/js/src/components/Map.vue deleted file mode 100644 index 30a561047..000000000 --- a/js/src/components/Map.vue +++ /dev/null @@ -1,175 +0,0 @@ - - - - diff --git a/js/src/components/Map/Vue2LeafletLocateControl.vue b/js/src/components/Map/Vue2LeafletLocateControl.vue deleted file mode 100644 index 5a8369cf9..000000000 --- a/js/src/components/Map/Vue2LeafletLocateControl.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - diff --git a/js/src/components/Map/VueBottomSheet.vue b/js/src/components/Map/VueBottomSheet.vue new file mode 100644 index 000000000..6a36caab9 --- /dev/null +++ b/js/src/components/Map/VueBottomSheet.vue @@ -0,0 +1,370 @@ + + + + + diff --git a/js/src/components/Logo.vue b/js/src/components/MobilizonLogo.vue similarity index 90% rename from js/src/components/Logo.vue rename to js/src/components/MobilizonLogo.vue index 92c1d47ea..c179e0e89 100644 --- a/js/src/components/Logo.vue +++ b/js/src/components/MobilizonLogo.vue @@ -1,5 +1,10 @@ - - diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue index 0d8d6ed2c..8a85f7bd3 100644 --- a/js/src/components/NavBar.vue +++ b/js/src/components/NavBar.vue @@ -1,65 +1,255 @@ - + --> - - diff --git a/js/src/components/PageFooter.vue b/js/src/components/PageFooter.vue new file mode 100644 index 000000000..2ae331cc6 --- /dev/null +++ b/js/src/components/PageFooter.vue @@ -0,0 +1,115 @@ + + diff --git a/js/src/components/Participation/ConfirmParticipation.vue b/js/src/components/Participation/ConfirmParticipation.vue index b27b49701..935daad0a 100644 --- a/js/src/components/Participation/ConfirmParticipation.vue +++ b/js/src/components/Participation/ConfirmParticipation.vue @@ -1,60 +1,58 @@ - diff --git a/js/src/components/Participation/ParticipationSection.vue b/js/src/components/Participation/ParticipationSection.vue index 96badea38..a0ef1d248 100644 --- a/js/src/components/Participation/ParticipationSection.vue +++ b/js/src/components/Participation/ParticipationSection.vue @@ -1,14 +1,12 @@ - diff --git a/js/src/components/Participation/ParticipationWithAccount.vue b/js/src/components/Participation/ParticipationWithAccount.vue index 9d9e5a4f6..df8b97fb4 100644 --- a/js/src/components/Participation/ParticipationWithAccount.vue +++ b/js/src/components/Participation/ParticipationWithAccount.vue @@ -6,46 +6,31 @@ :sentence="sentence" /> - diff --git a/js/src/components/Participation/ParticipationWithoutAccount.vue b/js/src/components/Participation/ParticipationWithoutAccount.vue index bf18cc5d0..8b9a24661 100644 --- a/js/src/components/Participation/ParticipationWithoutAccount.vue +++ b/js/src/components/Participation/ParticipationWithoutAccount.vue @@ -1,118 +1,128 @@ - - diff --git a/js/src/components/Participation/UnloggedParticipation.vue b/js/src/components/Participation/UnloggedParticipation.vue index 1457f459a..be78ce52b 100644 --- a/js/src/components/Participation/UnloggedParticipation.vue +++ b/js/src/components/Participation/UnloggedParticipation.vue @@ -1,10 +1,10 @@ - - diff --git a/js/src/components/Post/MultiPostListItem.vue b/js/src/components/Post/MultiPostListItem.vue index 53baa91e1..3812e0f88 100644 --- a/js/src/components/Post/MultiPostListItem.vue +++ b/js/src/components/Post/MultiPostListItem.vue @@ -8,22 +8,17 @@ /> - diff --git a/js/src/components/Post/SharePostModal.story.vue b/js/src/components/Post/SharePostModal.story.vue new file mode 100644 index 000000000..9531800ea --- /dev/null +++ b/js/src/components/Post/SharePostModal.story.vue @@ -0,0 +1,20 @@ + + + diff --git a/js/src/components/Post/SharePostModal.vue b/js/src/components/Post/SharePostModal.vue index d75919e46..d784646d6 100644 --- a/js/src/components/Post/SharePostModal.vue +++ b/js/src/components/Post/SharePostModal.vue @@ -1,218 +1,56 @@ - diff --git a/js/src/components/Resource/FolderItem.vue b/js/src/components/Resource/FolderItem.vue index b06aeabca..d8a567821 100644 --- a/js/src/components/Resource/FolderItem.vue +++ b/js/src/components/Resource/FolderItem.vue @@ -4,24 +4,25 @@ :to="{ name: RouteName.RESOURCE_FOLDER, params: { - path: ResourceMixin.resourcePathArray(resource), + path: resourcePathArray(resource), preferredUsername: usernameWithDomain(group), }, }" > -
- +
+

{{ resource.title }}

- {{ - resource.updatedAt | formatDateTimeString + {{ + formatDateTimeString(resource.updatedAt?.toString()) }}
- diff --git a/js/src/components/Search/filters/FilterSection.vue b/js/src/components/Search/filters/FilterSection.vue new file mode 100644 index 000000000..c1643741e --- /dev/null +++ b/js/src/components/Search/filters/FilterSection.vue @@ -0,0 +1,70 @@ + + diff --git a/js/src/components/SearchField.vue b/js/src/components/SearchField.vue index b6376f748..ab9ca7824 100644 --- a/js/src/components/SearchField.vue +++ b/js/src/components/SearchField.vue @@ -1,46 +1,50 @@ - diff --git a/js/src/components/Settings/SettingMenuSection.vue b/js/src/components/Settings/SettingMenuSection.vue index 264eb19b1..173839567 100644 --- a/js/src/components/Settings/SettingMenuSection.vue +++ b/js/src/components/Settings/SettingMenuSection.vue @@ -1,61 +1,38 @@ - - - diff --git a/js/src/components/Settings/SettingsMenu.vue b/js/src/components/Settings/SettingsMenu.vue index 3be7a96ad..90e3ecbb9 100644 --- a/js/src/components/Settings/SettingsMenu.vue +++ b/js/src/components/Settings/SettingsMenu.vue @@ -1,25 +1,25 @@ - diff --git a/js/src/components/Settings/SettingsOnboarding.vue b/js/src/components/Settings/SettingsOnboarding.vue index 4eaa5614f..fa936c5ad 100644 --- a/js/src/components/Settings/SettingsOnboarding.vue +++ b/js/src/components/Settings/SettingsOnboarding.vue @@ -2,106 +2,89 @@
-

{{ $t("Settings") }}

+

{{ t("Settings") }}

-

{{ $t("Language") }}

+

{{ t("Language") }}

{{ - $t( + t( "This setting will be used to display the website and send you emails in the correct language." ) }}

- - +
-

{{ $t("Timezone") }}

+

{{ t("Timezone") }}

{{ - $t( + t( "We use your timezone to make sure you get notifications for an event at the correct time." ) }} {{ - $t("Your timezone was detected as {timezone}.", { + t("Your timezone was detected as {timezone}.", { timezone, }) }} - - {{ $t("Your timezone {timezone} isn't supported.", { timezone }) }} - + {{ t("Your timezone {timezone} isn't supported.", { timezone }) }} +

- diff --git a/js/src/components/Share/TelegramLogo.vue b/js/src/components/Share/TelegramLogo.vue index feb255786..aed851fe3 100644 --- a/js/src/components/Share/TelegramLogo.vue +++ b/js/src/components/Share/TelegramLogo.vue @@ -1,5 +1,5 @@ - diff --git a/js/src/components/Tag.vue b/js/src/components/Tag.vue deleted file mode 100644 index 2fcf28699..000000000 --- a/js/src/components/Tag.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - diff --git a/js/src/components/TagElement.vue b/js/src/components/TagElement.vue new file mode 100644 index 000000000..cde862ae6 --- /dev/null +++ b/js/src/components/TagElement.vue @@ -0,0 +1,44 @@ + + + + diff --git a/js/src/components/TextEditor.vue b/js/src/components/TextEditor.vue new file mode 100644 index 000000000..86a21dfc7 --- /dev/null +++ b/js/src/components/TextEditor.vue @@ -0,0 +1,596 @@ + + + + diff --git a/js/src/components/Todo/CompactTodo.vue b/js/src/components/Todo/CompactTodo.vue index 4d745c938..0eccab10f 100644 --- a/js/src/components/Todo/CompactTodo.vue +++ b/js/src/components/Todo/CompactTodo.vue @@ -1,18 +1,19 @@ - - diff --git a/js/src/components/Todo/FullTodo.vue b/js/src/components/Todo/FullTodo.vue index 7719b3cde..ca8295c76 100644 --- a/js/src/components/Todo/FullTodo.vue +++ b/js/src/components/Todo/FullTodo.vue @@ -1,101 +1,99 @@ - diff --git a/js/src/components/User/AuthProvider.vue b/js/src/components/User/AuthProvider.vue index f3817ef5e..bd586e315 100644 --- a/js/src/components/User/AuthProvider.vue +++ b/js/src/components/User/AuthProvider.vue @@ -1,34 +1,33 @@ - diff --git a/js/src/components/User/AuthProviders.story.vue b/js/src/components/User/AuthProviders.story.vue new file mode 100644 index 000000000..ee88b54a1 --- /dev/null +++ b/js/src/components/User/AuthProviders.story.vue @@ -0,0 +1,13 @@ + + + diff --git a/js/src/components/User/AuthProviders.vue b/js/src/components/User/AuthProviders.vue index cd04d09d8..2e894205f 100644 --- a/js/src/components/User/AuthProviders.vue +++ b/js/src/components/User/AuthProviders.vue @@ -1,7 +1,7 @@ - diff --git a/js/src/components/Utils/EmptyContent.vue b/js/src/components/Utils/EmptyContent.vue index e1b685fd9..9b937cb63 100644 --- a/js/src/components/Utils/EmptyContent.vue +++ b/js/src/components/Utils/EmptyContent.vue @@ -1,45 +1,32 @@ - - - diff --git a/js/src/components/Utils/HomepageRedirectComponent.vue b/js/src/components/Utils/HomepageRedirectComponent.vue index 909b9f6f0..86db7b0ee 100644 --- a/js/src/components/Utils/HomepageRedirectComponent.vue +++ b/js/src/components/Utils/HomepageRedirectComponent.vue @@ -2,14 +2,11 @@
a
- diff --git a/js/src/components/Utils/Breadcrumbs.vue b/js/src/components/Utils/NavBreadcrumbs.vue similarity index 71% rename from js/src/components/Utils/Breadcrumbs.vue rename to js/src/components/Utils/NavBreadcrumbs.vue index 8122314a3..616edc040 100644 --- a/js/src/components/Utils/Breadcrumbs.vue +++ b/js/src/components/Utils/NavBreadcrumbs.vue @@ -1,5 +1,5 @@ - diff --git a/js/src/components/Utils/Observer.vue b/js/src/components/Utils/Observer.vue deleted file mode 100644 index 49f24678d..000000000 --- a/js/src/components/Utils/Observer.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/js/src/components/Utils/ObserverElement.vue b/js/src/components/Utils/ObserverElement.vue new file mode 100644 index 000000000..7bf41e147 --- /dev/null +++ b/js/src/components/Utils/ObserverElement.vue @@ -0,0 +1,35 @@ + + + diff --git a/js/src/components/Utils/RedirectWithAccount.vue b/js/src/components/Utils/RedirectWithAccount.vue index 4cb6fedca..755744db7 100644 --- a/js/src/components/Utils/RedirectWithAccount.vue +++ b/js/src/components/Utils/RedirectWithAccount.vue @@ -1,12 +1,12 @@ - diff --git a/js/src/components/Utils/Subtitle.vue b/js/src/components/Utils/Subtitle.vue deleted file mode 100644 index acf311ad1..000000000 --- a/js/src/components/Utils/Subtitle.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/js/src/components/Utils/VerticalDivider.vue b/js/src/components/Utils/VerticalDivider.vue index 5e1dbe2e9..c73b9f729 100644 --- a/js/src/components/Utils/VerticalDivider.vue +++ b/js/src/components/Utils/VerticalDivider.vue @@ -1,20 +1,14 @@ - - diff --git a/js/src/components/core/CustomDialog.vue b/js/src/components/core/CustomDialog.vue new file mode 100644 index 000000000..7c5e87e9f --- /dev/null +++ b/js/src/components/core/CustomDialog.vue @@ -0,0 +1,143 @@ + + + diff --git a/js/src/components/core/CustomSnackbar.vue b/js/src/components/core/CustomSnackbar.vue new file mode 100644 index 000000000..f0193db25 --- /dev/null +++ b/js/src/components/core/CustomSnackbar.vue @@ -0,0 +1,104 @@ + + diff --git a/js/src/components/core/LinkOrRouterLink.vue b/js/src/components/core/LinkOrRouterLink.vue new file mode 100644 index 000000000..b624fa68e --- /dev/null +++ b/js/src/components/core/LinkOrRouterLink.vue @@ -0,0 +1,39 @@ + + + diff --git a/js/src/components/core/MaterialIcon.story.vue b/js/src/components/core/MaterialIcon.story.vue new file mode 100644 index 000000000..1b6a20a58 --- /dev/null +++ b/js/src/components/core/MaterialIcon.story.vue @@ -0,0 +1,16 @@ + + diff --git a/js/src/components/core/MaterialIcon.vue b/js/src/components/core/MaterialIcon.vue new file mode 100644 index 000000000..491e45fde --- /dev/null +++ b/js/src/components/core/MaterialIcon.vue @@ -0,0 +1,263 @@ + + diff --git a/js/src/composition/activity.ts b/js/src/composition/activity.ts new file mode 100644 index 000000000..a52951049 --- /dev/null +++ b/js/src/composition/activity.ts @@ -0,0 +1,32 @@ +import { IActivity } from "@/types/activity.model"; +import { IMember } from "@/types/actor/member.model"; +import { useCurrentActorClient } from "./apollo/actor"; + +export function useIsActivityAuthorCurrentActor() { + const { currentActor } = useCurrentActorClient(); + + return (activity: IActivity): boolean => { + return ( + activity.author.id === currentActor.value?.id && + currentActor.value?.id !== undefined + ); + }; +} + +export function useIsActivityObjectCurrentActor() { + const { currentActor } = useCurrentActorClient(); + return (activity: IActivity): boolean => + (activity?.object as IMember)?.actor?.id === currentActor.value?.id && + currentActor.value?.id !== undefined; +} + +export function useActivitySubjectParams() { + return (activity: IActivity) => + activity.subjectParams.reduce( + (acc: Record, { key, value }) => { + acc[key] = value; + return acc; + }, + {} + ); +} diff --git a/js/src/composition/apollo/actor.ts b/js/src/composition/apollo/actor.ts new file mode 100644 index 000000000..ca2463452 --- /dev/null +++ b/js/src/composition/apollo/actor.ts @@ -0,0 +1,70 @@ +import { + CURRENT_ACTOR_CLIENT, + GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED, + IDENTITIES, + PERSON_STATUS_GROUP, +} from "@/graphql/actor"; +import { IPerson } from "@/types/actor"; +import { useQuery } from "@vue/apollo-composable"; +import { computed, Ref, unref } from "vue"; +import { useCurrentUserClient } from "./user"; + +export function useCurrentActorClient() { + const { + result: currentActorResult, + error, + loading, + } = useQuery<{ currentActor: IPerson }>(CURRENT_ACTOR_CLIENT); + const currentActor = computed( + () => currentActorResult.value?.currentActor + ); + return { currentActor, error, loading }; +} + +export function useCurrentUserIdentities() { + const { currentUser } = useCurrentUserClient(); + + const { result, error, loading } = useQuery<{ identities: IPerson[] }>( + IDENTITIES, + {}, + () => ({ + enabled: + currentUser.value?.id !== undefined && + currentUser.value?.id !== null && + currentUser.value?.isLoggedIn === true, + }) + ); + + const identities = computed(() => result.value?.identities); + return { identities, error, loading }; +} + +export function usePersonStatusGroup( + groupFederatedUsername: string | undefined | Ref +) { + const { currentActor } = useCurrentActorClient(); + const { result, error, loading, subscribeToMore } = useQuery<{ + person: IPerson; + }>( + PERSON_STATUS_GROUP, + () => ({ + id: currentActor.value?.id, + group: unref(groupFederatedUsername), + }), + () => ({ + enabled: + currentActor.value?.id !== undefined && + unref(groupFederatedUsername) !== undefined && + unref(groupFederatedUsername) !== "", + }) + ); + subscribeToMore(() => ({ + document: GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED, + variables: { + actorId: currentActor.value?.id, + group: unref(groupFederatedUsername), + }, + })); + const person = computed(() => result.value?.person); + return { person, error, loading }; +} diff --git a/js/src/composition/apollo/address.ts b/js/src/composition/apollo/address.ts new file mode 100644 index 000000000..407031a89 --- /dev/null +++ b/js/src/composition/apollo/address.ts @@ -0,0 +1,16 @@ +import { REVERSE_GEOCODE } from "@/graphql/address"; +import { useLazyQuery } from "@vue/apollo-composable"; +import { IAddress } from "@/types/address.model"; + +type reverseGeoCodeType = { + latitude: number; + longitude: number; + zoom: number; + locale: string; +}; + +export function useReverseGeocode() { + return useLazyQuery<{ reverseGeocode: IAddress[] }, reverseGeoCodeType>( + REVERSE_GEOCODE + ); +} diff --git a/js/src/composition/apollo/config.ts b/js/src/composition/apollo/config.ts new file mode 100644 index 000000000..0f933bd5f --- /dev/null +++ b/js/src/composition/apollo/config.ts @@ -0,0 +1,206 @@ +import { + ABOUT, + ANALYTICS, + ANONYMOUS_ACTOR_ID, + ANONYMOUS_PARTICIPATION_CONFIG, + ANONYMOUS_REPORTS_CONFIG, + DEMO_MODE, + EVENT_CATEGORIES, + EVENT_PARTICIPANTS, + FEATURES, + GEOCODING_AUTOCOMPLETE, + LOCATION, + MAPS_TILES, + RESOURCE_PROVIDERS, + RESTRICTIONS, + ROUTING_TYPE, + SEARCH_CONFIG, + TIMEZONES, + UPLOAD_LIMITS, +} from "@/graphql/config"; +import { IConfig } from "@/types/config.model"; +import { useQuery } from "@vue/apollo-composable"; +import { computed } from "vue"; + +export function useTimezones() { + const { + result: timezoneResult, + error, + loading, + } = useQuery<{ + config: Pick; + }>(TIMEZONES); + + const timezones = computed(() => timezoneResult.value?.config?.timezones); + return { timezones, error, loading }; +} + +export function useAnonymousParticipationConfig() { + const { + result: configResult, + error, + loading, + } = useQuery<{ + config: Pick; + }>(ANONYMOUS_PARTICIPATION_CONFIG); + + const anonymousParticipationConfig = computed( + () => configResult.value?.config?.anonymous?.participation + ); + + return { anonymousParticipationConfig, error, loading }; +} + +export function useAnonymousReportsConfig() { + const { + result: configResult, + error, + loading, + } = useQuery<{ + config: Pick; + }>(ANONYMOUS_REPORTS_CONFIG); + + const anonymousReportsConfig = computed( + () => configResult.value?.config?.anonymous?.participation + ); + return { anonymousReportsConfig, error, loading }; +} + +export function useInstanceName() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(ABOUT); + + const instanceName = computed(() => result.value?.config?.name); + return { instanceName, error, loading }; +} + +export function useAnonymousActorId() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(ANONYMOUS_ACTOR_ID); + + const anonymousActorId = computed( + () => result.value?.config?.anonymous?.actorId + ); + return { anonymousActorId, error, loading }; +} + +export function useUploadLimits() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(UPLOAD_LIMITS); + + const uploadLimits = computed(() => result.value?.config?.uploadLimits); + return { uploadLimits, error, loading }; +} + +export function useEventCategories() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(EVENT_CATEGORIES); + + const eventCategories = computed(() => result.value?.config.eventCategories); + return { eventCategories, error, loading }; +} + +export function useRestrictions() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(RESTRICTIONS); + + const restrictions = computed(() => result.value?.config.restrictions); + return { restrictions, error, loading }; +} + +export function useExportFormats() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(EVENT_PARTICIPANTS); + const exportFormats = computed(() => result.value?.config?.exportFormats); + return { exportFormats, error, loading }; +} + +export function useGeocodingAutocomplete() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(GEOCODING_AUTOCOMPLETE); + const geocodingAutocomplete = computed( + () => result.value?.config?.geocoding?.autocomplete + ); + return { geocodingAutocomplete, error, loading }; +} + +export function useMapTiles() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(MAPS_TILES); + + const tiles = computed(() => result.value?.config.maps.tiles); + return { tiles, error, loading }; +} + +export function useRoutingType() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(ROUTING_TYPE); + + const routingType = computed(() => result.value?.config.maps.routing.type); + return { routingType, error, loading }; +} + +export function useFeatures() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(FEATURES); + + const features = computed(() => result.value?.config.features); + return { features, error, loading }; +} + +export function useResourceProviders() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(RESOURCE_PROVIDERS); + + const resourceProviders = computed( + () => result.value?.config.resourceProviders + ); + return { resourceProviders, error, loading }; +} + +export function useServerProvidedLocation() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(LOCATION); + + const location = computed(() => result.value?.config.location); + return { location, error, loading }; +} + +export function useIsDemoMode() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(DEMO_MODE); + + const isDemoMode = computed(() => result.value?.config.demoMode); + return { isDemoMode, error, loading }; +} + +export function useAnalytics() { + const { result, error, loading } = useQuery<{ + config: Pick; + }>(ANALYTICS); + + const analytics = computed(() => result.value?.config.analytics); + return { analytics, error, loading }; +} + +export function useSearchConfig() { + const { result, error, loading, onResult } = useQuery<{ + config: Pick; + }>(SEARCH_CONFIG); + + const searchConfig = computed(() => result.value?.config.search); + return { searchConfig, error, loading, onResult }; +} diff --git a/js/src/composition/apollo/event.ts b/js/src/composition/apollo/event.ts new file mode 100644 index 000000000..cbd156271 --- /dev/null +++ b/js/src/composition/apollo/event.ts @@ -0,0 +1,51 @@ +import { DELETE_EVENT, FETCH_EVENT, FETCH_EVENT_BASIC } from "@/graphql/event"; +import { IEvent } from "@/types/event.model"; +import { useMutation, useQuery } from "@vue/apollo-composable"; +import { computed } from "vue"; + +export function useFetchEvent(uuid?: string) { + const { + result: fetchEventResult, + loading, + error, + onError, + onResult, + } = useQuery<{ event: IEvent }>( + FETCH_EVENT, + { + uuid, + }, + () => ({ + enabled: uuid !== undefined, + }) + ); + + const event = computed(() => fetchEventResult.value?.event); + + return { event, loading, error, onError, onResult }; +} + +export function useFetchEventBasic(uuid: string) { + const { + result: fetchEventResult, + loading, + error, + onResult, + onError, + } = useQuery<{ event: IEvent }>(FETCH_EVENT_BASIC, { + uuid, + }); + + const event = computed(() => fetchEventResult.value?.event); + + return { event, loading, error, onResult, onError }; +} + +export function useDeleteEvent() { + return useMutation<{ id: string }, { eventId: string }>(DELETE_EVENT, () => ({ + update(cache, { data }) { + cache.evict({ id: `Event:${data?.id}` }); + cache.gc(); + }, + })); +} diff --git a/js/src/composition/apollo/group.ts b/js/src/composition/apollo/group.ts new file mode 100644 index 000000000..63c238ebe --- /dev/null +++ b/js/src/composition/apollo/group.ts @@ -0,0 +1,123 @@ +import { PERSON_MEMBERSHIPS } from "@/graphql/actor"; +import { + CREATE_GROUP, + DELETE_GROUP, + FETCH_GROUP, + LEAVE_GROUP, + UPDATE_GROUP, +} from "@/graphql/group"; +import { IGroup, IPerson } from "@/types/actor"; +import { IAddress } from "@/types/address.model"; +import { GroupVisibility, MemberRole, Openness } from "@/types/enums"; +import { IMediaUploadWrapper } from "@/types/media.model"; +import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core"; +import { useMutation, useQuery } from "@vue/apollo-composable"; +import { computed, Ref, unref } from "vue"; +import { useCurrentActorClient } from "./actor"; + +type useGroupOptions = { + beforeDateTime?: string | Date; + afterDateTime?: string | Date; + organisedEventsPage?: number; + organisedEventsLimit?: number; + postsPage?: number; + postsLimit?: number; + membersPage?: number; + membersLimit?: number; + discussionsPage?: number; + discussionsLimit?: number; +}; + +export function useGroup( + name: string | undefined | Ref, + options: useGroupOptions = {} +) { + const { result, error, loading, onResult, onError, refetch } = useQuery< + { + group: IGroup; + }, + { + name: string; + beforeDateTime?: string | Date; + afterDateTime?: string | Date; + organisedEventsPage?: number; + organisedEventsLimit?: number; + postsPage?: number; + postsLimit?: number; + membersPage?: number; + membersLimit?: number; + discussionsPage?: number; + discussionsLimit?: number; + } + >( + FETCH_GROUP, + () => ({ + name: unref(name), + ...options, + }), + () => ({ enabled: unref(name) !== undefined && unref(name) !== "" }) + ); + const group = computed(() => result.value?.group); + return { group, error, loading, onResult, onError, refetch }; +} + +export function useCreateGroup() { + const { currentActor } = useCurrentActorClient(); + + return useMutation< + { createGroup: IGroup }, + { + preferredUsername: string; + name: string; + summary?: string; + avatar?: IMediaUploadWrapper; + banner?: IMediaUploadWrapper; + } + >(CREATE_GROUP, () => ({ + update: (store: ApolloCache, { data }: FetchResult) => { + const query = { + query: PERSON_MEMBERSHIPS, + variables: { + id: currentActor.value?.id, + }, + }; + const membershipData = store.readQuery<{ person: IPerson }>(query); + if (!membershipData) return; + if (!currentActor.value) return; + const { person } = membershipData; + person.memberships?.elements.push({ + parent: data?.createGroup, + role: MemberRole.ADMINISTRATOR, + actor: currentActor.value, + insertedAt: new Date().toString(), + updatedAt: new Date().toString(), + }); + store.writeQuery({ ...query, data: { person } }); + }, + })); +} + +export function useUpdateGroup() { + return useMutation< + { updateGroup: IGroup }, + { + id: string; + name?: string; + summary?: string; + openness?: Openness; + visibility?: GroupVisibility; + physicalAddress?: IAddress; + manuallyApprovesFollowers?: boolean; + } + >(UPDATE_GROUP); +} + +export function useDeleteGroup(variables: { groupId: string }) { + return useMutation<{ deleteGroup: IGroup }>(DELETE_GROUP, () => ({ + variables, + })); +} + +export function useLeaveGroup() { + return useMutation<{ leaveGroup: { id: string } }>(LEAVE_GROUP); +} diff --git a/js/src/composition/apollo/report.ts b/js/src/composition/apollo/report.ts new file mode 100644 index 000000000..37ecaece6 --- /dev/null +++ b/js/src/composition/apollo/report.ts @@ -0,0 +1,15 @@ +import { CREATE_REPORT } from "@/graphql/report"; +import { useMutation } from "@vue/apollo-composable"; + +export function useCreateReport() { + return useMutation< + { createReport: { id: string } }, + { + eventId?: string; + reportedId: string; + content?: string; + commentsIds?: string[]; + forward?: boolean; + } + >(CREATE_REPORT); +} diff --git a/js/src/composition/apollo/tags.ts b/js/src/composition/apollo/tags.ts new file mode 100644 index 000000000..252cd1013 --- /dev/null +++ b/js/src/composition/apollo/tags.ts @@ -0,0 +1,18 @@ +import { FILTER_TAGS } from "@/graphql/tags"; +import { ITag } from "@/types/tag.model"; +import { apolloClient } from "@/vue-apollo"; +import { provideApolloClient, useQuery } from "@vue/apollo-composable"; + +export function fetchTags(text: string): Promise { + return new Promise((resolve, reject) => { + const { onResult, onError } = provideApolloClient(apolloClient)(() => + useQuery<{ tags: ITag[] }, { filter: string }>(FILTER_TAGS, { + filter: text, + }) + ); + + onResult(({ data }) => resolve(data.tags)); + + onError((error) => reject(error)); + }); +} diff --git a/js/src/composition/apollo/user.ts b/js/src/composition/apollo/user.ts new file mode 100644 index 000000000..daa130a23 --- /dev/null +++ b/js/src/composition/apollo/user.ts @@ -0,0 +1,114 @@ +import { IDENTITIES, REGISTER_PERSON } from "@/graphql/actor"; +import { + CURRENT_USER_CLIENT, + LOGGED_USER, + SET_USER_SETTINGS, + UPDATE_USER_LOCALE, + USER_SETTINGS, +} from "@/graphql/user"; +import { IPerson } from "@/types/actor"; +import { ICurrentUser, IUser } from "@/types/current-user.model"; +import { ActorType } from "@/types/enums"; +import { ApolloCache, FetchResult } from "@apollo/client/core"; +import { useMutation, useQuery } from "@vue/apollo-composable"; +import { computed } from "vue"; + +export function useCurrentUserClient() { + const { + result: currentUserResult, + error, + loading, + } = useQuery<{ + currentUser: ICurrentUser; + }>(CURRENT_USER_CLIENT); + + const currentUser = computed(() => currentUserResult.value?.currentUser); + return { currentUser, error, loading }; +} + +export function useLoggedUser() { + const { currentUser } = useCurrentUserClient(); + + const { result, error, onError } = useQuery<{ loggedUser: IUser }>( + LOGGED_USER, + {}, + () => ({ enabled: currentUser.value?.id != null }) + ); + + const loggedUser = computed(() => result.value?.loggedUser); + return { loggedUser, error, onError }; +} + +export function useUserSettings() { + const { + result: userSettingsResult, + error, + loading, + } = useQuery<{ loggedUser: IUser }>(USER_SETTINGS); + + const loggedUser = computed(() => userSettingsResult.value?.loggedUser); + return { loggedUser, error, loading }; +} + +export async function doUpdateSetting( + variables: Record +): Promise { + useMutation<{ setUserSettings: string }>(SET_USER_SETTINGS, () => ({ + variables, + })); +} + +export async function updateLocale(locale: string) { + useMutation<{ id: string; locale: string }>(UPDATE_USER_LOCALE, () => ({ + variables: { + locale, + }, + })); +} + +export function registerAccount( + variables: { + preferredUsername: string; + name: string; + summary: string; + email: string; + }, + userAlreadyActivated: boolean +) { + return useMutation< + { registerPerson: IPerson }, + { + preferredUsername: string; + name: string; + summary: string; + email: string; + } + >(REGISTER_PERSON, () => ({ + variables, + update: ( + store: ApolloCache<{ registerPerson: IPerson }>, + { data: localData }: FetchResult + ) => { + if (userAlreadyActivated) { + const identitiesData = store.readQuery<{ identities: IPerson[] }>({ + query: IDENTITIES, + }); + + if (identitiesData && localData) { + const newPersonData = { + ...localData.registerPerson, + type: ActorType.PERSON, + }; + + store.writeQuery({ + query: IDENTITIES, + data: { + ...identitiesData, + identities: [...identitiesData.identities, newPersonData], + }, + }); + } + } + }, + })); +} diff --git a/js/src/composition/config.ts b/js/src/composition/config.ts new file mode 100644 index 000000000..2c5899a18 --- /dev/null +++ b/js/src/composition/config.ts @@ -0,0 +1,22 @@ +import { useExportFormats, useUploadLimits } from "./apollo/config"; + +export const useHost = (): string => { + return window.location.hostname; +}; + +export const useAvatarMaxSize = (): number | undefined => { + const { uploadLimits } = useUploadLimits(); + + return uploadLimits.value?.avatar; +}; + +export const useBannerMaxSize = (): number | undefined => { + const { uploadLimits } = useUploadLimits(); + + return uploadLimits.value?.banner; +}; + +export const useParticipantsExportFormats = () => { + const { exportFormats } = useExportFormats(); + return exportFormats.value?.eventParticipants; +}; diff --git a/js/src/composition/group.ts b/js/src/composition/group.ts new file mode 100644 index 000000000..e69de29bb diff --git a/js/src/filters/datetime.ts b/js/src/filters/datetime.ts index bdc14e7d3..c58799b69 100644 --- a/js/src/filters/datetime.ts +++ b/js/src/filters/datetime.ts @@ -1,4 +1,3 @@ -import { DateTimeFormatOptions } from "vue-i18n"; import { i18n } from "../utils/i18n"; function parseDateTime(value: string): Date { @@ -14,7 +13,7 @@ function formatDateString(value: string): string { }); } -function formatTimeString(value: string, timeZone: string): string { +function formatTimeString(value: string, timeZone?: string): string { return parseDateTime(value).toLocaleTimeString(locale(), { hour: "numeric", minute: "numeric", @@ -24,7 +23,7 @@ function formatTimeString(value: string, timeZone: string): string { // TODO: These can be removed in favor of dateStyle/timeStyle when those two have sufficient support // https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_datestyle -const LONG_DATE_FORMAT_OPTIONS: DateTimeFormatOptions = { +const LONG_DATE_FORMAT_OPTIONS: any = { weekday: undefined, year: "numeric", month: "long", @@ -33,13 +32,13 @@ const LONG_DATE_FORMAT_OPTIONS: DateTimeFormatOptions = { minute: undefined, }; -const LONG_TIME_FORMAT_OPTIONS: DateTimeFormatOptions = { +const LONG_TIME_FORMAT_OPTIONS: any = { weekday: "long", hour: "numeric", minute: "numeric", }; -const SHORT_DATE_FORMAT_OPTIONS: DateTimeFormatOptions = { +const SHORT_DATE_FORMAT_OPTIONS: any = { weekday: undefined, year: "numeric", month: "short", @@ -48,7 +47,7 @@ const SHORT_DATE_FORMAT_OPTIONS: DateTimeFormatOptions = { minute: undefined, }; -const SHORT_TIME_FORMAT_OPTIONS: DateTimeFormatOptions = { +const SHORT_TIME_FORMAT_OPTIONS: any = { weekday: "short", hour: "numeric", minute: "numeric", @@ -75,6 +74,6 @@ function formatDateTimeString( return format.format(parseDateTime(value)); } -const locale = () => i18n.locale.replace("_", "-"); +const locale = () => i18n.global.locale.replace("_", "-"); export { formatDateString, formatTimeString, formatDateTimeString }; diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 89850cd31..de899ef21 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -125,6 +125,15 @@ export const PERSON_FRAGMENT = gql` } `; +export const PERSON_FRAGMENT_FEED_TOKENS = gql` + fragment PersonFeedTokensFragment on Person { + id + feedTokens { + token + } + } +`; + export const LIST_PROFILES = gql` query ListProfiles( $preferredUsername: String @@ -177,10 +186,10 @@ export const CURRENT_ACTOR_CLIENT = gql` export const UPDATE_CURRENT_ACTOR_CLIENT = gql` mutation UpdateCurrentActor( - $id: String! + $id: String $avatar: String - $preferredUsername: String! - $name: String! + $preferredUsername: String + $name: String ) { updateCurrentActor( id: $id @@ -342,7 +351,7 @@ export const PERSON_STATUS_GROUP = gql` `; export const PERSON_GROUP_MEMBERSHIPS = gql` - query PersonGroupMemberships($id: ID!, $groupId: ID!) { + query PersonGroupMemberships($id: ID!, $groupId: ID) { person(id: $id) { id memberships(groupId: $groupId) { diff --git a/js/src/graphql/address.ts b/js/src/graphql/address.ts index 163621e1d..7b301afdb 100644 --- a/js/src/graphql/address.ts +++ b/js/src/graphql/address.ts @@ -14,11 +14,26 @@ export const ADDRESS_FRAGMENT = gql` url originId timezone + pictureInfo { + url + author { + name + url + } + source { + name + url + } + } } `; export const ADDRESS = gql` - query ($query: String!, $locale: String, $type: AddressSearchType) { + query SearchAddress( + $query: String! + $locale: String + $type: AddressSearchType + ) { searchAddress(query: $query, locale: $locale, type: $type) { ...AdressFragment } @@ -27,7 +42,12 @@ export const ADDRESS = gql` `; export const REVERSE_GEOCODE = gql` - query ($latitude: Float!, $longitude: Float!, $zoom: Int, $locale: String) { + query ReverseGeocode( + $latitude: Float! + $longitude: Float! + $zoom: Int + $locale: String + ) { reverseGeocode( latitude: $latitude longitude: $longitude diff --git a/js/src/graphql/config.ts b/js/src/graphql/config.ts index 490346061..c2dcd10cf 100644 --- a/js/src/graphql/config.ts +++ b/js/src/graphql/config.ts @@ -6,6 +6,7 @@ export const CONFIG = gql` name description slogan + version registrationsOpen registrationsAllowlist demoMode @@ -104,6 +105,12 @@ export const CONFIG = gql` type } } + search { + global { + isEnabled + isDefault + } + } } } `; @@ -155,6 +162,7 @@ export const ABOUT = gql` name description longDescription + slogan contact languages registrationsOpen @@ -230,3 +238,209 @@ export const EVENT_PARTICIPANTS = gql` } } `; + +export const ANONYMOUS_PARTICIPATION_CONFIG = gql` + query AnonymousParticipationConfig { + config { + anonymous { + participation { + allowed + validation { + email { + enabled + confirmationRequired + } + captcha { + enabled + } + } + } + } + } + } +`; + +export const ANONYMOUS_REPORTS_CONFIG = gql` + query AnonymousParticipationConfig { + config { + anonymous { + reports { + allowed + } + } + } + } +`; + +export const INSTANCE_NAME = gql` + query InstanceName { + config { + name + } + } +`; + +export const ANONYMOUS_ACTOR_ID = gql` + query AnonymousActorId { + config { + anonymous { + actorId + } + } + } +`; + +export const UPLOAD_LIMITS = gql` + query UploadLimits { + config { + uploadLimits { + default + avatar + banner + } + } + } +`; + +export const EVENT_CATEGORIES = gql` + query EventCategories { + config { + eventCategories { + id + label + } + } + } +`; + +export const RESTRICTIONS = gql` + query OnlyGroupsCanCreateEvents { + config { + restrictions { + onlyGroupsCanCreateEvents + onlyAdminCanCreateGroups + } + } + } +`; + +export const GEOCODING_AUTOCOMPLETE = gql` + query GeoCodingAutocomplete { + config { + geocoding { + autocomplete + } + } + } +`; + +export const MAPS_TILES = gql` + query MapsTiles { + config { + maps { + tiles { + endpoint + attribution + } + } + } + } +`; + +export const ROUTING_TYPE = gql` + query RoutingType { + config { + maps { + routing { + type + } + } + } + } +`; + +export const FEATURES = gql` + query Features { + config { + features { + groups + eventCreation + } + } + } +`; + +export const RESOURCE_PROVIDERS = gql` + query ResourceProviders { + config { + resourceProviders { + type + endpoint + software + } + } + } +`; + +export const LOGIN_CONFIG = gql` + query LoginConfig { + config { + auth { + oauthProviders { + id + label + } + } + registrationsOpen + } + } +`; + +export const LOCATION = gql` + query Location { + config { + location { + latitude + longitude + # accuracyRadius + } + } + } +`; + +export const DEMO_MODE = gql` + query DemoMode { + config { + demoMode + } + } +`; + +export const ANALYTICS = gql` + query Analytics { + config { + analytics { + id + enabled + configuration { + key + value + type + } + } + } + } +`; + +export const SEARCH_CONFIG = gql` + query SearchConfig { + config { + search { + global { + isEnabled + isDefault + } + } + } + } +`; diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 605d9ddc3..6cffea1f3 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -116,7 +116,7 @@ export const FETCH_EVENT = gql` `; export const FETCH_EVENT_BASIC = gql` - query ($uuid: UUID!) { + query FetchEventBasic($uuid: UUID!) { event(uuid: $uuid) { id uuid diff --git a/js/src/graphql/feed_tokens.ts b/js/src/graphql/feed_tokens.ts index 706dd7f09..d284ecc1f 100644 --- a/js/src/graphql/feed_tokens.ts +++ b/js/src/graphql/feed_tokens.ts @@ -15,7 +15,7 @@ export const CREATE_FEED_TOKEN_ACTOR = gql` `; export const CREATE_FEED_TOKEN = gql` - mutation { + mutation CreateFeedToken { createFeedToken { token actor { @@ -29,7 +29,7 @@ export const CREATE_FEED_TOKEN = gql` `; export const DELETE_FEED_TOKEN = gql` - mutation deleteFeedToken($token: String!) { + mutation DeleteFeedToken($token: String!) { deleteFeedToken(token: $token) { actor { id diff --git a/js/src/graphql/followers.ts b/js/src/graphql/followers.ts index c72f67e06..565fc3806 100644 --- a/js/src/graphql/followers.ts +++ b/js/src/graphql/followers.ts @@ -36,6 +36,10 @@ export const UPDATE_FOLLOWER = gql` updateFollower(id: $id, approved: $approved) { id approved + actor { + id + preferredUsername + } } } `; diff --git a/js/src/graphql/location.ts b/js/src/graphql/location.ts new file mode 100644 index 000000000..13106de4c --- /dev/null +++ b/js/src/graphql/location.ts @@ -0,0 +1,34 @@ +import gql from "graphql-tag"; + +export const CURRENT_USER_LOCATION_CLIENT = gql` + query currentUserLocation { + currentUserLocation @client { + lat + lon + accuracy + isIPLocation + name + picture + } + } +`; + +export const UPDATE_CURRENT_USER_LOCATION_CLIENT = gql` + mutation UpdateCurrentUserLocation( + $lat: Float + $lon: Float + $accuracy: Int + $isIPLocation: Boolean + $name: String + $picture: pictureInfoElement + ) { + updateCurrentUserLocation( + lat: $lat + lon: $lon + accuracy: $accuracy + isIPLocation: $isIPLocation + name: $name + picture: $picture + ) @client + } +`; diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index d25436182..acf0c5de1 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -49,10 +49,17 @@ const REPORT_FRAGMENT = gql` uuid title description + beginsOn picture { id url } + organizerActor { + ...ActorFragment + } + attributedTo { + ...ActorFragment + } } comments { id diff --git a/js/src/graphql/search.ts b/js/src/graphql/search.ts index 22e2d9a33..29a61271b 100644 --- a/js/src/graphql/search.ts +++ b/js/src/graphql/search.ts @@ -4,8 +4,134 @@ import { ADDRESS_FRAGMENT } from "./address"; import { EVENT_OPTIONS_FRAGMENT } from "./event_options"; import { TAG_FRAGMENT } from "./tags"; +export const GROUP_RESULT_FRAGMENT = gql` + fragment GroupResultFragment on GroupSearchResult { + id + avatar { + id + url + } + type + preferredUsername + name + domain + summary + url + } +`; + export const SEARCH_EVENTS_AND_GROUPS = gql` query SearchEventsAndGroups( + $location: String + $radius: Float + $tags: String + $term: String + $type: EventType + $categoryOneOf: [String] + $statusOneOf: [EventStatus] + $languageOneOf: [String] + $searchTarget: SearchTarget + $beginsOn: DateTime + $endsOn: DateTime + $bbox: String + $zoom: Int + $eventPage: Int + $groupPage: Int + $limit: Int + ) { + searchEvents( + location: $location + radius: $radius + tags: $tags + term: $term + type: $type + categoryOneOf: $categoryOneOf + statusOneOf: $statusOneOf + languageOneOf: $languageOneOf + searchTarget: $searchTarget + beginsOn: $beginsOn + endsOn: $endsOn + bbox: $bbox + zoom: $zoom + page: $eventPage + limit: $limit + ) { + total + elements { + id + title + uuid + beginsOn + picture { + id + url + } + url + status + tags { + ...TagFragment + } + physicalAddress { + ...AdressFragment + } + organizerActor { + ...ActorFragment + } + attributedTo { + ...ActorFragment + } + options { + isOnline + } + __typename + } + } + searchGroups( + term: $term + location: $location + radius: $radius + languageOneOf: $languageOneOf + searchTarget: $searchTarget + bbox: $bbox + zoom: $zoom + page: $groupPage + limit: $limit + ) { + total + elements { + __typename + id + avatar { + id + url + } + type + preferredUsername + name + domain + summary + url + ...GroupResultFragment + banner { + id + url + } + followersCount + membersCount + physicalAddress { + ...AdressFragment + } + } + } + } + ${TAG_FRAGMENT} + ${ADDRESS_FRAGMENT} + ${GROUP_RESULT_FRAGMENT} + ${ACTOR_FRAGMENT} +`; + +export const SEARCH_EVENTS = gql` + query SearchEvents( $location: String $radius: Float $tags: String @@ -15,7 +141,6 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` $beginsOn: DateTime $endsOn: DateTime $eventPage: Int - $groupPage: Int $limit: Int ) { searchEvents( @@ -59,6 +184,21 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` __typename } } + } + ${EVENT_OPTIONS_FRAGMENT} + ${TAG_FRAGMENT} + ${ADDRESS_FRAGMENT} + ${ACTOR_FRAGMENT} +`; + +export const SEARCH_GROUPS = gql` + query SearchGroups( + $location: String + $radius: Float + $term: String + $groupPage: Int + $limit: Int + ) { searchGroups( term: $term location: $location @@ -73,20 +213,14 @@ export const SEARCH_EVENTS_AND_GROUPS = gql` id url } - members(roles: "member,moderator,administrator,creator") { - total - } - followers(approved: true) { - total - } + membersCount + followersCount physicalAddress { ...AdressFragment } } } } - ${EVENT_OPTIONS_FRAGMENT} - ${TAG_FRAGMENT} ${ADDRESS_FRAGMENT} ${ACTOR_FRAGMENT} `; diff --git a/js/src/graphql/statistics.ts b/js/src/graphql/statistics.ts index e229eafce..feda64c38 100644 --- a/js/src/graphql/statistics.ts +++ b/js/src/graphql/statistics.ts @@ -1,7 +1,7 @@ import gql from "graphql-tag"; export const STATISTICS = gql` - query { + query Statistics { statistics { numberOfUsers numberOfEvents @@ -15,3 +15,12 @@ export const STATISTICS = gql` } } `; + +export const CATEGORY_STATISTICS = gql` + query CategoryStatistics { + categoryStatistics { + key + number + } + } +`; diff --git a/js/src/graphql/tags.ts b/js/src/graphql/tags.ts index eadd78ab8..ff84a0a78 100644 --- a/js/src/graphql/tags.ts +++ b/js/src/graphql/tags.ts @@ -21,7 +21,7 @@ export const TAGS = gql` `; export const FILTER_TAGS = gql` - query FilterTags($filter: String) { + query FilterTags($filter: String!) { tags(filter: $filter) { ...TagFragment } diff --git a/js/src/graphql/user.ts b/js/src/graphql/user.ts index 6c9c1bb9b..7c3aefd7a 100644 --- a/js/src/graphql/user.ts +++ b/js/src/graphql/user.ts @@ -94,10 +94,10 @@ export const CURRENT_USER_CLIENT = gql` export const UPDATE_CURRENT_USER_CLIENT = gql` mutation UpdateCurrentUser( - $id: String! - $email: String! - $isLoggedIn: Boolean! - $role: UserRole! + $id: String + $email: String + $isLoggedIn: Boolean + $role: UserRole ) { updateCurrentUser( id: $id @@ -184,6 +184,12 @@ export const USER_NOTIFICATIONS = gql` settings { ...UserSettingFragment } + feedTokens { + token + actor { + id + } + } activitySettings { key method @@ -194,6 +200,15 @@ export const USER_NOTIFICATIONS = gql` ${USER_SETTINGS_FRAGMENT} `; +export const USER_FRAGMENT_FEED_TOKENS = gql` + fragment UserFeedTokensFragment on User { + id + feedTokens { + token + } + } +`; + export const UPDATE_ACTIVITY_SETTING = gql` mutation UpdateActivitySetting( $key: String! diff --git a/js/src/histoire.setup.ts b/js/src/histoire.setup.ts new file mode 100644 index 000000000..b9757b419 --- /dev/null +++ b/js/src/histoire.setup.ts @@ -0,0 +1,17 @@ +import { defineSetupVue3 } from "@histoire/plugin-vue"; +import { orugaConfig } from "./oruga-config"; +import { i18n } from "./utils/i18n"; +import Oruga from "@oruga-ui/oruga-next"; +import "@oruga-ui/oruga-next/dist/oruga-full-vars.css"; +import "./assets/tailwind.css"; +import "./assets/oruga-tailwindcss.css"; +import locale from "date-fns/locale/en-US"; +import MaterialIcon from "./components/core/MaterialIcon.vue"; + +export const setupVue3 = defineSetupVue3(({ app }) => { + // Vue plugin + app.use(i18n); + app.use(Oruga, orugaConfig); + app.component("material-icon", MaterialIcon); + app.provide("dateFnsLocale", locale); +}); diff --git a/js/src/i18n/ar.json b/js/src/i18n/ar.json index 0abe22d32..8f6921325 100644 --- a/js/src/i18n/ar.json +++ b/js/src/i18n/ar.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/be.json b/js/src/i18n/be.json index 9a3b13bfe..85b360c9c 100644 --- a/js/src/i18n/be.json +++ b/js/src/i18n/be.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/bn.json b/js/src/i18n/bn.json index 18e8a1f05..a5e325574 100644 --- a/js/src/i18n/bn.json +++ b/js/src/i18n/bn.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/ca.json b/js/src/i18n/ca.json index d59273ebf..35b8c5b49 100644 --- a/js/src/i18n/ca.json +++ b/js/src/i18n/ca.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Comença una discussió", "{contact} will be displayed as contact.": "Es mostrarà {contact} com a contacte.|Es mostraran {contact} com a contactes.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "S'ha acceptat la soŀlicitud de seguiment de @{username}", "@{username}'s follow request was rejected": "S'ha rebutjat la soŀlicitud de seguir-te de @{username}", diff --git a/js/src/i18n/cs.json b/js/src/i18n/cs.json index 8d43a6a53..431719b8b 100644 --- a/js/src/i18n/cs.json +++ b/js/src/i18n/cs.json @@ -10,7 +10,7 @@ "0 Bytes": "0 bajtů", "{contact} will be displayed as contact.": "{contact} bude zobrazen jako kontakt.|{contact} bude zobrazen jako kontakty.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Žádost o sledování uživatele @{username} byla přijata", "@{username}'s follow request was rejected": "Žádost o sledování uživatele @{username} byla zamítnuta", @@ -1246,7 +1246,7 @@ "instance rules": "pravidla instance", "mobilizon-instance.tld": "mobilizon-instance.tld", "more than 1360 contributors": "více než 1360 přispěvatelů", - "new@email.com": "new@email.com", + "new{'@'}email.com": "new{'@'}email.com", "profile@instance": "profile@instance", "report #{report_number}": "hlášení #{report_number}", "return to the event's page": "návrat na stránku události", diff --git a/js/src/i18n/cy.json b/js/src/i18n/cy.json index a55304c58..a1cc30bff 100644 --- a/js/src/i18n/cy.json +++ b/js/src/i18n/cy.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/de.json b/js/src/i18n/de.json index 787c9b5af..7f95e3369 100644 --- a/js/src/i18n/de.json +++ b/js/src/i18n/de.json @@ -10,7 +10,7 @@ "0 Bytes": "0 Bytes", "{contact} will be displayed as contact.": "{contact} wird als Kontakt angezeigt.|{contact} werden als Kontakte angezeigt.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Die Folgeanfrage von @{username} wurde angenommen", "@{username}'s follow request was rejected": "@{username}'s Folgeanfrage wurde zurückgewiesen", @@ -1246,7 +1246,7 @@ "instance rules": "Instanz-Regeln", "mobilizon-instance.tld": "mobilizon-instance.tld", "more than 1360 contributors": "mehr als 1360 Spender:innen", - "new@email.com": "new@email.com", + "new{'@'}email.com": "new{'@'}email.com", "profile@instance": "profil@instanz", "report #{report_number}": "Meldung #{report_number}", "return to the event's page": "zurück zur Seite der Veranstaltung", diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 4a0150659..3df97a0cb 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -312,7 +312,7 @@ "Terms": "Terms", "The account's email address was changed. Check your emails to verify it.": "The account's email address was changed. Check your emails to verify it.", "The actual number of participants may differ, as this event is hosted on another instance.": "The actual number of participants may differ, as this event is hosted on another instance.", - "The content came from another server. Transfer an anonymous copy of the report?": "The content came from another server. Transfer an anonymous copy of the report ?", + "The content came from another server. Transfer an anonymous copy of the report?": "The content came from another server. Transfer an anonymous copy of the report?", "The draft event has been updated": "The draft event has been updated", "The event has been created as a draft": "The event has been created as a draft", "The event has been published": "The event has been published", @@ -352,7 +352,7 @@ "Use my location": "Use my location", "Username": "Username", "Users": "Users", - "View a reply": "|View one reply|View {totalReplies} replies", + "View a reply": "View no replies|View one reply|View {totalReplies} replies", "View event page": "View event page", "View everything": "View everything", "View page on {hostname} (in a new window)": "View page on {hostname} (in a new window)", @@ -843,7 +843,7 @@ "Last published events": "Last published events", "Events nearby": "Events nearby", "Within {number} kilometers of {place}": "|Within one kilometer of {place}|Within {number} kilometers of {place}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "Yesterday": "Yesterday", "You created the event {event}.": "You created the event {event}.", "The event {event} was created by {profile}.": "The event {event} was created by {profile}.", @@ -1158,6 +1158,7 @@ "When the post is private, you'll need to share the link around.": "When the post is private, you'll need to share the link around.", "Reset": "Reset", "Local time ({timezone})": "Local time ({timezone})", + "Local times ({timezone})": "Local times ({timezone})", "Time in your timezone ({timezone})": "Time in your timezone ({timezone})", "Export": "Export", "Times in your timezone ({timezone})": "Times in your timezone ({timezone})", @@ -1300,7 +1301,7 @@ "Do you really want to suspend this account? All of the user's profiles will be deleted.": "Do you really want to suspend this account? All of the user's profiles will be deleted.", "Suspend the account": "Suspend the account", "No user matches the filter": "No user matches the filter", - "new@email.com": "new@email.com", + "new{'@'}email.com": "new{'@'}email.com", "Other users with the same email domain": "Other users with the same email domain", "Other users with the same IP address": "Other users with the same IP address", "IP Address": "IP Address", @@ -1330,5 +1331,79 @@ "Your membership is pending approval": "Your membership is pending approval", "Activate notifications": "Activate notifications", "Deactivate notifications": "Deactivate notifications", - "Membership requests will be approved by a group moderator": "Membership requests will be approved by a group moderator" + "Membership requests will be approved by a group moderator": "Membership requests will be approved by a group moderator", + "Geolocate me": "Geolocate me", + "Events nearby {position}": "Events nearby {position}", + "View more events around {position}": "View more events around {position}", + "Popular groups nearby {position}": "Popular groups nearby {position}", + "View more groups around {position}": "View more groups around {position}", + "Photo by {author} on {source}": "Photo by {author} on {source}", + "Online upcoming events": "Online upcoming events", + "View more online events": "View more online events", + "Owncast": "Owncast", + "{count} events": "{count} events", + "Categories": "Categories", + "Category illustrations credits": "Category illustrations credits", + "Illustration picture for “{category}” by {author} on {source} ({license})": "Illustration picture for “{category}” by {author} on {source} ({license})", + "View all categories": "View all categories", + "{instanceName} ({domain})": "{instanceName} ({domain})", + "This instance, {instanceName}, hosts your profile, so remember its name.": "This instance, {instanceName}, hosts your profile, so remember its name.", + "Keyword, event title, group name, etc.": "Keyword, event title, group name, etc.", + "Go!": "Go!", + "Explore!": "Explore!", + "Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance", + "Open user menu": "Open user menu", + "Open main menu": "Open main menu", + "{'@'}{username} ({role})": "{'@'}{username} ({role})", + "This is like your federated username ({username}) for groups. It will allow the group to be found on the federation, and is guaranteed to be unique.": "This is like your federated username ({username}) for groups. It will allow the group to be found on the federation, and is guaranteed to be unique.", + "Confirm": "Confirm", + "Published events with {comments} comments and {participations} confirmed participations": "Published events with {comments} comments and {participations} confirmed participations", + "Ex: someone{'@'}mobilizon.org": "Ex: someone{'@'}mobilizon.org", + "Group members": "Group members", + "e.g. Nantes, Berlin, Cork, …": "e.g. Nantes, Berlin, Cork, …", + "find, create and organise events": "find, create and organise events", + "tool designed to serve you": "tool designed to serve you", + "multitude of interconnected Mobilizon websites": "multitude of interconnected Mobilizon websites", + "Mobilizon is a tool that helps you {find_create_organize_events}.": "Mobilizon is a tool that helps you {find_create_organize_events}.", + "Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.": "Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.", + "Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.": "Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.", + "translation": "", + "detail": "", + "Events close to you": "Events close to you", + "Popular groups close to you": "Popular groups close to you", + "View more events": "View more events", + "Hide filters": "Hide filters", + "Show filters": "Show filters", + "Online events": "Online events", + "Event date": "Event date", + "Distance": "Distance", + "{numberOfCategories} selected": "{numberOfCategories} selected", + "Event status": "Event status", + "Statuses": "Statuses", + "Languages": "Languages", + "{numberOfLanguages} selected": "{numberOfLanguages} selected", + "Apply filters": "Apply filters", + "Any distance": "Any distance", + "{number} kilometers": "{number} kilometers", + "The pad will be created on {service}": "The pad will be created on {service}", + "The calc will be created on {service}": "The calc will be created on {service}", + "The videoconference will be created on {service}": "The videoconference will be created on {service}", + "Search target": "Search target", + "In this instance's network": "In this instance's network", + "On the Fediverse": "On the Fediverse", + "Report reason": "Report reason", + "Reported content": "Reported content", + "No results found": "No results found", + "{eventsCount} events found": "No events found|One event found|{eventsCount} events found", + "{groupsCount} groups found": "No groups found|One group found|{groupsCount} groups found", + "{resultsCount} results found": "No results found|On result found|{resultsCount} results found", + "Loading map": "Loading map", + "Sort by": "Sort by", + "Map": "Map", + "List": "List", + "Best match": "Best match", + "Most recently published": "Most recently published", + "Least recently published": "Least recently published", + "With the most participants": "With the most participants", + "Number of members": "Number of members" } \ No newline at end of file diff --git a/js/src/i18n/eo.json b/js/src/i18n/eo.json index b2dde7e30..c85f96c8d 100644 --- a/js/src/i18n/eo.json +++ b/js/src/i18n/eo.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/es.json b/js/src/i18n/es.json index f1c7a1021..e695929f6 100644 --- a/js/src/i18n/es.json +++ b/js/src/i18n/es.json @@ -10,7 +10,7 @@ "0 Bytes": "0 Bytes", "{contact} will be displayed as contact.": " {contact} se mostrará como contacto. |{contact}se mostrará como contactos.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Se aceptó la solicitud de seguimiento de @{username}", "@{username}'s follow request was rejected": "Se rechazó la solicitud de seguimiento de @{username}", @@ -1246,7 +1246,7 @@ "instance rules": "reglas de instancia", "mobilizon-instance.tld": "mobilizon-instance.tld", "more than 1360 contributors": "más de 1360 contribuyentes", - "new@email.com": "nuevo@email.com", + "new{'@'}email.com": "nuevo{'@'}email.com", "profile@instance": "perfil@instancia", "report #{report_number}": "informe #{report_number}", "return to the event's page": "volver a la página del evento", diff --git a/js/src/i18n/eu.json b/js/src/i18n/eu.json index af3debc6e..05f57f0d9 100644 --- a/js/src/i18n/eu.json +++ b/js/src/i18n/eu.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/fa.json b/js/src/i18n/fa.json index 874697a2b..28a2a6ebb 100644 --- a/js/src/i18n/fa.json +++ b/js/src/i18n/fa.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/fi.json b/js/src/i18n/fi.json index 3a3614de3..9550ada1a 100644 --- a/js/src/i18n/fi.json +++ b/js/src/i18n/fi.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Aloita keskustelu", "{contact} will be displayed as contact.": "{contact} näytetään kontaktina.|{contact} näytetään kontakteina.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role)}", "@{username}'s follow request was accepted": "Käyttäjän @{username} seurauspyyntö hyväksyttiin", "@{username}'s follow request was rejected": "Käyttäjän @{username} seuraamispyyntö hylättiin", diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index eb757f344..81affe511 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -10,8 +10,8 @@ "0 Bytes": "0 octets", "{contact} will be displayed as contact.": "{contact} sera affiché·e comme contact.|{contact} seront affiché·e·s comme contacts.", "@{group}": "@{group}", - "@{username}": "@{username}", - "@{username} ({role})": "@{username} ({role})", + "{'@'}{username}": "{'@'}{username}", + "{'@'}{username} ({role})": "{'@'}{username} ({role})", "@{username}'s follow request was accepted": "La demande de suivi de @{username} a été acceptée", "@{username}'s follow request was rejected": "La demande de suivi de @{username} a été rejettée", "A cookie is a small file containing information that is sent to your computer when you visit a website. When you visit the site again, the cookie allows that site to recognize your browser. Cookies may store user preferences and other information. You can configure your browser to refuse all cookies. However, this may result in some website features or services partially working. Local storage works the same way but allows you to store more data.": "Un cookie est un petit fichier contenant des informations qui est envoyé à votre ordinateur lorsque vous visitez un site web. Lorsque vous visitez le site à nouveau, le cookie permet à ce site de reconnaître votre navigateur. Les cookies peuvent stocker les préférences des utilisateur·rice·s et d'autres informations. Vous pouvez configurer votre navigateur pour qu'il refuse tous les cookies. Toutefois, cela peut entraîner le non-fonctionnement de certaines fonctions ou de certains services du site web. Le stockage local fonctionne de la même manière mais permet de stocker davantage de données.", @@ -516,6 +516,7 @@ "Loading comments…": "Chargement des commentaires…", "Local": "Local·e", "Local time ({timezone})": "Heure locale ({timezone})", + "Local times ({timezone})": "Heures locales ({timezone})", "Locality": "Commune", "Location": "Lieu", "Log in": "Se connecter", @@ -1222,7 +1223,7 @@ "instance rules": "règles de l'instance", "mobilizon-instance.tld": "instance-mobilizon.tld", "more than 1360 contributors": "plus de 1360 contributeur·rice·s", - "new@email.com": "nouvel@email.com", + "new{'@'}email.com": "nouvel{'@'}email.com", "profile@instance": "profil@instance", "report #{report_number}": "le signalement #{report_number}", "return to the event's page": "retourner sur la page de l'événement", @@ -1321,5 +1322,72 @@ "Your membership is pending approval": "Votre adhésion est en attente d'approbation", "Activate notifications": "Activer les notifications", "Deactivate notifications": "Désactiver les notifications", - "Membership requests will be approved by a group moderator": "Les demandes d'adhésion seront approuvées par un⋅e modérateur⋅ice du groupe" + "Membership requests will be approved by a group moderator": "Les demandes d'adhésion seront approuvées par un⋅e modérateur⋅ice du groupe", + "Geolocate me": "Me géolocaliser", + "Events nearby {position}": "Événements près de {position}", + "View more events around {position}": "Voir plus d'événements près de {position}", + "Popular groups nearby {position}": "Groupes populaires près de {position}", + "View more groups around {position}": "Voir plus de groupes près de {position}", + "Photo by {author} on {source}": "Photo par {author} sur {source}", + "Online upcoming events": "Événements en ligne à venir", + "View more online events": "Voir plus d'événements en ligne", + "Owncast": "Owncast", + "{count} events": "{count} événements", + "Categories": "Catégories", + "Category illustrations credits": "Crédits des illustrations des catégories", + "Illustration picture for “{category}” by {author} on {source} ({license})": "Image d'illustration pour “{category}” par {author} sur {source} ({license})", + "View all categories": "Voir toutes les catégories", + "{instanceName} ({domain})": "{instanceName} ({domain})", + "This instance, {instanceName}, hosts your profile, so remember its name.": "Cette instance, {instanceName}, héberge votre profil, donc souvenez-vous de son nom.", + "Keyword, event title, group name, etc.": "Mot clé, titre d'un événement, nom d'un groupe, etc.", + "Go!": "Go!", + "Explore!": "Explorer !", + "Join {instance}, a Mobilizon instance": "Rejoignez {instance}, une instance Mobilizon", + "Open user menu": "Ouvrir le menu utilisateur", + "Open main menu": "Ouvrir le menu principal", + "This is like your federated username ({username}) for groups. It will allow the group to be found on the federation, and is guaranteed to be unique.": "C'est comme votre adresse fédérée ({username}) pour les groupes. Cela permettra au groupe d'être trouvable sur la fédération, et est garanti d'être unique.", + "Published events with {comments} comments and {participations} confirmed participations": "Événements publiés avec {comments} commentaires et {participations} participations confirmées", + "find, create and organise events": "trouver, créer et organiser des événements", + "tool designed to serve you": "outil conçu pour vous servir", + "multitude of interconnected Mobilizon websites": "multitude de sites web Mobilizon interconnectés", + "Mobilizon is a tool that helps you {find_create_organize_events}.": "Mobilizon est un outil qui vous permet de {find_create_organize_events}.", + "Ethical alternative to Facebook events, groups and pages, Mobilizon is a {tool_designed_to_serve_you}. Period.": "Alternative éthique aux événements, groupes et pages Facebook, Mobilizon est un {tool_designed_to_serve_you}. Point.", + "Mobilizon is not a giant platform, but a {multitude_of_interconnected_mobilizon_websites}.": "Mobilizon n’est pas une plateforme géante, mais une {multitude_of_interconnected_mobilizon_websites}.", + "Events close to you": "Événements proches de vous", + "Popular groups close to you": "Groupes populaires proches de vous", + "View more events": "Voir plus d'événements", + "Hide filters": "Masquer les filtres", + "Show filters": "Afficher les filtres", + "Online events": "Événements en ligne", + "Event date": "Date de l'événement", + "Distance": "Distance", + "{numberOfCategories} selected": "{numberOfCategories} sélectionnées", + "Event status": "Statut de l'événement", + "Statuses": "Statuts", + "Languages": "Langues", + "{numberOfLanguages} selected": "{numberOfLanguages} sélectionnées", + "Apply filters": "Appliquer les filtres", + "Any distance": "N'importe quelle distance", + "{number} kilometers": "{number} kilomètres", + "The pad will be created on {service}": "Le pad sera créé sur {service}", + "The calc will be created on {service}": "Le calc sera créé sur {service}", + "The videoconference will be created on {service}": "La visio-conférence sera créée sur {service}", + "Search target": "Cible de la recherche", + "In this instance's network": "Dans le réseau de cette instance", + "On the Fediverse": "Dans le fediverse", + "Report reason": "Raison du signalement", + "Reported content": "Contenu signalé", + "No results found": "Aucun résultat trouvé", + "{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés", + "{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés", + "{resultsCount} results found": "Aucun résultat trouvé|Un résultat trouvé|{resultsCount} résultats trouvés", + "Loading map": "Chargement de la carte", + "Sort by": "Trier par", + "Map": "Carte", + "List": "Liste", + "Best match": "Pertinence", + "Most recently published": "Publié récemment", + "Least recently published": "Le moins récemment publié", + "With the most participants": "Avec le plus de participants", + "Number of members": "Nombre de membres" } diff --git a/js/src/i18n/gd.json b/js/src/i18n/gd.json index 9e643acbe..97f133d17 100644 --- a/js/src/i18n/gd.json +++ b/js/src/i18n/gd.json @@ -10,7 +10,7 @@ "0 Bytes": "0 baidht", "{contact} will be displayed as contact.": "Thèid {contact} a shealltain mar neach-conaltraidh.|Thèid {contact} a shealltain mar luchd-conaltraidh.|Thèid {contact} a shealltain mar luchd-conaltraidh.|Thèid {contact} a shealltain mar luchd-conaltraidh.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Chaidh gabhail ris an t-iarrtas leantainn aig @{username}", "@{username}'s follow request was rejected": "Chaidh an t-iarrtas leantainn aig @{username} a dhiùltadh", @@ -1245,7 +1245,7 @@ "instance rules": "riaghailtean an ionstans", "mobilizon-instance.tld": "ionstans-mobilizon.tld", "more than 1360 contributors": "còrr is 1360 luchd-cuideachaidh", - "new@email.com": "ùr@post-d.com", + "new{'@'}email.com": "ùr{'@'}post-d.com", "profile@instance": "ainm@ionstans", "report #{report_number}": "gearan #{report_number}", "return to the event's page": "till gu duilleag an tachartais", diff --git a/js/src/i18n/gl.json b/js/src/i18n/gl.json index b95d19559..f6181305d 100644 --- a/js/src/i18n/gl.json +++ b/js/src/i18n/gl.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Comezar un debate", "{contact} will be displayed as contact.": "{contact} será mostrado como contacto.|{contact} serán mostrados como contactos.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Foi aceptada a solicitude de seguimento de @{username}", "@{username}'s follow request was rejected": "A solicitude de seguimento de @{username} foi rexeitada", diff --git a/js/src/i18n/hr.json b/js/src/i18n/hr.json index 98ffd57df..88cfa87bd 100644 --- a/js/src/i18n/hr.json +++ b/js/src/i18n/hr.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Pokreni razgovor", "{contact} will be displayed as contact.": "{contact} će se prikazati kao kontakt.|{contact} će se prikazivati kao kontakti.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Prihvaćen je zahtjev za praćenje od @{username}", "@{username}'s follow request was rejected": "Odbijen zahtjev za praćenje od @{username}", diff --git a/js/src/i18n/hu.json b/js/src/i18n/hu.json index 832adb8e6..ea6180895 100644 --- a/js/src/i18n/hu.json +++ b/js/src/i18n/hu.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Megbeszélés indítása", "{contact} will be displayed as contact.": "{contact} meg lesz jelenítve kapcsolatként.|{contact} meg lesznek jelenítve kapcsolatokként.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "@{username} követési kérése el lett fogadva", "@{username}'s follow request was rejected": "@{username} követési kérése vissza lett utasítva", diff --git a/js/src/i18n/id.json b/js/src/i18n/id.json index 0b2451560..1137039bb 100644 --- a/js/src/i18n/id.json +++ b/js/src/i18n/id.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/it.json b/js/src/i18n/it.json index b748a74ac..83ddd8b79 100644 --- a/js/src/i18n/it.json +++ b/js/src/i18n/it.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Inizia una discussione", "{contact} will be displayed as contact.": "{contact} verrà visualizzato come contatto.|{contact} verranno visualizzati come contatti.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "La richiesta di poter seguire da parte dell'utente @{username} è stata accettata", "@{username}'s follow request was rejected": "La richiesta di follow a @{username} è stata respinta", diff --git a/js/src/i18n/ja.json b/js/src/i18n/ja.json index 79b28cc2c..7f9acdda6 100644 --- a/js/src/i18n/ja.json +++ b/js/src/i18n/ja.json @@ -10,7 +10,7 @@ "0 Bytes": "0バイト", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "@{username}のフォローリクエストが受け入れられました", "@{username}'s follow request was rejected": "@{username}のフォローリクエストは拒否されました", diff --git a/js/src/i18n/kab.json b/js/src/i18n/kab.json index 335edcbe6..411c6910a 100644 --- a/js/src/i18n/kab.json +++ b/js/src/i18n/kab.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/kn.json b/js/src/i18n/kn.json index 8c001ec70..b84a0ef72 100644 --- a/js/src/i18n/kn.json +++ b/js/src/i18n/kn.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/nl.json b/js/src/i18n/nl.json index d7d9769e9..57b7aad92 100644 --- a/js/src/i18n/nl.json +++ b/js/src/i18n/nl.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Start een discussie", "{contact} will be displayed as contact.": "{contact} wordt weergegeven als contactpersoon.|{contact} worden weergegeven als contactpersonen.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "@{username}'s volg verzoek is geaccepteerd", "@{username}'s follow request was rejected": "@{username}'s volg verzoek is afgewezen", diff --git a/js/src/i18n/nn.json b/js/src/i18n/nn.json index 002f6907a..b1d78ec31 100644 --- a/js/src/i18n/nn.json +++ b/js/src/i18n/nn.json @@ -10,7 +10,7 @@ "0 Bytes": "0 byte", "{contact} will be displayed as contact.": "{contact} vil bli vist som kontakt.|{contact} vil bli viste som kontaktar.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Fylgjeførespurnaden frå @{username} er godkjend", "@{username}'s follow request was rejected": "Fyljgeførespurnaden frå @{username} vart avslegen", @@ -1223,7 +1223,7 @@ "instance rules": "reglar for nettstaden", "mobilizon-instance.tld": "mobilizon-nettstad.domene", "more than 1360 contributors": "meir enn 1360 bidragsytarar", - "new@email.com": "ny@epost.no", + "new{'@'}email.com": "ny{'@'}epost.no", "profile@instance": "profil@nettstad", "report #{report_number}": "rapport nr. {report_number}", "return to the event's page": "gå tilbake til hendingssida", diff --git a/js/src/i18n/oc.json b/js/src/i18n/oc.json index 353f45d2b..76dfbb6cf 100644 --- a/js/src/i18n/oc.json +++ b/js/src/i18n/oc.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Començar una discussion", "{contact} will be displayed as contact.": "{contact} serà mostrat coma contacte.|{contact} seràn mostrats coma contactes.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "La demanda de seguiment de @{username} es estada regetada", diff --git a/js/src/i18n/pl.json b/js/src/i18n/pl.json index 66364b6f6..94cdd209c 100644 --- a/js/src/i18n/pl.json +++ b/js/src/i18n/pl.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Rozpocznij dyskusję", "{contact} will be displayed as contact.": "{contact} będzie wyświetlany jako kontakt.|{contact} będą wyświetlane jako kontakty.", "@{group}": "@{group}", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/pt.json b/js/src/i18n/pt.json index 76f1ae20d..799dd817b 100644 --- a/js/src/i18n/pt.json +++ b/js/src/i18n/pt.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/pt_BR.json b/js/src/i18n/pt_BR.json index 3943e4cfe..c8fa6a1de 100644 --- a/js/src/i18n/pt_BR.json +++ b/js/src/i18n/pt_BR.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Iniciar uma conversa", "{contact} will be displayed as contact.": "{contact} será mostrado como contato.|{contact} será mostrado como contatos.", "@{group}": "@{group}", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/ru.json b/js/src/i18n/ru.json index ad49389d8..5d6f77812 100644 --- a/js/src/i18n/ru.json +++ b/js/src/i18n/ru.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Начать обсуждение", "{contact} will be displayed as contact.": "{contact} будет отображаться как контакт.|{contact} будут отображаться как контакты.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Запрос на подписку от @{username} принят", "@{username}'s follow request was rejected": "Запрос на подписку от @{username} отклонён", diff --git a/js/src/i18n/sl.json b/js/src/i18n/sl.json index 7e9ac92c6..b7e3b3103 100644 --- a/js/src/i18n/sl.json +++ b/js/src/i18n/sl.json @@ -9,7 +9,7 @@ "+ Start a discussion": "+ Začni razpravo", "{contact} will be displayed as contact.": "{contact} bo prikazan kot stik.|{contact} bodo prikazani kot stiki.", "@{group}": "@{group}", - "@{username}": "@{username}", + "{'@'}{username}": "{'@'}{username}", "@{username} ({role})": "@{username} ({role})", "@{username}'s follow request was accepted": "Prošnja za sledenje je bila sprejeta. @{username} vam sedaj sledi", "@{username}'s follow request was rejected": "Prošnja za sledenje od uporabnika @{username} je bila zavrnjena", diff --git a/js/src/i18n/sv.json b/js/src/i18n/sv.json index e578aa6f1..3ce80bde9 100644 --- a/js/src/i18n/sv.json +++ b/js/src/i18n/sv.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "@{grupp}", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "@{användarnamn} ({roll})", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/i18n/zh_Hant.json b/js/src/i18n/zh_Hant.json index a98ac992e..cb49101af 100644 --- a/js/src/i18n/zh_Hant.json +++ b/js/src/i18n/zh_Hant.json @@ -9,7 +9,7 @@ "+ Start a discussion": "", "{contact} will be displayed as contact.": "", "@{group}": "", - "@{username}": "", + "{'@'}{username}": "", "@{username} ({role})": "", "@{username}'s follow request was accepted": "", "@{username}'s follow request was rejected": "", diff --git a/js/src/main.ts b/js/src/main.ts index 0e6695195..a3bfaf088 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -1,47 +1,71 @@ -import Vue from "vue"; -import Buefy from "buefy"; -import Component from "vue-class-component"; +import { provide, createApp, h, computed, ref } from "vue"; import VueScrollTo from "vue-scrollto"; -import VueMeta from "vue-meta"; -import VTooltip from "v-tooltip"; -import VueAnnouncer from "@vue-a11y/announcer"; -import VueSkipTo from "@vue-a11y/skip-to"; +// import VueAnnouncer from "@vue-a11y/announcer"; +// import VueSkipTo from "@vue-a11y/skip-to"; import App from "./App.vue"; -import router from "./router"; -import { NotifierPlugin } from "./plugins/notifier"; -import filters from "./filters"; -import { i18n } from "./utils/i18n"; -import apolloProvider from "./vue-apollo"; -import Breadcrumbs from "@/components/Utils/Breadcrumbs.vue"; +import { router } from "./router"; +import { i18n, locale } from "./utils/i18n"; +import { apolloClient } from "./vue-apollo"; +import Breadcrumbs from "@/components/Utils/NavBreadcrumbs.vue"; +import { DefaultApolloClient } from "@vue/apollo-composable"; import "./registerServiceWorker"; import "./assets/tailwind.css"; +import { setAppForAnalytics } from "./services/statistics"; +import { dateFnsPlugin } from "./plugins/dateFns"; +import { dialogPlugin } from "./plugins/dialog"; +import { snackbarPlugin } from "./plugins/snackbar"; +import { notifierPlugin } from "./plugins/notifier"; +import FloatingVue from "floating-vue"; +import "floating-vue/dist/style.css"; +import Oruga from "@oruga-ui/oruga-next"; +import "@oruga-ui/oruga-next/dist/oruga.css"; +import "./assets/oruga-tailwindcss.css"; +import { orugaConfig } from "./oruga-config"; +import MaterialIcon from "./components/core/MaterialIcon.vue"; +import { createHead } from "@vueuse/head"; +import { CONFIG } from "./graphql/config"; +import { IConfig } from "./types/config.model"; -Vue.config.productionTip = false; +// Vue.use(VueAnnouncer); +// Vue.use(VueSkipTo); -Vue.use(Buefy); -Vue.use(NotifierPlugin); -Vue.use(filters); -Vue.use(VueMeta); -Vue.use(VueScrollTo); -Vue.use(VTooltip); -Vue.use(VueAnnouncer); -Vue.use(VueSkipTo); -Vue.component("breadcrumbs-nav", Breadcrumbs); - -// Register the router hooks with their names -Component.registerHooks([ - "beforeRouteEnter", - "beforeRouteLeave", - "beforeRouteUpdate", // for vue-router 2.2+ -]); - -/* eslint-disable no-new */ -new Vue({ - router, - apolloProvider, - el: "#app", - template: "", - components: { App }, - render: (h) => h(App), - i18n, +const app = createApp({ + setup() { + provide(DefaultApolloClient, apolloClient); + }, + render: () => h(App), }); + +app.use(router); +app.use(i18n); +app.use(dateFnsPlugin, { locale }); +app.use(dialogPlugin); +app.use(snackbarPlugin); +app.use(notifierPlugin); +app.use(VueScrollTo); +app.use(FloatingVue); + +app.component("breadcrumbs-nav", Breadcrumbs); +app.component("material-icon", MaterialIcon); +app.use(Oruga, orugaConfig); + +const instanceName = ref(); + +apolloClient + .query<{ config: IConfig }>({ + query: CONFIG, + }) + .then(({ data: configData }) => { + instanceName.value = configData.config?.name; + }); + +const head = createHead({ + titleTemplate: computed(() => + instanceName.value ? `%s | ${instanceName.value}` : "%s" + ).value, +}); +app.use(head); + +app.mount("#app"); + +setAppForAnalytics(app); diff --git a/js/src/mixins/AddressAutoCompleteMixin.ts b/js/src/mixins/AddressAutoCompleteMixin.ts deleted file mode 100644 index b5074b1af..000000000 --- a/js/src/mixins/AddressAutoCompleteMixin.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Component, Prop, Vue, Watch } from "vue-property-decorator"; -import { LatLng } from "leaflet"; -import { Address, IAddress } from "../types/address.model"; -import { ADDRESS, REVERSE_GEOCODE } from "../graphql/address"; -import { CONFIG } from "../graphql/config"; -import { IConfig } from "../types/config.model"; -import debounce from "lodash/debounce"; -import { DebouncedFunc } from "lodash"; - -@Component({ - components: { - "map-leaflet": () => - import(/* webpackChunkName: "map" */ "@/components/Map.vue"), - }, - apollo: { - config: CONFIG, - }, -}) -export default class AddressAutoCompleteMixin extends Vue { - @Prop({ required: true }) - value!: IAddress; - gettingLocationError: string | null = null; - - gettingLocation = false; - - mapDefaultZoom = 15; - - addressData: IAddress[] = []; - - selected: IAddress = new Address(); - - config!: IConfig; - - isFetching = false; - - fetchAsyncData!: DebouncedFunc<(query: string) => Promise>; - - // eslint-disable-next-line no-undef - protected location!: GeolocationPosition; - - // We put this in data because of issues like - // https://github.com/vuejs/vue-class-component/issues/263 - data(): Record { - return { - fetchAsyncData: debounce(this.asyncData, 200), - }; - } - - @Watch("config") - watchConfig(config: IConfig): void { - if (!config.geocoding.autocomplete) { - // If autocomplete is disabled, we put a larger debounce value - // so that we don't request with incomplete address - this.fetchAsyncData = debounce(this.asyncData, 2000); - } - } - - async asyncData(query: string): Promise { - if (!query.length) { - this.addressData = []; - this.selected = new Address(); - return; - } - - if (query.length < 3) { - this.addressData = []; - return; - } - - this.isFetching = true; - const result = await this.$apollo.query({ - query: ADDRESS, - fetchPolicy: "network-only", - variables: { - query, - locale: this.$i18n.locale, - }, - }); - - this.addressData = result.data.searchAddress.map( - (address: IAddress) => new Address(address) - ); - this.isFetching = false; - } - - get queryText(): string { - return (this.value && new Address(this.value).fullName) || ""; - } - - set queryText(text: string) { - if (text === "" && this.selected?.id) { - console.log("doing reset"); - this.resetAddress(); - } - } - - resetAddress(): void { - this.$emit("input", null); - this.selected = new Address(); - } - - async locateMe(): Promise { - this.gettingLocation = true; - this.gettingLocationError = null; - try { - this.location = await this.getLocation(); - this.mapDefaultZoom = 12; - this.reverseGeoCode( - new LatLng( - this.location.coords.latitude, - this.location.coords.longitude - ), - 12 - ); - } catch (e: any) { - this.gettingLocationError = e.message; - } - this.gettingLocation = false; - } - - async reverseGeoCode(e: LatLng, zoom: number): Promise { - // If the position has been updated through autocomplete selection, no need to geocode it! - if (this.checkCurrentPosition(e)) return; - const result = await this.$apollo.query({ - query: REVERSE_GEOCODE, - variables: { - latitude: e.lat, - longitude: e.lng, - zoom, - locale: this.$i18n.locale, - }, - }); - - this.addressData = result.data.reverseGeocode.map( - (address: IAddress) => new Address(address) - ); - if (this.addressData.length > 0) { - const defaultAddress = new Address(this.addressData[0]); - this.selected = defaultAddress; - this.$emit("input", this.selected); - } - } - - checkCurrentPosition(e: LatLng): boolean { - if (!this.selected || !this.selected.geom) return false; - const lat = parseFloat(this.selected.geom.split(";")[1]); - const lon = parseFloat(this.selected.geom.split(";")[0]); - - return e.lat === lat && e.lng === lon; - } - - // eslint-disable-next-line no-undef - async getLocation(): Promise { - let errorMessage = this.$t("Failed to get location."); - return new Promise((resolve, reject) => { - if (!("geolocation" in navigator)) { - reject(new Error(errorMessage as string)); - } - - navigator.geolocation.getCurrentPosition( - (pos) => { - resolve(pos); - }, - (err) => { - switch (err.code) { - // eslint-disable-next-line no-undef - case GeolocationPositionError.PERMISSION_DENIED: - errorMessage = this.$t("The geolocation prompt was denied."); - break; - // eslint-disable-next-line no-undef - case GeolocationPositionError.POSITION_UNAVAILABLE: - errorMessage = this.$t("Your position was not available."); - break; - // eslint-disable-next-line no-undef - case GeolocationPositionError.TIMEOUT: - errorMessage = this.$t("Geolocation was not determined in time."); - break; - default: - errorMessage = err.message; - } - reject(new Error(errorMessage as string)); - } - ); - }); - } - - get fieldErrors(): Array> { - const errors = []; - if (this.gettingLocationError) { - errors.push({ - [this.gettingLocationError]: true, - }); - } - return errors; - } - - // eslint-disable-next-line class-methods-use-this - get isSecureContext(): boolean { - return window.isSecureContext; - } -} diff --git a/js/src/mixins/activity.ts b/js/src/mixins/activity.ts deleted file mode 100644 index fa8fa4f37..000000000 --- a/js/src/mixins/activity.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; -import { IActivity } from "@/types/activity.model"; -import { IActor } from "@/types/actor"; -import { Component, Prop, Vue } from "vue-property-decorator"; - -@Component({ - apollo: { - currentActor: CURRENT_ACTOR_CLIENT, - }, -}) -export default class ActivityMixin extends Vue { - @Prop({ required: true, type: Object }) activity!: IActivity; - currentActor!: IActor; - - get subjectParams(): Record { - return this.activity.subjectParams.reduce( - (acc: Record, { key, value }) => { - acc[key] = value; - return acc; - }, - {} - ); - } - - get isAuthorCurrentActor(): boolean { - return ( - this.activity.author.id === this.currentActor.id && - this.currentActor.id !== undefined - ); - } -} diff --git a/js/src/mixins/actor.ts b/js/src/mixins/actor.ts deleted file mode 100644 index 66e31af49..000000000 --- a/js/src/mixins/actor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IActor } from "@/types/actor"; -import { IEvent } from "@/types/event.model"; -import { Component, Vue } from "vue-property-decorator"; - -@Component -export default class ActorMixin extends Vue { - static actorIsOrganizer(actor: IActor, event: IEvent): boolean { - console.log("actorIsOrganizer actor", actor.id); - console.log("actorIsOrganizer event", event); - return ( - event.organizerActor !== undefined && actor.id === event.organizerActor.id - ); - } -} diff --git a/js/src/mixins/event.ts b/js/src/mixins/event.ts deleted file mode 100644 index f0f426525..000000000 --- a/js/src/mixins/event.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { mixins } from "vue-class-component"; -import { Component, Vue } from "vue-property-decorator"; -import { SnackbarProgrammatic as Snackbar } from "buefy"; -import { ParticipantRole } from "@/types/enums"; -import { IParticipant } from "../types/participant.model"; -import { IEvent } from "../types/event.model"; -import { - DELETE_EVENT, - EVENT_PERSON_PARTICIPATION, - FETCH_EVENT, - LEAVE_EVENT, -} from "../graphql/event"; -import { IPerson } from "../types/actor"; -import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core"; - -@Component -export default class EventMixin extends mixins(Vue) { - protected async leaveEvent( - event: IEvent, - actorId: string, - token: string | null = null, - anonymousParticipationConfirmed: boolean | null = null - ): Promise { - try { - const { data: resultData } = await this.$apollo.mutate<{ - leaveEvent: IParticipant; - }>({ - mutation: LEAVE_EVENT, - variables: { - eventId: event.id, - actorId, - token, - }, - update: ( - store: ApolloCache<{ - leaveEvent: IParticipant; - }>, - { data }: FetchResult - ) => { - if (data == null) return; - let participation; - - if (!token) { - const participationCachedData = store.readQuery<{ - person: IPerson; - }>({ - query: EVENT_PERSON_PARTICIPATION, - variables: { eventId: event.id, actorId }, - }); - if (participationCachedData == null) return; - const { person } = participationCachedData; - [participation] = person.participations.elements; - - store.modify({ - id: `Person:${actorId}`, - fields: { - participations() { - return { - elements: [], - total: 0, - }; - }, - }, - }); - } - - const eventCachedData = store.readQuery<{ event: IEvent }>({ - query: FETCH_EVENT, - variables: { uuid: event.uuid }, - }); - if (eventCachedData == null) return; - const { event: eventCached } = eventCachedData; - if (eventCached === null) { - console.error("Cannot update event cache, because of null value."); - return; - } - const participantStats = { ...eventCached.participantStats }; - if ( - participation && - participation?.role === ParticipantRole.NOT_APPROVED - ) { - participantStats.notApproved -= 1; - } else if (anonymousParticipationConfirmed === false) { - participantStats.notConfirmed -= 1; - } else { - participantStats.going -= 1; - participantStats.participant -= 1; - } - store.writeQuery({ - query: FETCH_EVENT, - variables: { uuid: event.uuid }, - data: { - event: { - ...eventCached, - participantStats, - }, - }, - }); - }, - }); - if (resultData) { - this.participationCancelledMessage(); - } - } catch (error: any) { - Snackbar.open({ - message: error.message, - type: "is-danger", - position: "is-bottom", - }); - console.error(error); - } - } - - private participationCancelledMessage() { - this.$notifier.success( - this.$t("You have cancelled your participation") as string - ); - } - - protected async openDeleteEventModal(event: IEvent): Promise { - function escapeRegExp(string: string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - } - const participantsLength = event.participantStats.participant; - const prefix = participantsLength - ? this.$tc( - "There are {participants} participants.", - event.participantStats.participant, - { - participants: event.participantStats.participant, - } - ) - : ""; - - this.$buefy.dialog.prompt({ - type: "is-danger", - title: this.$t("Delete event") as string, - message: `${prefix} - ${this.$t( - "Are you sure you want to delete this event? This action cannot be reverted." - )} -

- ${this.$t('To confirm, type your event title "{eventTitle}"', { - eventTitle: event.title, - })}`, - confirmText: this.$t("Delete {eventTitle}", { - eventTitle: event.title, - }) as string, - inputAttrs: { - placeholder: event.title, - pattern: escapeRegExp(event.title), - }, - onConfirm: () => this.deleteEvent(event), - }); - } - - private async deleteEvent(event: IEvent) { - const { title: eventTitle, id: eventId } = event; - - try { - await this.$apollo.mutate({ - mutation: DELETE_EVENT, - variables: { - eventId: event.id, - }, - }); - const cache = this.$apollo.getClient().cache as InMemoryCache; - cache.evict({ id: `Event:${eventId}` }); - cache.gc(); - /** - * When the event corresponding has been deleted (by the organizer). - * A notification is already triggered. - * - * @type {string} - */ - this.$emit("event-deleted", event.id); - - this.$buefy.notification.open({ - message: this.$t("Event {eventTitle} deleted", { - eventTitle, - }) as string, - type: "is-success", - position: "is-bottom-right", - duration: 5000, - }); - } catch (error: any) { - Snackbar.open({ - message: error.message, - type: "is-danger", - position: "is-bottom", - }); - - console.error(error); - } - } -} diff --git a/js/src/mixins/group.ts b/js/src/mixins/group.ts deleted file mode 100644 index e7d8d7204..000000000 --- a/js/src/mixins/group.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - CURRENT_ACTOR_CLIENT, - GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED, - PERSON_STATUS_GROUP, -} from "@/graphql/actor"; -import { DELETE_GROUP, FETCH_GROUP } from "@/graphql/group"; -import RouteName from "@/router/name"; -import { - IActor, - IFollower, - IGroup, - IPerson, - usernameWithDomain, -} from "@/types/actor"; -import { MemberRole } from "@/types/enums"; -import { Component, Vue } from "vue-property-decorator"; -import { Route } from "vue-router"; - -const now = new Date(); - -@Component({ - apollo: { - group: { - query: FETCH_GROUP, - fetchPolicy: "cache-and-network", - variables() { - return { - name: this.$route.params.preferredUsername, - beforeDateTime: null, - afterDateTime: now, - }; - }, - skip() { - return !this.$route.params.preferredUsername; - }, - error({ graphQLErrors }) { - this.handleErrors(graphQLErrors); - }, - }, - person: { - query: PERSON_STATUS_GROUP, - fetchPolicy: "cache-and-network", - variables() { - return { - id: this.currentActor.id, - group: usernameWithDomain(this.group), - }; - }, - subscribeToMore: { - document: GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED, - variables() { - return { - actorId: this.currentActor.id, - group: this.group?.preferredUsername, - }; - }, - skip() { - return ( - !this.currentActor || - !this.currentActor.id || - !this.group?.preferredUsername - ); - }, - }, - skip() { - return ( - !this.currentActor || - !this.currentActor.id || - !this.group?.preferredUsername - ); - }, - }, - currentActor: CURRENT_ACTOR_CLIENT, - }, -}) -export default class GroupMixin extends Vue { - group!: IGroup; - - currentActor!: IActor; - - person!: IPerson; - - get isCurrentActorAGroupAdmin(): boolean { - return this.hasCurrentActorThisRole(MemberRole.ADMINISTRATOR); - } - - get isCurrentActorAGroupModerator(): boolean { - return this.hasCurrentActorThisRole([ - MemberRole.MODERATOR, - MemberRole.ADMINISTRATOR, - ]); - } - - get isCurrentActorAGroupMember(): boolean { - return this.hasCurrentActorThisRole([ - MemberRole.MODERATOR, - MemberRole.ADMINISTRATOR, - MemberRole.MEMBER, - ]); - } - - get isCurrentActorAPendingGroupMember(): boolean { - return this.hasCurrentActorThisRole([MemberRole.NOT_APPROVED]); - } - - hasCurrentActorThisRole(givenRole: string | string[]): boolean { - const roles = Array.isArray(givenRole) ? givenRole : [givenRole]; - return ( - this.person?.memberships?.total > 0 && - roles.includes(this.person?.memberships?.elements[0].role) - ); - } - - get isCurrentActorFollowing(): boolean { - return this.currentActorFollow?.approved === true; - } - - get isCurrentActorPendingFollow(): boolean { - return this.currentActorFollow?.approved === false; - } - - get isCurrentActorFollowingNotify(): boolean { - return ( - this.isCurrentActorFollowing && this.currentActorFollow?.notify === true - ); - } - - get currentActorFollow(): IFollower | null { - if (this.person?.follows?.total > 0) { - return this.person?.follows?.elements[0]; - } - return null; - } - - handleErrors(errors: any[]): void { - if ( - errors.some((error) => error.status_code === 404) || - errors.some(({ message }) => message.includes("has invalid value $uuid")) - ) { - this.$router.replace({ name: RouteName.PAGE_NOT_FOUND }); - } - } - - confirmDeleteGroup(): void { - this.$buefy.dialog.confirm({ - title: this.$t("Delete group") as string, - message: this.$t( - "Are you sure you want to completely delete this group? All members - including remote ones - will be notified and removed from the group, and all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed." - ) as string, - confirmText: this.$t("Delete group") as string, - cancelText: this.$t("Cancel") as string, - type: "is-danger", - hasIcon: true, - onConfirm: () => this.deleteGroup(), - }); - } - - async deleteGroup(): Promise { - await this.$apollo.mutate<{ deleteGroup: IGroup }>({ - mutation: DELETE_GROUP, - variables: { - groupId: this.group.id, - }, - }); - return this.$router.push({ name: RouteName.MY_GROUPS }); - } -} diff --git a/js/src/mixins/identityEdition.ts b/js/src/mixins/identityEdition.ts deleted file mode 100644 index f3e0a2282..000000000 --- a/js/src/mixins/identityEdition.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Component, Mixins, Vue } from "vue-property-decorator"; -import { Person } from "@/types/actor"; - -// TODO: Refactor into js/src/utils/username.ts -@Component -export default class IdentityEditionMixin extends Mixins(Vue) { - identity: Person = new Person(); - - oldDisplayName: string | null = null; - - autoUpdateUsername(newDisplayName: string | null): void { - const oldUsername = IdentityEditionMixin.convertToUsername( - this.oldDisplayName - ); - - if (this.identity.preferredUsername === oldUsername) { - this.identity.preferredUsername = - IdentityEditionMixin.convertToUsername(newDisplayName); - } - - this.oldDisplayName = newDisplayName; - } - - private static convertToUsername(value: string | null) { - if (!value) return ""; - - // https://stackoverflow.com/a/37511463 - return value - .toLocaleLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/\s{2,}/, " ") - .replace(/ /g, "_") - .replace(/[^a-z0-9_]/g, "") - .replace(/_{2,}/, ""); - } - - validateUsername(): boolean { - return ( - this.identity.preferredUsername === - IdentityEditionMixin.convertToUsername(this.identity.preferredUsername) - ); - } -} diff --git a/js/src/mixins/onboarding.ts b/js/src/mixins/onboarding.ts deleted file mode 100644 index a4cbd608d..000000000 --- a/js/src/mixins/onboarding.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SET_USER_SETTINGS, USER_SETTINGS } from "@/graphql/user"; -import RouteName from "@/router/name"; -import { ICurrentUser } from "@/types/current-user.model"; -import { Component, Vue } from "vue-property-decorator"; - -@Component({ - apollo: { - loggedUser: USER_SETTINGS, - }, -}) -export default class Onboarding extends Vue { - loggedUser!: ICurrentUser; - - RouteName = RouteName; - - protected async doUpdateSetting( - variables: Record - ): Promise { - await this.$apollo.mutate<{ setUserSettings: string }>({ - mutation: SET_USER_SETTINGS, - variables, - }); - } -} diff --git a/js/src/mixins/post.ts b/js/src/mixins/post.ts deleted file mode 100644 index cfd7968e1..000000000 --- a/js/src/mixins/post.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { DELETE_POST, FETCH_POST } from "@/graphql/post"; -import { usernameWithDomain } from "@/types/actor"; -import { IPost } from "@/types/post.model"; -import { Component, Vue } from "vue-property-decorator"; -import RouteName from "../router/name"; - -@Component({ - apollo: { - post: { - query: FETCH_POST, - fetchPolicy: "cache-and-network", - variables() { - return { - slug: this.slug, - }; - }, - skip() { - return !this.slug; - }, - error({ graphQLErrors }) { - this.handleErrors(graphQLErrors); - }, - }, - }, -}) -export default class PostMixin extends Vue { - post!: IPost; - - RouteName = RouteName; - - protected async openDeletePostModal(): Promise { - this.$buefy.dialog.confirm({ - type: "is-danger", - title: this.$t("Delete post") as string, - message: this.$t( - "Are you sure you want to delete this post? This action cannot be reverted." - ) as string, - onConfirm: () => this.deletePost(), - }); - } - - async deletePost(): Promise { - const { data } = await this.$apollo.mutate({ - mutation: DELETE_POST, - variables: { - id: this.post.id, - }, - }); - if (data && this.post.attributedTo) { - this.$router.push({ - name: RouteName.POSTS, - params: { - preferredUsername: usernameWithDomain(this.post.attributedTo), - }, - }); - } - } - - handleErrors(errors: any[]): void { - if (errors.some((error) => error.status_code === 404)) { - this.$router.replace({ name: RouteName.PAGE_NOT_FOUND }); - } - } -} diff --git a/js/src/mixins/resource.ts b/js/src/mixins/resource.ts deleted file mode 100644 index f4c6bbb34..000000000 --- a/js/src/mixins/resource.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component, Vue } from "vue-property-decorator"; -import { IResource } from "@/types/resource"; - -@Component -export default class ResourceMixin extends Vue { - static resourcePath(resource: IResource): string { - const { path } = resource; - if (path && path[0] === "/") { - return path.slice(1); - } - return path || ""; - } - - static resourcePathArray(resource: IResource): string[] { - return ResourceMixin.resourcePath(resource).split("/"); - } -} diff --git a/js/src/oruga-config.ts b/js/src/oruga-config.ts new file mode 100644 index 000000000..f3101b623 --- /dev/null +++ b/js/src/oruga-config.ts @@ -0,0 +1,110 @@ +export const orugaConfig = { + iconPack: "", + iconComponent: "material-icon", + statusIcon: true, + button: { + rootClass: "btn", + variantClass: "btn-", + roundedClass: "btn-rounded", + outlinedClass: "btn-outlined-", + disabledClass: "btn-disabled", + sizeClass: "btn-size-", + }, + field: { + rootClass: "field", + labelClass: "field-label", + messageClass: "text-sm italic", + variantClass: "field-", + variantMessageClass: "field-message-", + }, + input: { + inputClass: "input", + roundedClass: "rounded", + variantClass: "input-", + iconRightClass: "input-icon-right", + }, + inputitems: { + itemClass: "inputitems-item", + }, + autocomplete: { + menuClass: "autocomplete-menu", + itemClass: "autocomplete-item", + }, + icon: { + variantClass: "icon-", + }, + checkbox: { + checkClass: "checkbox", + checkCheckedClass: "checkbox-checked", + labelClass: "checkbox-label", + }, + dropdown: { + rootClass: "dropdown", + menuClass: "dropdown-menu", + itemClass: "dropdown-item", + itemActiveClass: "dropdown-item-active", + }, + steps: { + itemHeaderActiveClass: "steps-nav-item-active", + itemHeaderPreviousClass: "steps-nav-item-previous", + stepMarkerClass: "step-marker", + stepDividerClass: "step-divider", + }, + datepicker: { + iconNext: "ChevronRight", + iconPrev: "ChevronLeft", + }, + modal: { + rootClass: "modal", + contentClass: "modal-content", + }, + switch: { + labelClass: "switch-label", + checkCheckedClass: "switch-check-checked", + }, + select: { + selectClass: "select", + }, + radio: { + checkCheckedClass: "radio-checked", + checkClass: "form-radio", + labelClass: "radio-label", + }, + notification: { + rootClass: "notification", + variantClass: "notification-", + }, + table: { + tableClass: "table", + tdClass: "table-td", + thClass: "table-th", + rootClass: "table-root", + }, + pagination: { + rootClass: "pagination", + simpleClass: "pagination-simple", + listClass: "pagination-list", + infoClass: "pagination-info", + linkClass: "pagination-link", + linkCurrentClass: "pagination-link-current", + linkDisabledClass: "pagination-link-disabled", + nextBtnClass: "pagination-next", + prevBtnClass: "pagination-previous", + ellipsisClass: "pagination-ellipsis", + }, + tabs: { + rootClass: "tabs", + navTabsClass: "tabs-nav", + navTypeClass: "tabs-nav-", + navSizeClass: "tabs-nav-", + tabItemWrapperClass: "tabs-nav-item-wrapper", + itemHeaderTypeClass: "tabs-nav-item-", + itemHeaderActiveClass: "tabs-nav-item-active-", + }, + tooltip: { + rootClass: "tooltip", + contentClass: "tooltip-content", + arrowClass: "tooltip-arrow", + variantClass: "tooltip-content-", + }, +}; diff --git a/js/src/plugins/dateFns.ts b/js/src/plugins/dateFns.ts index 78c48dd02..545428f0b 100644 --- a/js/src/plugins/dateFns.ts +++ b/js/src/plugins/dateFns.ts @@ -1,17 +1,23 @@ -import Locale from "date-fns"; -import VueInstance from "vue"; +import type { Locale } from "date-fns"; +import { App } from "vue"; -declare module "vue/types/vue" { - interface Vue { - $dateFnsLocale: Locale; - } -} +export const dateFnsPlugin = { + install(app: App, options: { locale: string }) { + function dateFnsfileForLanguage(lang: string) { + const matches: Record = { + en_US: "en-US", + en: "en-US", + }; + return matches[lang] ?? lang; + } -export function DateFnsPlugin( - vue: typeof VueInstance, - { locale }: { locale: string } -): void { - import(`date-fns/locale/${locale}/index.js`).then((localeEntity) => { - VueInstance.prototype.$dateFnsLocale = localeEntity; - }); -} + import( + `../../node_modules/date-fns/esm/locale/${dateFnsfileForLanguage( + options.locale + )}/index.js` + ).then((localeEntity: { default: Locale }) => { + app.provide("dateFnsLocale", localeEntity.default); + app.config.globalProperties.$dateFnsLocale = localeEntity.default; + }); + }, +}; diff --git a/js/src/plugins/dialog.ts b/js/src/plugins/dialog.ts new file mode 100644 index 000000000..42eefc509 --- /dev/null +++ b/js/src/plugins/dialog.ts @@ -0,0 +1,99 @@ +import DialogComponent from "@/components/core/CustomDialog.vue"; +import { App } from "vue"; + +export class Dialog { + private app: App; + + constructor(app: App) { + this.app = app; + } + + prompt({ + title, + message, + confirmText, + cancelText, + variant, + hasIcon, + size, + onConfirm, + onCancel, + inputAttrs, + hasInput, + }: { + title?: string; + message: string; + confirmText?: string; + cancelText?: string; + variant?: string; + hasIcon?: boolean; + size?: string; + onConfirm?: (prompt: string) => void; + onCancel?: (source: string) => void; + inputAttrs?: Record; + hasInput?: boolean; + }) { + this.app.config.globalProperties.$oruga.modal.open({ + component: DialogComponent, + props: { + title, + message, + confirmText, + cancelText, + variant, + hasIcon, + size, + onConfirm, + onCancel, + inputAttrs, + hasInput, + }, + }); + } + + confirm({ + title, + message, + confirmText, + cancelText, + variant, + hasIcon, + size, + onConfirm, + onCancel, + }: { + title: string; + message: string; + confirmText?: string; + cancelText?: string; + variant: string; + hasIcon?: boolean; + size?: string; + onConfirm: () => any; + onCancel?: (source: string) => any; + }) { + console.debug("confirming something"); + this.app.config.globalProperties.$oruga.modal.open({ + component: DialogComponent, + props: { + title, + message, + confirmText, + cancelText, + variant, + hasIcon, + size, + onConfirm, + onCancel, + }, + }); + } +} + +export const dialogPlugin = { + install(app: App) { + const dialog = new Dialog(app); + app.config.globalProperties.$dialog = dialog; + app.provide("dialog", dialog); + }, +}; diff --git a/js/src/plugins/notifier.ts b/js/src/plugins/notifier.ts index 5000bf986..0b5e9c3fe 100644 --- a/js/src/plugins/notifier.ts +++ b/js/src/plugins/notifier.ts @@ -1,66 +1,39 @@ -/* eslint-disable no-shadow */ -import VueInstance from "vue"; -import { ColorModifiers } from "buefy/types/helpers.d"; -import { Route, RawLocation } from "vue-router"; - -declare module "vue/types/vue" { - interface Vue { - $notifier: { - success: (message: string) => void; - error: (message: string) => void; - info: (message: string) => void; - }; - beforeRouteEnter?( - to: Route, - from: Route, - next: (to?: RawLocation | false | ((vm: VueInstance) => void)) => void - ): void; - - beforeRouteLeave?( - to: Route, - from: Route, - next: (to?: RawLocation | false | ((vm: VueInstance) => void)) => void - ): void; - - beforeRouteUpdate?( - to: Route, - from: Route, - next: (to?: RawLocation | false | ((vm: VueInstance) => void)) => void - ): void; - } -} +import { App } from "vue"; export class Notifier { - private readonly vue: typeof VueInstance; + private app: App; - constructor(vue: typeof VueInstance) { - this.vue = vue; + constructor(app: App) { + this.app = app; } success(message: string): void { - this.notification(message, "is-success"); + this.notification(message, "success"); } error(message: string): void { - this.notification(message, "is-danger"); + this.notification(message, "danger"); } info(message: string): void { - this.notification(message, "is-info"); + this.notification(message, "info"); } - private notification(message: string, type: ColorModifiers) { - this.vue.prototype.$buefy.notification.open({ + private notification(message: string, type: string) { + this.app.config.globalProperties.$oruga.notification.open({ message, duration: 5000, - position: "is-bottom-right", + position: "bottom-right", type, hasIcon: true, }); } } -/* eslint-disable */ -export function NotifierPlugin(vue: typeof VueInstance): void { - vue.prototype.$notifier = new Notifier(vue); -} +export const notifierPlugin = { + install(app: App) { + const notifier = new Notifier(app); + app.config.globalProperties.$notifier = notifier; + app.provide("notifier", notifier); + }, +}; diff --git a/js/src/plugins/snackbar.ts b/js/src/plugins/snackbar.ts new file mode 100644 index 000000000..cb425b173 --- /dev/null +++ b/js/src/plugins/snackbar.ts @@ -0,0 +1,53 @@ +import SnackbarComponent from "@/components/core/CustomSnackbar.vue"; +import { App } from "vue"; + +export class Snackbar { + private app: App; + + constructor(app: App) { + this.app = app; + } + + open({ + message, + variant, + position, + actionText, + cancelText, + onAction, + }: { + message?: string; + queue?: boolean; + indefinite?: boolean; + variant?: string; + position?: string; + actionText?: string; + cancelText?: string; + onAction?: () => any; + }) { + this.app.config.globalProperties.$oruga.notification.open({ + component: SnackbarComponent, + props: { + message, + // queue, + // indefinite, + actionText, + cancelText, + onAction, + position: position ?? "bottom-right", + variant: variant ?? "dark", + }, + position: position ?? "bottom-right", + variant: variant ?? "dark", + duration: 5000000, + }); + } +} + +export const snackbarPlugin = { + install(app: App) { + const snackbar = new Snackbar(app); + app.config.globalProperties.$snackbar = snackbar; + app.provide("snackbar", snackbar); + }, +}; diff --git a/js/src/registerServiceWorker.ts b/js/src/registerServiceWorker.ts index ee0de1d9b..a84837468 100644 --- a/js/src/registerServiceWorker.ts +++ b/js/src/registerServiceWorker.ts @@ -1,9 +1,7 @@ -/* eslint-disable no-console */ - import { register } from "register-service-worker"; -if ("serviceWorker" in navigator && isProduction()) { - register(`${process.env.BASE_URL}service-worker.js`, { +if ("serviceWorker" in navigator && import.meta.env.PROD) { + register(`${import.meta.env.BASE_URL}service-worker.js`, { ready() { console.debug( "App is being served from cache by a service worker.\n" + @@ -34,7 +32,3 @@ if ("serviceWorker" in navigator && isProduction()) { }, }); } - -function isProduction(): boolean { - return process.env.NODE_ENV === "production"; -} diff --git a/js/src/router/actor.ts b/js/src/router/actor.ts index 6ac74ef98..59bb89a9b 100644 --- a/js/src/router/actor.ts +++ b/js/src/router/actor.ts @@ -1,7 +1,8 @@ -import { RouteConfig } from "vue-router"; -import { ImportedComponent } from "vue/types/options"; +import { RouteRecordRaw } from "vue-router"; import { i18n } from "@/utils/i18n"; +const { t } = i18n.global; + export enum ActorRouteName { GROUP = "Group", CREATE_GROUP = "CreateGroup", @@ -9,33 +10,30 @@ export enum ActorRouteName { MY_GROUPS = "MY_GROUPS", } -export const actorRoutes: RouteConfig[] = [ +export const actorRoutes: RouteRecordRaw[] = [ { path: "/groups/create", name: ActorRouteName.CREATE_GROUP, - component: (): Promise => - import(/* webpackChunkName: "CreateGroup" */ "@/views/Group/Create.vue"), + component: (): Promise => import("@/views/Group/CreateView.vue"), meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("Create group") as string }, + announcer: { message: (): string => t("Create group") as string }, }, }, { path: "/@:preferredUsername", name: ActorRouteName.GROUP, - component: (): Promise => - import(/* webpackChunkName: "Group" */ "@/views/Group/Group.vue"), + component: (): Promise => import("@/views/Group/GroupView.vue"), props: true, meta: { requiredAuth: false, announcer: { skip: true } }, }, { path: "/groups/me", name: ActorRouteName.MY_GROUPS, - component: (): Promise => - import(/* webpackChunkName: "MyGroups" */ "@/views/Group/MyGroups.vue"), + component: (): Promise => import("@/views/Group/MyGroups.vue"), meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("My groups") as string }, + announcer: { message: (): string => t("My groups") as string }, }, }, ]; diff --git a/js/src/router/discussion.ts b/js/src/router/discussion.ts index ec538e466..d31092888 100644 --- a/js/src/router/discussion.ts +++ b/js/src/router/discussion.ts @@ -1,51 +1,45 @@ -import { RouteConfig } from "vue-router"; -import { ImportedComponent } from "vue/types/options"; +import { RouteRecordRaw } from "vue-router"; import { i18n } from "@/utils/i18n"; +const t = i18n.global.t; + export enum DiscussionRouteName { DISCUSSION_LIST = "DISCUSSION_LIST", CREATE_DISCUSSION = "CREATE_DISCUSSION", DISCUSSION = "DISCUSSION", } -export const discussionRoutes: RouteConfig[] = [ +export const discussionRoutes: RouteRecordRaw[] = [ { path: "/@:preferredUsername/discussions", name: DiscussionRouteName.DISCUSSION_LIST, - component: (): Promise => - import( - /* webpackChunkName: "DiscussionsList" */ "@/views/Discussions/DiscussionsList.vue" - ), + component: (): Promise => + import("@/views/Discussions/DiscussionsListView.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Discussions list") as string, + message: (): string => t("Discussions list") as string, }, }, }, { path: "/@:preferredUsername/discussions/new", name: DiscussionRouteName.CREATE_DISCUSSION, - component: (): Promise => - import( - /* webpackChunkName: "CreateDiscussion" */ "@/views/Discussions/Create.vue" - ), + component: (): Promise => import("@/views/Discussions/CreateView.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Create discussion") as string, + message: (): string => t("Create discussion") as string, }, }, }, { path: "/@:preferredUsername/c/:slug/:comment_id?", name: DiscussionRouteName.DISCUSSION, - component: (): Promise => - import( - /* webpackChunkName: "Discussion" */ "@/views/Discussions/Discussion.vue" - ), + component: (): Promise => + import("@/views/Discussions/DiscussionView.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, }, diff --git a/js/src/router/error.ts b/js/src/router/error.ts index 9f1f563ea..d33973a22 100644 --- a/js/src/router/error.ts +++ b/js/src/router/error.ts @@ -1,19 +1,19 @@ -import { RouteConfig } from "vue-router"; -import { ImportedComponent } from "vue/types/options"; +import { RouteRecordRaw } from "vue-router"; import { i18n } from "@/utils/i18n"; +const { t } = i18n.global.t; + export enum ErrorRouteName { ERROR = "Error", } -export const errorRoutes: RouteConfig[] = [ +export const errorRoutes: RouteRecordRaw[] = [ { path: "/error", name: ErrorRouteName.ERROR, - component: (): Promise => - import(/* webpackChunkName: "Error" */ "../views/Error.vue"), + component: (): Promise => import("../views/ErrorView.vue"), meta: { - announcer: { message: (): string => i18n.t("Error") as string }, + announcer: { message: (): string => t("Error") }, }, }, ]; diff --git a/js/src/router/event.ts b/js/src/router/event.ts index 1851b9e0d..c413326a2 100644 --- a/js/src/router/event.ts +++ b/js/src/router/event.ts @@ -1,17 +1,12 @@ -import { RouteConfig, Route } from "vue-router"; -import { ImportedComponent } from "vue/types/options"; import { i18n } from "@/utils/i18n"; +import { RouteLocationNormalized, RouteRecordRaw } from "vue-router"; -const participations = (): Promise => - import( - /* webpackChunkName: "participations" */ "@/views/Event/Participants.vue" - ); -const editEvent = (): Promise => - import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue"); -const event = (): Promise => - import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue"); -const myEvents = (): Promise => - import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue"); +const t = i18n.global.t; + +const participations = () => import("@/views/Event/ParticipantsView.vue"); +const editEvent = () => import("@/views/Event/EditView.vue"); +const event = () => import("@/views/Event/EventView.vue"); +const myEvents = () => import("@/views/Event/MyEventsView.vue"); export enum EventRouteName { EVENT_LIST = "EventList", @@ -28,24 +23,14 @@ export enum EventRouteName { TAG = "Tag", } -export const eventRoutes: RouteConfig[] = [ - { - path: "/events/list/:location?", - name: EventRouteName.EVENT_LIST, - component: (): Promise => - import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"), - meta: { - requiredAuth: false, - announcer: { message: (): string => i18n.t("Event list") as string }, - }, - }, +export const eventRoutes: RouteRecordRaw[] = [ { path: "/events/create", name: EventRouteName.CREATE_EVENT, component: editEvent, meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("Create event") as string }, + announcer: { message: (): string => t("Create event") as string }, }, }, { @@ -55,7 +40,7 @@ export const eventRoutes: RouteConfig[] = [ props: true, meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("My events") as string }, + announcer: { message: (): string => t("My events") as string }, }, }, { @@ -63,7 +48,7 @@ export const eventRoutes: RouteConfig[] = [ name: EventRouteName.EDIT_EVENT, component: editEvent, meta: { requiredAuth: true, announcer: { skip: true } }, - props: (route: Route): Record => { + props: (route: RouteLocationNormalized): Record => { return { ...route.params, ...{ isUpdate: true } }; }, }, @@ -72,7 +57,7 @@ export const eventRoutes: RouteConfig[] = [ name: EventRouteName.DUPLICATE_EVENT, component: editEvent, meta: { requiredAuth: true, announce: { skip: true } }, - props: (route: Route): Record => ({ + props: (route: RouteLocationNormalized): Record => ({ ...route.params, ...{ isDuplicate: true }, }), @@ -94,23 +79,23 @@ export const eventRoutes: RouteConfig[] = [ { path: "/events/:uuid/participate", name: EventRouteName.EVENT_PARTICIPATE_LOGGED_OUT, - component: (): Promise => + component: () => import("../components/Participation/UnloggedParticipation.vue"), props: true, meta: { announcer: { - message: (): string => i18n.t("Unlogged participation") as string, + message: (): string => t("Unlogged participation") as string, }, }, }, { path: "/events/:uuid/participate/with-account", name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, - component: (): Promise => + component: () => import("../components/Participation/ParticipationWithAccount.vue"), meta: { announcer: { - message: (): string => i18n.t("Participation with account") as string, + message: (): string => t("Participation with account") as string, }, }, props: true, @@ -118,12 +103,11 @@ export const eventRoutes: RouteConfig[] = [ { path: "/events/:uuid/participate/without-account", name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT, - component: (): Promise => + component: () => import("../components/Participation/ParticipationWithoutAccount.vue"), meta: { announcer: { - message: (): string => - i18n.t("Participation without account") as string, + message: (): string => t("Participation without account") as string, }, }, props: true, @@ -131,11 +115,11 @@ export const eventRoutes: RouteConfig[] = [ { path: "/participation/email/confirm/:token", name: EventRouteName.EVENT_PARTICIPATE_CONFIRM, - component: (): Promise => + component: () => import("../components/Participation/ConfirmParticipation.vue"), meta: { announcer: { - message: (): string => i18n.t("Confirm participation") as string, + message: (): string => t("Confirm participation") as string, }, }, props: true, @@ -143,12 +127,11 @@ export const eventRoutes: RouteConfig[] = [ { path: "/tag/:tag", name: EventRouteName.TAG, - component: (): Promise => - import(/* webpackChunkName: "Search" */ "@/views/Search.vue"), + component: () => import("@/views/SearchView.vue"), props: true, meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Tag search") as string }, + announcer: { message: (): string => t("Tag search") as string }, }, }, ]; diff --git a/js/src/router/groups.ts b/js/src/router/groups.ts index 067b35879..3a7824311 100644 --- a/js/src/router/groups.ts +++ b/js/src/router/groups.ts @@ -1,5 +1,4 @@ -import { RouteConfig, Route } from "vue-router"; -import { ImportedComponent } from "vue/types/options"; +import { RouteLocationNormalized, RouteRecordRaw } from "vue-router"; export enum GroupsRouteName { TODO_LISTS = "TODO_LISTS", @@ -22,33 +21,29 @@ export enum GroupsRouteName { TIMELINE = "TIMELINE", } -const resourceFolder = (): Promise => +const resourceFolder = (): Promise => import("@/views/Resources/ResourceFolder.vue"); -const groupEvents = (): Promise => - import(/* webpackChunkName: "groupEvents" */ "@/views/Event/GroupEvents.vue"); +const groupEvents = (): Promise => import("@/views/Event/GroupEvents.vue"); -export const groupsRoutes: RouteConfig[] = [ +export const groupsRoutes: RouteRecordRaw[] = [ { path: "/@:preferredUsername/todo-lists", name: GroupsRouteName.TODO_LISTS, - component: (): Promise => - import("@/views/Todos/TodoLists.vue"), + component: (): Promise => import("@/views/Todos/TodoLists.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, }, { path: "/todo-lists/:id", name: GroupsRouteName.TODO_LIST, - component: (): Promise => - import("@/views/Todos/TodoList.vue"), + component: (): Promise => import("@/views/Todos/TodoList.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, }, { path: "/todo/:todoId", name: GroupsRouteName.TODO, - component: (): Promise => - import("@/views/Todos/Todo.vue"), + component: (): Promise => import("@/views/Todos/TodoView.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, }, @@ -56,7 +51,10 @@ export const groupsRoutes: RouteConfig[] = [ path: "/@:preferredUsername/resources", name: GroupsRouteName.RESOURCE_FOLDER_ROOT, component: resourceFolder, - props: { path: "/" }, + props: (to) => ({ + path: "/", + preferredUsername: to.params.preferredUsername, + }), meta: { requiredAuth: true, announcer: { skip: true } }, }, { @@ -68,8 +66,7 @@ export const groupsRoutes: RouteConfig[] = [ }, { path: "/@:preferredUsername/settings", - component: (): Promise => - import("@/views/Group/Settings.vue"), + component: (): Promise => import("@/views/Group/SettingsView.vue"), props: true, meta: { requiredAuth: true }, redirect: { name: GroupsRouteName.GROUP_PUBLIC_SETTINGS }, @@ -78,14 +75,15 @@ export const groupsRoutes: RouteConfig[] = [ { path: "public", name: GroupsRouteName.GROUP_PUBLIC_SETTINGS, - component: (): Promise => + props: true, + component: (): Promise => import("../views/Group/GroupSettings.vue"), meta: { announcer: { skip: true } }, }, { path: "members", name: GroupsRouteName.GROUP_MEMBERS_SETTINGS, - component: (): Promise => + component: (): Promise => import("../views/Group/GroupMembers.vue"), props: true, meta: { announcer: { skip: true } }, @@ -93,7 +91,7 @@ export const groupsRoutes: RouteConfig[] = [ { path: "followers", name: GroupsRouteName.GROUP_FOLLOWERS_SETTINGS, - component: (): Promise => + component: (): Promise => import("../views/Group/GroupFollowers.vue"), props: true, meta: { announcer: { skip: true } }, @@ -102,17 +100,15 @@ export const groupsRoutes: RouteConfig[] = [ }, { path: "/@:preferredUsername/p/new", - component: (): Promise => - import("@/views/Posts/Edit.vue"), + component: (): Promise => import("@/views/Posts/EditView.vue"), props: true, name: GroupsRouteName.POST_CREATE, meta: { requiredAuth: true, announcer: { skip: true } }, }, { path: "/p/:slug/edit", - component: (): Promise => - import("@/views/Posts/Edit.vue"), - props: (route: Route): Record => ({ + component: (): Promise => import("@/views/Posts/EditView.vue"), + props: (route: RouteLocationNormalized): Record => ({ ...route.params, ...{ isUpdate: true }, }), @@ -121,16 +117,14 @@ export const groupsRoutes: RouteConfig[] = [ }, { path: "/p/:slug", - component: (): Promise => - import("@/views/Posts/Post.vue"), + component: (): Promise => import("@/views/Posts/PostView.vue"), props: true, name: GroupsRouteName.POST, meta: { requiredAuth: false, announcer: { skip: true } }, }, { path: "/@:preferredUsername/p", - component: (): Promise => - import("@/views/Posts/List.vue"), + component: (): Promise => import("@/views/Posts/ListView.vue"), props: true, name: GroupsRouteName.POSTS, meta: { requiredAuth: false, announcer: { skip: true } }, @@ -144,7 +138,7 @@ export const groupsRoutes: RouteConfig[] = [ }, { path: "/@:preferredUsername/join", - component: (): Promise => + component: (): Promise => import("@/components/Group/JoinGroupWithAccount.vue"), props: true, name: GroupsRouteName.GROUP_JOIN, @@ -152,7 +146,7 @@ export const groupsRoutes: RouteConfig[] = [ }, { path: "/@:preferredUsername/follow", - component: (): Promise => + component: (): Promise => import("@/components/Group/JoinGroupWithAccount.vue"), props: true, name: GroupsRouteName.GROUP_FOLLOW, @@ -161,8 +155,7 @@ export const groupsRoutes: RouteConfig[] = [ { path: "/@:preferredUsername/timeline", name: GroupsRouteName.TIMELINE, - component: (): Promise => - import("@/views/Group/Timeline.vue"), + component: (): Promise => import("@/views/Group/TimelineView.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, }, diff --git a/js/src/router/guards/register-guard.ts b/js/src/router/guards/register-guard.ts index ac9ec65d4..9cde41b92 100644 --- a/js/src/router/guards/register-guard.ts +++ b/js/src/router/guards/register-guard.ts @@ -1,22 +1,32 @@ +import { IConfig } from "@/types/config.model"; import { ErrorCode } from "@/types/enums"; +import { provideApolloClient, useQuery } from "@vue/apollo-composable"; import { NavigationGuard } from "vue-router"; import { CONFIG } from "../../graphql/config"; -import apolloProvider from "../../vue-apollo"; +import { apolloClient } from "../../vue-apollo"; import { ErrorRouteName } from "../error"; export const beforeRegisterGuard: NavigationGuard = async (to, from, next) => { - const { data } = await apolloProvider.defaultClient.query({ - query: CONFIG, + const { onResult, onError } = provideApolloClient(apolloClient)(() => + useQuery<{ config: IConfig }>(CONFIG) + ); + + onResult(({ data }) => { + const { config } = data; + + if (!config.registrationsOpen && !config.registrationsAllowlist) { + return next({ + name: ErrorRouteName.ERROR, + query: { code: ErrorCode.REGISTRATION_CLOSED }, + }); + } + + return next(); }); - const { config } = data; - - if (!config.registrationsOpen && !config.registrationsAllowlist) { - return next({ - name: ErrorRouteName.ERROR, - query: { code: ErrorCode.REGISTRATION_CLOSED }, - }); - } - + onError((err) => { + console.error(err); + return next(); + }); return next(); }; diff --git a/js/src/router/index.ts b/js/src/router/index.ts index 14f880edc..579999522 100644 --- a/js/src/router/index.ts +++ b/js/src/router/index.ts @@ -1,9 +1,6 @@ -import Vue from "vue"; -import Router, { Route } from "vue-router"; +import { createRouter, createWebHistory } from "vue-router"; import VueScrollTo from "vue-scrollto"; -import { PositionResult } from "vue-router/types/router.d"; -import { ImportedComponent } from "vue/types/options"; -import Home from "../views/Home.vue"; +import HomeView from "../views/HomeView.vue"; import { eventRoutes } from "./event"; import { actorRoutes } from "./actor"; import { errorRoutes } from "./error"; @@ -15,25 +12,21 @@ import { userRoutes } from "./user"; import RouteName from "./name"; import { AVAILABLE_LANGUAGES, i18n } from "@/utils/i18n"; -Vue.use(Router); +const { t } = i18n.global; -function scrollBehavior( - to: Route, - from: Route, - savedPosition: any -): PositionResult | undefined | null { +function scrollBehavior(to: any, from: any, savedPosition: any) { if (to.hash) { VueScrollTo.scrollTo(to.hash, 700); return { selector: to.hash, - offset: { x: 0, y: 10 }, + offset: { left: 0, top: 10 }, }; } if (savedPosition) { return savedPosition; } - return { x: 0, y: 0 }; + return { left: 0, top: 0 }; } export const routes = [ @@ -47,84 +40,83 @@ export const routes = [ { path: "/search", name: RouteName.SEARCH, - component: (): Promise => - import(/* webpackChunkName: "Search" */ "@/views/Search.vue"), + component: (): Promise => import("@/views/SearchView.vue"), props: true, meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Search") as string }, + announcer: { message: (): string => t("Search") as string }, }, }, { path: "/", name: RouteName.HOME, - component: Home, + component: HomeView, meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Homepage") as string }, + announcer: { message: (): string => t("Homepage") as string }, + }, + }, + { + path: "/categories", + name: RouteName.CATEGORIES, + component: (): Promise => import("@/views/CategoriesView.vue"), + meta: { + requiredAuth: false, + announcer: { message: (): string => t("Categories") as string }, }, }, { path: "/about", name: RouteName.ABOUT, - component: (): Promise => - import(/* webpackChunkName: "about" */ "@/views/About.vue"), + component: (): Promise => import("@/views/AboutView.vue"), meta: { requiredAuth: false }, redirect: { name: RouteName.ABOUT_INSTANCE }, children: [ { path: "instance", name: RouteName.ABOUT_INSTANCE, - component: (): Promise => - import( - /* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue" - ), + component: (): Promise => + import("@/views/About/AboutInstanceView.vue"), meta: { announcer: { - message: (): string => i18n.t("About instance") as string, + message: (): string => t("About instance") as string, }, }, }, { path: "/terms", name: RouteName.TERMS, - component: (): Promise => - import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"), + component: (): Promise => import("@/views/About/TermsView.vue"), meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Terms") as string }, + announcer: { message: (): string => t("Terms") as string }, }, }, { path: "/privacy", name: RouteName.PRIVACY, - component: (): Promise => - import(/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"), + component: (): Promise => import("@/views/About/PrivacyView.vue"), meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Privacy") as string }, + announcer: { message: (): string => t("Privacy") as string }, }, }, { path: "/rules", name: RouteName.RULES, - component: (): Promise => - import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"), + component: (): Promise => import("@/views/About/RulesView.vue"), meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Rules") as string }, + announcer: { message: (): string => t("Rules") as string }, }, }, { path: "/glossary", name: RouteName.GLOSSARY, - component: (): Promise => - import( - /* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue" - ), + component: (): Promise => import("@/views/About/GlossaryView.vue"), meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Glossary") as string }, + announcer: { message: (): string => t("Glossary") as string }, }, }, ], @@ -132,38 +124,37 @@ export const routes = [ { path: "/interact", name: RouteName.INTERACT, - component: (): Promise => - import(/* webpackChunkName: "interact" */ "@/views/Interact.vue"), + component: (): Promise => import("@/views/InteractView.vue"), meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Interact") as string }, + announcer: { message: (): string => t("Interact") as string }, }, }, { path: "/auth/:provider/callback", name: "auth-callback", - component: (): Promise => + component: (): Promise => import( /* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue" ), meta: { announcer: { - message: (): string => i18n.t("Redirecting to Mobilizon") as string, + message: (): string => t("Redirecting to Mobilizon") as string, }, }, }, { path: "/welcome/:step?", name: RouteName.WELCOME_SCREEN, - component: (): Promise => + component: (): Promise => import( /* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue" ), meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("First steps") as string }, + announcer: { message: (): string => t("First steps") as string }, }, - props: (route: Route): Record => { + props: (route: any): Record => { const step = Number.parseInt(route.params.step, 10); if (Number.isNaN(step)) { return { step: 1 }; @@ -174,13 +165,13 @@ export const routes = [ { path: "/404", name: RouteName.PAGE_NOT_FOUND, - component: (): Promise => + component: (): Promise => import( /* webpackChunkName: "PageNotFound" */ "../views/PageNotFound.vue" ), meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Page not found") as string }, + announcer: { message: (): string => t("Page not found") as string }, }, }, ]; @@ -188,36 +179,31 @@ export const routes = [ for (const locale of AVAILABLE_LANGUAGES) { routes.push({ path: `/${locale}`, - component: (): Promise => - import( - /* webpackChunkName: "HomepageRedirectComponent" */ "../components/Utils/HomepageRedirectComponent.vue" - ), + component: () => + import("../components/Utils/HomepageRedirectComponent.vue"), }); } routes.push({ - path: "*", + path: "/:pathMatch(.*)*", redirect: { name: RouteName.PAGE_NOT_FOUND }, }); -const router = new Router({ +export const router = createRouter({ scrollBehavior, - mode: "history", - base: "/", + history: createWebHistory("/"), routes, }); router.beforeEach(authGuardIfNeeded); -router.afterEach(() => { - try { - if (router.app.$children[0]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - router.app.$children[0].error = null; - } - } catch (e) { - console.error(e); - } -}); - -export default router; +// router.afterEach(() => { +// try { +// if (router.app.$children[0]) { +// // eslint-disable-next-line @typescript-eslint/ban-ts-comment +// // @ts-ignore +// router.app.$children[0].error = null; +// } +// } catch (e) { +// console.error(e); +// } +// }); diff --git a/js/src/router/name.ts b/js/src/router/name.ts index 37c3f8c6e..5c3de3422 100644 --- a/js/src/router/name.ts +++ b/js/src/router/name.ts @@ -7,11 +7,12 @@ import { DiscussionRouteName } from "./discussion"; import { UserRouteName } from "./user"; enum GlobalRouteName { - HOME = "Home", - ABOUT = "About", + HOME = "HOME", + ABOUT = "ABOUT", + CATEGORIES = "CATEGORIES", ABOUT_INSTANCE = "ABOUT_INSTANCE", PAGE_NOT_FOUND = "PageNotFound", - SEARCH = "Search", + SEARCH = "SEARCH", TERMS = "TERMS", PRIVACY = "PRIVACY", GLOSSARY = "GLOSSARY", diff --git a/js/src/router/settings.ts b/js/src/router/settings.ts index c71730ba0..8e49755e2 100644 --- a/js/src/router/settings.ts +++ b/js/src/router/settings.ts @@ -1,6 +1,7 @@ -import { Route, RouteConfig } from "vue-router"; -import { ImportedComponent } from "vue/types/options"; import { i18n } from "@/utils/i18n"; +import { RouteLocationNormalized, RouteRecordRaw } from "vue-router"; + +const { t } = i18n.global; export enum SettingsRouteName { SETTINGS = "SETTINGS", @@ -28,11 +29,10 @@ export enum SettingsRouteName { IDENTITIES = "IDENTITIES", } -export const settingsRoutes: RouteConfig[] = [ +export const settingsRoutes: RouteRecordRaw[] = [ { path: "/settings", - component: (): Promise => - import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"), + component: () => import("@/views/SettingsView.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS }, @@ -50,43 +50,37 @@ export const settingsRoutes: RouteConfig[] = [ { path: "account/general", name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL, - component: (): Promise => - import( - /* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue" - ), + component: (): Promise => + import("@/views/Settings/AccountSettings.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Account settings") as string, + message: (): string => t("Account settings") as string, }, }, }, { path: "preferences", name: SettingsRouteName.PREFERENCES, - component: (): Promise => - import( - /* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue" - ), + component: (): Promise => + import("@/views/Settings/PreferencesView.vue"), props: true, meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("Preferences") as string }, + announcer: { message: (): string => t("Preferences") as string }, }, }, { path: "notifications", name: SettingsRouteName.NOTIFICATIONS, - component: (): Promise => - import( - /* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue" - ), + component: (): Promise => + import("@/views/Settings/NotificationsView.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Notifications") as string, + message: (): string => t("Notifications") as string, }, }, }, @@ -99,50 +93,42 @@ export const settingsRoutes: RouteConfig[] = [ { path: "admin/dashboard", name: SettingsRouteName.ADMIN_DASHBOARD, - component: (): Promise => - import( - /* webpackChunkName: "Dashboard" */ "@/views/Admin/Dashboard.vue" - ), + component: (): Promise => + import("@/views/Admin/DashboardView.vue"), meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Admin dashboard") as string, + message: (): string => t("Admin dashboard") as string, }, }, }, { path: "admin/settings", name: SettingsRouteName.ADMIN_SETTINGS, - component: (): Promise => - import( - /* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue" - ), + component: (): Promise => import("@/views/Admin/SettingsView.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Admin settings") as string, + message: (): string => t("Admin settings") as string, }, }, }, { path: "admin/users", name: SettingsRouteName.USERS, - component: (): Promise => - import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"), + component: (): Promise => import("@/views/Admin/UsersView.vue"), props: true, meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("Users") as string }, + announcer: { message: (): string => t("Users") as string }, }, }, { path: "admin/users/:id", name: SettingsRouteName.ADMIN_USER_PROFILE, - component: (): Promise => - import( - /* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue" - ), + component: (): Promise => + import("@/views/Admin/AdminUserProfile.vue"), props: true, meta: { requiredAuth: true, @@ -152,62 +138,50 @@ export const settingsRoutes: RouteConfig[] = [ { path: "admin/profiles", name: SettingsRouteName.PROFILES, - component: (): Promise => - import( - /* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue" - ), + component: (): Promise => import("@/views/Admin/ProfilesView.vue"), props: true, meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("Profiles") as string }, + announcer: { message: (): string => t("Profiles") as string }, }, }, { path: "admin/profiles/:id", name: SettingsRouteName.ADMIN_PROFILE, - component: (): Promise => - import( - /* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue" - ), + component: (): Promise => import("@/views/Admin/AdminProfile.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, }, { path: "admin/groups", name: SettingsRouteName.ADMIN_GROUPS, - component: (): Promise => - import( - /* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue" - ), + component: (): Promise => + import("@/views/Admin/GroupProfiles.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Group profiles") as string, + message: (): string => t("Group profiles") as string, }, }, }, { path: "admin/groups/:id", name: SettingsRouteName.ADMIN_GROUP_PROFILE, - component: (): Promise => - import( - /* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue" - ), + component: (): Promise => + import("@/views/Admin/AdminGroupProfile.vue"), props: true, meta: { requiredAuth: true, announcer: { skip: true } }, }, { path: "admin/instances", name: SettingsRouteName.INSTANCES, - component: (): Promise => - import( - /* webpackChunkName: "Instances" */ "@/views/Admin/Instances.vue" - ), + component: (): Promise => + import("@/views/Admin/InstancesView.vue"), meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Instances") as string, + message: (): string => t("Instances") as string, }, }, props: true, @@ -215,15 +189,12 @@ export const settingsRoutes: RouteConfig[] = [ { path: "admin/instances/:domain", name: SettingsRouteName.INSTANCE, - component: (): Promise => - import( - /* webpackChunkName: "Instance" */ "@/views/Admin/Instance.vue" - ), + component: (): Promise => import("@/views/Admin/InstanceView.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Instance") as string, + message: (): string => t("Instance") as string, }, }, }, @@ -236,43 +207,37 @@ export const settingsRoutes: RouteConfig[] = [ { path: "/moderation/reports", name: SettingsRouteName.REPORTS, - component: (): Promise => - import( - /* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue" - ), + component: (): Promise => + import("@/views/Moderation/ReportListView.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Reports list") as string, + message: (): string => t("Reports list") as string, }, }, }, { path: "/moderation/report/:reportId", name: SettingsRouteName.REPORT, - component: (): Promise => - import( - /* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue" - ), + component: (): Promise => + import("@/views/Moderation/ReportView.vue"), props: true, meta: { requiredAuth: true, - announcer: { message: (): string => i18n.t("Report") as string }, + announcer: { message: (): string => t("Report") as string }, }, }, { path: "/moderation/logs", name: SettingsRouteName.REPORT_LOGS, - component: (): Promise => - import( - /* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue" - ), + component: (): Promise => + import("@/views/Moderation/LogsView.vue"), props: true, meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Moderation logs") as string, + message: (): string => t("Moderation logs") as string, }, }, }, @@ -285,29 +250,25 @@ export const settingsRoutes: RouteConfig[] = [ { path: "/identity/create", name: SettingsRouteName.CREATE_IDENTITY, - component: (): Promise => - import( - /* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue" - ), - props: (route: Route): Record => ({ + component: (): Promise => + import("@/views/Account/children/EditIdentity.vue"), + props: (route: RouteLocationNormalized): Record => ({ identityName: route.params.identityName, isUpdate: false, }), meta: { requiredAuth: true, announcer: { - message: (): string => i18n.t("Create identity") as string, + message: (): string => t("Create identity") as string, }, }, }, { path: "/identity/update/:identityName?", name: SettingsRouteName.UPDATE_IDENTITY, - component: (): Promise => - import( - /* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue" - ), - props: (route: Route): Record => ({ + component: (): Promise => + import("@/views/Account/children/EditIdentity.vue"), + props: (route: RouteLocationNormalized): Record => ({ identityName: route.params.identityName, isUpdate: true, }), diff --git a/js/src/router/user.ts b/js/src/router/user.ts index 2089df9a7..978cb9c83 100644 --- a/js/src/router/user.ts +++ b/js/src/router/user.ts @@ -1,8 +1,9 @@ import { beforeRegisterGuard } from "@/router/guards/register-guard"; -import { Route, RouteConfig } from "vue-router"; -import { ImportedComponent } from "vue/types/options"; +import { RouteLocationNormalized, RouteRecordRaw } from "vue-router"; import { i18n } from "@/utils/i18n"; +const t = i18n.global.t; + export enum UserRouteName { REGISTER = "Register", REGISTER_PROFILE = "RegisterProfile", @@ -14,116 +15,97 @@ export enum UserRouteName { LOGIN = "Login", } -export const userRoutes: RouteConfig[] = [ +export const userRoutes: RouteRecordRaw[] = [ { path: "/register/user", name: UserRouteName.REGISTER, - component: (): Promise => - import( - /* webpackChunkName: "RegisterUser" */ "@/views/User/Register.vue" - ), + component: (): Promise => import("@/views/User/RegisterView.vue"), props: true, meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Register") as string }, + announcer: { message: (): string => t("Register") as string }, }, beforeEnter: beforeRegisterGuard, }, { path: "/register/profile", name: UserRouteName.REGISTER_PROFILE, - component: (): Promise => - import( - /* webpackChunkName: "RegisterProfile" */ "@/views/Account/Register.vue" - ), + component: (): Promise => import("@/views/Account/RegisterView.vue"), // We can only pass string values through params, therefore - props: (route: Route): Record => ({ + props: (route: RouteLocationNormalized): Record => ({ email: route.params.email, userAlreadyActivated: route.params.userAlreadyActivated === "true", }), meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Register") as string }, + announcer: { message: (): string => t("Register") as string }, }, }, { path: "/resend-instructions", name: UserRouteName.RESEND_CONFIRMATION, - component: (): Promise => - import( - /* webpackChunkName: "ResendConfirmation" */ "@/views/User/ResendConfirmation.vue" - ), + component: (): Promise => + import("@/views/User/ResendConfirmation.vue"), props: true, meta: { requiresAuth: false, announcer: { - message: (): string => i18n.t("Resent confirmation email") as string, + message: (): string => t("Resent confirmation email") as string, }, }, }, { path: "/password-reset/send", name: UserRouteName.SEND_PASSWORD_RESET, - component: (): Promise => - import( - /* webpackChunkName: "SendPasswordReset" */ "@/views/User/SendPasswordReset.vue" - ), + component: (): Promise => import("@/views/User/SendPasswordReset.vue"), props: true, meta: { requiresAuth: false, announcer: { - message: (): string => i18n.t("Send password reset") as string, + message: (): string => t("Send password reset") as string, }, }, }, { path: "/password-reset/:token", name: UserRouteName.PASSWORD_RESET, - component: (): Promise => - import( - /* webpackChunkName: "PasswordReset" */ "@/views/User/PasswordReset.vue" - ), + component: (): Promise => import("@/views/User/PasswordReset.vue"), meta: { requiresAuth: false, - announcer: { message: (): string => i18n.t("Password reset") as string }, + announcer: { message: (): string => t("Password reset") as string }, }, props: true, }, { path: "/validate/email/:token", name: UserRouteName.EMAIL_VALIDATE, - component: (): Promise => - import( - /* webpackChunkName: "EmailValidate" */ "@/views/User/EmailValidate.vue" - ), + component: (): Promise => import("@/views/User/EmailValidate.vue"), props: true, meta: { requiresAuth: false, - announcer: { message: (): string => i18n.t("Email validate") as string }, + announcer: { message: (): string => t("Email validate") as string }, }, }, { path: "/validate/:token", name: UserRouteName.VALIDATE, - component: (): Promise => - import(/* webpackChunkName: "Validate" */ "@/views/User/Validate.vue"), + component: (): Promise => import("@/views/User/ValidateUser.vue"), props: true, meta: { requiresAuth: false, announcer: { - message: (): string => i18n.t("Validating account") as string, + message: (): string => t("Validating account") as string, }, }, }, { path: "/login", name: UserRouteName.LOGIN, - component: (): Promise => - import(/* webpackChunkName: "Login" */ "@/views/User/Login.vue"), + component: (): Promise => import("@/views/User/LoginView.vue"), props: true, meta: { requiredAuth: false, - announcer: { message: (): string => i18n.t("Login") as string }, + announcer: { message: (): string => t("Login") as string }, }, }, ]; diff --git a/js/src/service-worker.ts b/js/src/service-worker.ts index af230659d..c7aee7a22 100644 --- a/js/src/service-worker.ts +++ b/js/src/service-worker.ts @@ -104,7 +104,7 @@ async function isClientFocused(): Promise { self.addEventListener("push", async (event: PushEvent) => { if (!event.data) return; const payload = event.data.json(); - console.log("received push", payload); + console.debug("received push", payload); const options = { body: payload.body, icon: "/img/icons/android-chrome-512x512.png", @@ -157,7 +157,7 @@ self.addEventListener("message", (event: ExtendableMessageEvent) => { const replyPort = event.ports[0]; const message = event.data; if (replyPort && message && message.type === "skip-waiting") { - console.log("doing skip waiting"); + console.debug("doing skip waiting"); event.waitUntil( self.skipWaiting().then( () => replyPort.postMessage({ error: null }), diff --git a/js/src/services/AnonymousParticipationStorage.ts b/js/src/services/AnonymousParticipationStorage.ts index ab03f3a68..53a49d482 100644 --- a/js/src/services/AnonymousParticipationStorage.ts +++ b/js/src/services/AnonymousParticipationStorage.ts @@ -73,7 +73,7 @@ function insertLocalAnonymousParticipation( } function buildExpiration(event: IEvent): Date { - const expiration = event.endsOn || event.beginsOn; + const expiration = new Date(event.endsOn ?? event.beginsOn); expiration.setMonth(expiration.getMonth() + 1); return expiration; } diff --git a/js/src/services/EventMetadata.ts b/js/src/services/EventMetadata.ts index efe6f0ae5..53ab340d6 100644 --- a/js/src/services/EventMetadata.ts +++ b/js/src/services/EventMetadata.ts @@ -6,63 +6,61 @@ import { import { IEventMetadataDescription } from "@/types/event-metadata"; import { i18n } from "@/utils/i18n"; +const t = i18n.global.t; + export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "wheelchair-accessibility", key: "mz:accessibility:wheelchairAccessible", - label: i18n.t("Wheelchair accessibility") as string, - description: i18n.t( + label: t("Wheelchair accessibility") as string, + description: t( "Whether the event is accessible with a wheelchair" ) as string, value: "no", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.CHOICE, choices: { - no: i18n.t("Not accessible with a wheelchair") as string, - partially: i18n.t("Partially accessible with a wheelchair") as string, - fully: i18n.t("Fully accessible with a wheelchair") as string, + no: t("Not accessible with a wheelchair") as string, + partially: t("Partially accessible with a wheelchair") as string, + fully: t("Fully accessible with a wheelchair") as string, }, category: EventMetadataCategories.ACCESSIBILITY, }, { icon: "subtitles", key: "mz:accessibility:live:subtitle", - label: i18n.t("Subtitles") as string, - description: i18n.t("Whether the event live video is subtitled") as string, + label: t("Subtitles") as string, + description: t("Whether the event live video is subtitled") as string, value: "false", type: EventMetadataType.BOOLEAN, keyType: EventMetadataKeyType.PLAIN, choices: { - true: i18n.t("The event live video contains subtitles") as string, - false: i18n.t( - "The event live video does not contain subtitles" - ) as string, + true: t("The event live video contains subtitles") as string, + false: t("The event live video does not contain subtitles") as string, }, category: EventMetadataCategories.ACCESSIBILITY, }, { icon: "mz:icon:sign_language", key: "mz:accessibility:live:sign_language", - label: i18n.t("Sign Language") as string, - description: i18n.t( + label: t("Sign Language") as string, + description: t( "Whether the event is interpreted in sign language" ) as string, value: "false", type: EventMetadataType.BOOLEAN, keyType: EventMetadataKeyType.PLAIN, choices: { - true: i18n.t("The event has a sign language interpreter") as string, - false: i18n.t( - "The event hasn't got a sign language interpreter" - ) as string, + true: t("The event has a sign language interpreter") as string, + false: t("The event hasn't got a sign language interpreter") as string, }, category: EventMetadataCategories.ACCESSIBILITY, }, { icon: "youtube", key: "mz:replay:youtube:url", - label: i18n.t("YouTube replay") as string, - description: i18n.t( + label: t("YouTube replay") as string, + description: t( "The URL where the event live can be watched again after it has ended" ) as string, value: "", @@ -75,8 +73,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ // { // icon: "twitch", // key: "mz:replay:twitch:url", - // label: i18n.t("Twitch replay") as string, - // description: i18n.t( + // label: t("Twitch replay") as string, + // description: t( // "The URL where the event live can be watched again after it has ended" // ) as string, // value: "", @@ -85,8 +83,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "mz:icon:peertube", key: "mz:replay:peertube:url", - label: i18n.t("PeerTube replay") as string, - description: i18n.t( + label: t("PeerTube replay") as string, + description: t( "The URL where the event live can be watched again after it has ended" ) as string, value: "", @@ -98,10 +96,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "mz:icon:peertube", key: "mz:live:peertube:url", - label: i18n.t("PeerTube live") as string, - description: i18n.t( - "The URL where the event can be watched live" - ) as string, + label: t("PeerTube live") as string, + description: t("The URL where the event can be watched live") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -111,10 +107,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "twitch", key: "mz:live:twitch:url", - label: i18n.t("Twitch live") as string, - description: i18n.t( - "The URL where the event can be watched live" - ) as string, + label: t("Twitch live") as string, + description: t("The URL where the event can be watched live") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -125,10 +119,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "youtube", key: "mz:live:youtube:url", - label: i18n.t("YouTube live") as string, - description: i18n.t( - "The URL where the event can be watched live" - ) as string, + label: t("YouTube live") as string, + description: t("The URL where the event can be watched live") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -139,10 +131,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "mz:icon:owncast", key: "mz:live:owncast:url", - label: i18n.t("Owncast") as string, - description: i18n.t( - "The URL where the event can be watched live" - ) as string, + label: t("Owncast") as string, + description: t("The URL where the event can be watched live") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -152,8 +142,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "calendar-check", key: "mz:poll:framadate:url", - label: i18n.t("Framadate poll") as string, - description: i18n.t( + label: t("Framadate poll") as string, + description: t( "The URL of a poll where the choice for the event date is happening" ) as string, value: "", @@ -165,12 +155,12 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "file-document-edit", key: "mz:notes:etherpad:url", - label: i18n.t("Etherpad notes") as string, - description: i18n.t( + label: t("Etherpad notes") as string, + description: t( "The URL of a pad where notes are being taken collaboratively" ) as string, value: "", - placeholder: i18n.t( + placeholder: t( "https://mensuel.framapad.org/p/some-secret-token" ) as string, type: EventMetadataType.STRING, @@ -180,8 +170,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "twitter", key: "mz:social:twitter:account", - label: i18n.t("Twitter account") as string, - description: i18n.t( + label: t("Twitter account") as string, + description: t( "A twitter account handle to follow for event updates" ) as string, value: "", @@ -193,8 +183,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "mz:icon:fediverse", key: "mz:social:fediverse:account_url", - label: i18n.t("Fediverse account") as string, - description: i18n.t( + label: t("Fediverse account") as string, + description: t( "A fediverse account URL to follow for event updates" ) as string, value: "", @@ -206,8 +196,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "ticket-confirmation", key: "mz:ticket:external_url", - label: i18n.t("Online ticketing") as string, - description: i18n.t("An URL to an external ticketing platform") as string, + label: t("Online ticketing") as string, + description: t("An URL to an external ticketing platform") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -216,10 +206,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "cash", key: "mz:ticket:price_url", - label: i18n.t("Price sheet") as string, - description: i18n.t( - "A link to a page presenting the price options" - ) as string, + label: t("Price sheet") as string, + description: t("A link to a page presenting the price options") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -228,10 +216,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "calendar-text", key: "mz:schedule_url", - label: i18n.t("Schedule") as string, - description: i18n.t( - "A link to a page presenting the event schedule" - ) as string, + label: t("Schedule") as string, + description: t("A link to a page presenting the event schedule") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -240,8 +226,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "webcam", key: "mz:visio:jitsi_meet", - label: i18n.t("Jitsi Meet") as string, - description: i18n.t("The Jitsi Meet video teleconference URL") as string, + label: t("Jitsi Meet") as string, + description: t("The Jitsi Meet video teleconference URL") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -251,8 +237,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "webcam", key: "mz:visio:zoom", - label: i18n.t("Zoom") as string, - description: i18n.t("The Zoom video teleconference URL") as string, + label: t("Zoom") as string, + description: t("The Zoom video teleconference URL") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -262,10 +248,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "microsoft-teams", key: "mz:visio:microsoft_teams", - label: i18n.t("Microsoft Teams") as string, - description: i18n.t( - "The Microsoft Teams video teleconference URL" - ) as string, + label: t("Microsoft Teams") as string, + description: t("The Microsoft Teams video teleconference URL") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -275,8 +259,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "google-hangouts", key: "mz:visio:google_meet", - label: i18n.t("Google Meet") as string, - description: i18n.t("The Google Meet video teleconference URL") as string, + label: t("Google Meet") as string, + description: t("The Google Meet video teleconference URL") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, @@ -286,10 +270,8 @@ export const eventMetaDataList: IEventMetadataDescription[] = [ { icon: "webcam", key: "mz:visio:big_blue_button", - label: i18n.t("Big Blue Button") as string, - description: i18n.t( - "The Big Blue Button video teleconference URL" - ) as string, + label: t("Big Blue Button") as string, + description: t("The Big Blue Button video teleconference URL") as string, value: "", type: EventMetadataType.STRING, keyType: EventMetadataKeyType.URL, diff --git a/js/src/services/push-subscription.ts b/js/src/services/push-subscription.ts index ef4734b6a..0abb0d3c4 100644 --- a/js/src/services/push-subscription.ts +++ b/js/src/services/push-subscription.ts @@ -1,6 +1,5 @@ -import apolloProvider from "@/vue-apollo"; -import { NormalizedCacheObject } from "@apollo/client/cache/inmemory/types"; -import { ApolloClient } from "@apollo/client/core/ApolloClient"; +import { apolloClient } from "@/vue-apollo"; +import { provideApolloClient, useQuery } from "@vue/apollo-composable"; import { WEB_PUSH } from "../graphql/config"; import { IConfig } from "../types/config.model"; @@ -18,44 +17,45 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array { } export async function subscribeUserToPush(): Promise { - const client = - apolloProvider.defaultClient as ApolloClient; + const { onResult } = provideApolloClient(apolloClient)(() => + useQuery<{ config: IConfig }>(WEB_PUSH) + ); - const registration = await navigator.serviceWorker.ready; - const { data } = await client.query<{ config: IConfig }>({ - query: WEB_PUSH, + return new Promise((resolve, reject) => { + onResult(async ({ data }) => { + if (data?.config?.webPush?.enabled && data?.config?.webPush?.publicKey) { + const subscribeOptions = { + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array( + data?.config?.webPush?.publicKey + ), + }; + const registration = await navigator.serviceWorker.ready; + try { + const pushSubscription = await registration.pushManager.subscribe( + subscribeOptions + ); + console.debug("Received PushSubscription: ", pushSubscription); + resolve(pushSubscription); + } catch (e) { + console.error("Error while subscribing to push notifications", e); + } + } + reject(null); + }); }); - - if (data?.config?.webPush?.enabled && data?.config?.webPush?.publicKey) { - const subscribeOptions = { - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array( - data?.config?.webPush?.publicKey - ), - }; - try { - const pushSubscription = await registration.pushManager.subscribe( - subscribeOptions - ); - console.debug("Received PushSubscription: ", pushSubscription); - return pushSubscription; - } catch (e) { - console.error("Error while subscribing to push notifications", e); - } - } - return null; } export async function unsubscribeUserToPush(): Promise { - console.log("performing unsubscribeUserToPush"); + console.debug("performing unsubscribeUserToPush"); const registration = await navigator.serviceWorker.ready; - console.log("found registration", registration); + console.debug("found registration", registration); const subscription = await registration.pushManager?.getSubscription(); - console.log("found subscription", subscription); + console.debug("found subscription", subscription); if (subscription && (await subscription?.unsubscribe()) === true) { - console.log("done unsubscription"); + console.debug("done unsubscription"); return subscription?.endpoint; } - console.log("went wrong"); + console.debug("went wrong"); return undefined; } diff --git a/js/src/services/statistics/index.ts b/js/src/services/statistics/index.ts index c7e3a1db0..b4c051ccd 100644 --- a/js/src/services/statistics/index.ts +++ b/js/src/services/statistics/index.ts @@ -1,29 +1,34 @@ -import { - IAnalyticsConfig, - IConfig, - IKeyValueConfig, -} from "@/types/config.model"; +import { IAnalyticsConfig, IKeyValueConfig } from "@/types/config.model"; -export const statistics = async (config: IConfig, environement: any) => { - console.debug("Loading statistics", config.analytics); - const matomoConfig = checkProviderConfig(config, "matomo"); +let app: any = null; + +export const setAppForAnalytics = (newApp: any) => { + app = newApp; +}; + +export const statistics = async ( + configAnalytics: IAnalyticsConfig[], + environement: any +) => { + console.debug("Loading statistics", configAnalytics); + const matomoConfig = checkProviderConfig(configAnalytics, "matomo"); if (matomoConfig?.enabled === true) { const { matomo } = (await import("./matomo")) as any; - matomo(environement, convertConfig(matomoConfig.configuration)); + matomo({ ...environement, app }, convertConfig(matomoConfig.configuration)); } - const sentryConfig = checkProviderConfig(config, "sentry"); + const sentryConfig = checkProviderConfig(configAnalytics, "sentry"); if (sentryConfig?.enabled === true) { const { sentry } = (await import("./sentry")) as any; - sentry(environement, convertConfig(sentryConfig.configuration)); + sentry({ ...environement, app }, convertConfig(sentryConfig.configuration)); } }; export const checkProviderConfig = ( - config: IConfig, + configAnalytics: IAnalyticsConfig[], providerName: string ): IAnalyticsConfig | undefined => { - return config?.analytics?.find((provider) => provider.id === providerName); + return configAnalytics?.find((provider) => provider.id === providerName); }; export const convertConfig = ( diff --git a/js/src/services/statistics/matomo.ts b/js/src/services/statistics/matomo.ts index 7b636f96d..92385376c 100644 --- a/js/src/services/statistics/matomo.ts +++ b/js/src/services/statistics/matomo.ts @@ -1,4 +1,3 @@ -import Vue from "vue"; import VueMatomo from "vue-matomo"; export const matomo = (environment: any, matomoConfiguration: any) => { @@ -7,8 +6,9 @@ export const matomo = (environment: any, matomoConfiguration: any) => { "Calling VueMatomo with the following configuration", matomoConfiguration ); - Vue.use(VueMatomo, { + environment.app.use(VueMatomo, { ...matomoConfiguration, router: environment.router, + debug: import.meta.env.DEV, }); }; diff --git a/js/src/services/statistics/plausible.ts b/js/src/services/statistics/plausible.ts index 51712fdc9..fed349851 100644 --- a/js/src/services/statistics/plausible.ts +++ b/js/src/services/statistics/plausible.ts @@ -1,10 +1,8 @@ -import VueRouter from "vue-router"; -import Vue from "vue"; import { VuePlausible } from "vue-plausible"; -export default (router: VueRouter, plausibleConfiguration: any) => { +export default (environment: any, plausibleConfiguration: any) => { console.debug("Loading Plausible statistics"); - Vue.use(VuePlausible, { + environment.app.use(VuePlausible, { // see configuration section ...plausibleConfiguration, }); diff --git a/js/src/services/statistics/sentry.ts b/js/src/services/statistics/sentry.ts index ff165a304..2dbc4602d 100644 --- a/js/src/services/statistics/sentry.ts +++ b/js/src/services/statistics/sentry.ts @@ -1,5 +1,3 @@ -import Vue from "vue"; - import * as Sentry from "@sentry/vue"; import { Integrations } from "@sentry/tracing"; @@ -12,14 +10,15 @@ export const sentry = (environment: any, sentryConfiguration: any) => { // Don't attach errors to previous events window.sessionStorage.removeItem("lastEventId"); Sentry.init({ - Vue, + app: environment.app, dsn: sentryConfiguration.dsn, + debug: import.meta.env.DEV, integrations: [ new Integrations.BrowserTracing({ routingInstrumentation: Sentry.vueRouterInstrumentation( environment.router ), - tracingOrigins: ["localhost", "mobilizon1.com", /^\//], + tracingOrigins: [window.origin, /^\//], }), ], beforeSend(event) { @@ -33,8 +32,9 @@ export const sentry = (environment: any, sentryConfiguration: any) => { // Set tracesSampleRate to 1.0 to capture 100% // of transactions for performance monitoring. // We recommend adjusting this value in production - tracesSampleRate: sentryConfiguration.tracesSampleRate, + tracesSampleRate: Number.parseFloat(sentryConfiguration.tracesSampleRate), release: environment.version, + logErrors: true, }); }; diff --git a/js/src/shims-vue.d.ts b/js/src/shims-vue.d.ts index 19bacf77f..f94bf3282 100644 --- a/js/src/shims-vue.d.ts +++ b/js/src/shims-vue.d.ts @@ -2,7 +2,7 @@ declare module "*.vue" { import type { DefineComponent } from "vue"; // eslint-disable-next-line @typescript-eslint/ban-types - const component: DefineComponent<{}, {}, any>; + const component: DefineComponent<{}, {}, {}>; export default component; } @@ -12,3 +12,5 @@ declare module "*.svg" { const content: VueConstructor; export default content; } + +declare module "@vue-leaflet/vue-leaflet"; diff --git a/js/src/styles/_event-card.scss b/js/src/styles/_event-card.scss deleted file mode 100644 index 4854de21b..000000000 --- a/js/src/styles/_event-card.scss +++ /dev/null @@ -1,20 +0,0 @@ -@use "@/styles/_mixins" as *; - -.event-organizer { - display: flex; - align-items: center; - - .organizer-name { - @include padding-left(5px); - font-weight: 600; - } -} - -.event-subtitle { - display: flex; - align-items: center; - - & > span:not(.icon) { - @include padding-left(5px); - } -} diff --git a/js/src/styles/vue-skip-to.scss b/js/src/styles/vue-skip-to.scss index fdb62d8f2..222be9eda 100644 --- a/js/src/styles/vue-skip-to.scss +++ b/js/src/styles/vue-skip-to.scss @@ -28,8 +28,6 @@ clip: auto; height: auto; width: auto; - background-color: $white; - border: 2px solid $violet-3; } &, @@ -41,12 +39,10 @@ &__link { display: block; padding: 8px 16px; - color: $violet-3; font-size: 18px; } &__nav > span { - border-bottom: 2px solid $violet-3; font-weight: bold; } @@ -57,7 +53,6 @@ &__link:focus { outline: none; - background-color: $violet-3; color: #f2f2f2; } } diff --git a/js/src/types/actor/actor.model.ts b/js/src/types/actor/actor.model.ts index 984c7f33c..088fd0da6 100644 --- a/js/src/types/actor/actor.model.ts +++ b/js/src/types/actor/actor.model.ts @@ -6,7 +6,7 @@ export interface IActor { url: string; name: string; domain: string | null; - mediaSize: number; + mediaSize?: number; summary: string; preferredUsername: string; suspended: boolean; @@ -56,7 +56,10 @@ export class Actor implements IActor { } } -export function usernameWithDomain(actor: IActor, force = false): string { +export function usernameWithDomain( + actor: IActor | undefined, + force = false +): string { if (!actor) return ""; if (actor?.domain) { return `${actor.preferredUsername}@${actor.domain}`; @@ -67,7 +70,7 @@ export function usernameWithDomain(actor: IActor, force = false): string { return actor.preferredUsername; } -export function displayName(actor: IActor): string { +export function displayName(actor: IActor | undefined): string { return actor && actor.name != null && actor.name !== "" ? actor.name : usernameWithDomain(actor); diff --git a/js/src/types/actor/group.model.ts b/js/src/types/actor/group.model.ts index 6f6a2b073..26e20449a 100644 --- a/js/src/types/actor/group.model.ts +++ b/js/src/types/actor/group.model.ts @@ -11,6 +11,7 @@ import { ActorType, GroupVisibility, Openness } from "../enums"; import type { IMember } from "./member.model"; import type { ITodoList } from "../todolist"; import { IActivity } from "../activity.model"; +import { IFollower } from "./follower.model"; export interface IGroup extends IActor { members: Paginate; @@ -24,10 +25,14 @@ export interface IGroup extends IActor { visibility: GroupVisibility; manuallyApprovesFollowers: boolean; activity: Paginate; + followers: Paginate; + membersCount?: number; + followersCount?: number; } export class Group extends Actor implements IGroup { members: Paginate = { elements: [], total: 0 }; + followers: Paginate = { elements: [], total: 0 }; resources: Paginate = { elements: [], total: 0 }; diff --git a/js/src/types/actor/person.model.ts b/js/src/types/actor/person.model.ts index dc8451a42..c0a0ee215 100644 --- a/js/src/types/actor/person.model.ts +++ b/js/src/types/actor/person.model.ts @@ -10,11 +10,12 @@ import { IFollower } from "./follower.model"; export interface IPerson extends IActor { feedTokens: IFeedToken[]; - goingToEvents: IEvent[]; - participations: Paginate; - memberships: Paginate; - follows: Paginate; + goingToEvents?: IEvent[]; + participations?: Paginate; + memberships?: Paginate; + follows?: Paginate; user?: ICurrentUser; + organizedEvents?: Paginate; } export class Person extends Actor implements IPerson { @@ -26,6 +27,8 @@ export class Person extends Actor implements IPerson { memberships!: Paginate; + organizedEvents!: Paginate; + user!: ICurrentUser; constructor(hash: IPerson | Record = {}) { diff --git a/js/src/types/address.model.ts b/js/src/types/address.model.ts index e75d18f2f..4de385f12 100644 --- a/js/src/types/address.model.ts +++ b/js/src/types/address.model.ts @@ -1,5 +1,6 @@ import { poiIcons } from "@/utils/poiIcons"; import type { IPOIIcon } from "@/utils/poiIcons"; +import { PictureInformation } from "./picture"; export interface IAddress { id?: string; @@ -14,6 +15,8 @@ export interface IAddress { url?: string; originId?: string; timezone?: string; + pictureInfo?: PictureInformation; + poiInfos?: IPoiInfo; } export interface IPoiInfo { diff --git a/js/src/types/admin.model.ts b/js/src/types/admin.model.ts index 00d165428..5c8d62a6f 100644 --- a/js/src/types/admin.model.ts +++ b/js/src/types/admin.model.ts @@ -1,6 +1,6 @@ import type { IEvent } from "@/types/event.model"; import type { IGroup } from "./actor"; -import { InstanceTermsType } from "./enums"; +import { InstancePrivacyType, InstanceTermsType } from "./enums"; export interface IDashboard { lastPublicEventPublished: IEvent; @@ -29,7 +29,7 @@ export interface IAdminSettings { instanceTermsType: InstanceTermsType; instanceTermsUrl: string | null; instancePrivacyPolicy: string; - instancePrivacyPolicyType: InstanceTermsType; + instancePrivacyPolicyType: InstancePrivacyType; instancePrivacyPolicyUrl: string | null; instanceRules: string; registrationsOpen: boolean; diff --git a/js/src/types/apollo.ts b/js/src/types/apollo.ts index 244b56005..d0dd85e0f 100644 --- a/js/src/types/apollo.ts +++ b/js/src/types/apollo.ts @@ -3,3 +3,7 @@ import { GraphQLError } from "graphql/error/GraphQLError"; export class AbsintheGraphQLError extends GraphQLError { readonly field: string | undefined; } + +export type TypeNamed> = T & { + __typename: string; +}; diff --git a/js/src/types/comment.model.ts b/js/src/types/comment.model.ts index 5776bb9bc..80cc3366d 100644 --- a/js/src/types/comment.model.ts +++ b/js/src/types/comment.model.ts @@ -1,5 +1,4 @@ -import { Actor } from "@/types/actor"; -import type { IActor } from "@/types/actor"; +import { IPerson, Person } from "@/types/actor"; import type { IEvent } from "@/types/event.model"; import { EventModel } from "@/types/event.model"; @@ -9,21 +8,22 @@ export interface IComment { url?: string; text: string; local: boolean; - actor: IActor | null; + actor: IPerson | null; inReplyToComment?: IComment; originComment?: IComment; replies: IComment[]; event?: IEvent; - updatedAt?: Date | string; - deletedAt?: Date | string; + updatedAt?: string; + deletedAt?: string; totalReplies: number; - insertedAt?: Date | string; - publishedAt?: Date | string; + insertedAt?: string; + publishedAt?: string; isAnnouncement: boolean; + language?: string; } export class CommentModel implements IComment { - actor: IActor = new Actor(); + actor: IPerson = new Person(); id?: string; @@ -43,11 +43,11 @@ export class CommentModel implements IComment { event?: IEvent = undefined; - updatedAt?: Date | string = undefined; + updatedAt?: string = undefined; - deletedAt?: Date | string = undefined; + deletedAt?: string = undefined; - insertedAt?: Date | string = undefined; + insertedAt?: string = undefined; totalReplies = 0; @@ -62,12 +62,12 @@ export class CommentModel implements IComment { this.text = hash.text; this.inReplyToComment = hash.inReplyToComment; this.originComment = hash.originComment; - this.actor = hash.actor ? new Actor(hash.actor) : new Actor(); + this.actor = hash.actor ? new Person(hash.actor) : new Person(); this.event = new EventModel(hash.event); this.replies = hash.replies; - this.updatedAt = new Date(hash.updatedAt as string); + this.updatedAt = new Date(hash.updatedAt as string).toISOString(); this.deletedAt = hash.deletedAt; - this.insertedAt = new Date(hash.insertedAt as string); + this.insertedAt = new Date(hash.insertedAt as string).toISOString(); this.totalReplies = hash.totalReplies; this.isAnnouncement = hash.isAnnouncement; } diff --git a/js/src/types/config.model.ts b/js/src/types/config.model.ts index 97544c2e5..e2ed75994 100644 --- a/js/src/types/config.model.ts +++ b/js/src/types/config.model.ts @@ -3,7 +3,7 @@ import type { IProvider } from "./resource"; export interface IOAuthProvider { id: string; - label: string; + label?: string; } export interface IKeyValueConfig { @@ -18,6 +18,19 @@ export interface IAnalyticsConfig { configuration: IKeyValueConfig[]; } +export interface IAnonymousParticipationConfig { + allowed: boolean; + validation: { + email: { + enabled: boolean; + confirmationRequired: boolean; + }; + captcha: { + enabled: boolean; + }; + }; +} + export interface IConfig { name: string; description: string; @@ -37,18 +50,7 @@ export interface IConfig { // accuracyRadius: number; }; anonymous: { - participation: { - allowed: boolean; - validation: { - email: { - enabled: boolean; - confirmationRequired: boolean; - }; - captcha: { - enabled: boolean; - }; - }; - }; + participation: IAnonymousParticipationConfig; eventCreation: { allowed: boolean; validation: { @@ -122,4 +124,10 @@ export interface IConfig { eventParticipants: string[]; }; analytics: IAnalyticsConfig[]; + search: { + global: { + isEnabled: boolean; + isDefault: boolean; + }; + }; } diff --git a/js/src/types/current-user.model.ts b/js/src/types/current-user.model.ts index 428e36195..f2dda9ad2 100644 --- a/js/src/types/current-user.model.ts +++ b/js/src/types/current-user.model.ts @@ -4,6 +4,9 @@ import type { Paginate } from "./paginate"; import type { IParticipant } from "./participant.model"; import { ICurrentUserRole, INotificationPendingEnum } from "./enums"; import { IFollowedGroupEvent } from "./followedGroupEvent.model"; +import { PictureInformation } from "./picture"; +import { IMember } from "./actor/member.model"; +import { IFeedToken } from "./feedtoken.model"; export interface ICurrentUser { id: string; @@ -19,6 +22,12 @@ export interface IUserPreferredLocation { geohash?: string | null; } +export interface ExtendedIUserPreferredLocation extends IUserPreferredLocation { + lat: number | undefined; + lon: number | undefined; + picture?: PictureInformation; +} + export interface IUserSettings { timezone?: string; notificationOnDay?: boolean; @@ -39,8 +48,8 @@ export interface IActivitySetting { } export interface IUser extends ICurrentUser { - confirmedAt: Date; - confirmationSendAt: Date; + confirmedAt: string; + confirmationSendAt: string; actors: IPerson[]; disabled: boolean; participations: Paginate; @@ -55,4 +64,6 @@ export interface IUser extends ICurrentUser { lastSignInIp: string; currentSignInIp: string; currentSignInAt: string; + memberships: Paginate; + feedTokens: IFeedToken[]; } diff --git a/js/src/types/enums.ts b/js/src/types/enums.ts index 1061d196b..871ee46db 100644 --- a/js/src/types/enums.ts +++ b/js/src/types/enums.ts @@ -130,6 +130,12 @@ export enum SearchTabs { GROUPS = 1, } +export enum ContentType { + ALL = "ALL", + EVENTS = "EVENTS", + GROUPS = "GROUPS", +} + export enum ActorType { PERSON = "PERSON", APPLICATION = "APPLICATION", @@ -280,3 +286,8 @@ export enum InstanceFollowStatus { PENDING = "PENDING", NONE = "NONE", } + +export enum SearchTargets { + INTERNAL = "INTERNAL", + GLOBAL = "GLOBAL", +} diff --git a/js/src/types/errors.model.ts b/js/src/types/errors.model.ts index 1f8dcf7ea..95e8f2a11 100644 --- a/js/src/types/errors.model.ts +++ b/js/src/types/errors.model.ts @@ -4,6 +4,8 @@ import { ExecutionResult, GraphQLError } from "graphql"; export declare class AbsintheGraphQLError extends GraphQLError { field?: string; + code?: string; + status_code?: number; } export declare type AbsintheGraphQLErrors = ReadonlyArray; diff --git a/js/src/types/event-metadata.ts b/js/src/types/event-metadata.ts index 776ed7fd7..586652ff3 100644 --- a/js/src/types/event-metadata.ts +++ b/js/src/types/event-metadata.ts @@ -14,9 +14,9 @@ export interface IEventMetadata { export interface IEventMetadataDescription extends IEventMetadata { icon?: string; placeholder?: string; - description: string; + description?: string; choices?: Record; - keyType: EventMetadataKeyType; + keyType?: EventMetadataKeyType; pattern?: RegExp; label: string; category: EventMetadataCategories; diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index e06703665..11216edc6 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -10,14 +10,15 @@ import type { IParticipant } from "./participant.model"; import { EventOptions } from "./event-options.model"; import type { IEventOptions } from "./event-options.model"; import { EventJoinOptions, EventStatus, EventVisibility } from "./enums"; -import { IEventMetadata } from "./event-metadata"; +import { IEventMetadata, IEventMetadataDescription } from "./event-metadata"; export interface IEventCardOptions { - hideDate: boolean; - loggedPerson: IPerson | boolean; - hideDetails: boolean; - organizerActor: IActor | null; - memberofGroup: boolean; + hideDate?: boolean; + loggedPerson?: IPerson | boolean; + hideDetails?: boolean; + organizerActor?: IActor | null; + isRemoteEvent?: boolean; + isLoggedIn?: boolean; } export interface IEventParticipantStats { @@ -65,9 +66,9 @@ export interface IEvent { title: string; slug: string; description: string; - beginsOn: Date; - endsOn: Date | null; - publishAt: Date; + beginsOn: string; + endsOn: string | null; + publishAt: string; status: EventStatus; visibility: EventVisibility; joinOptions: EventJoinOptions; @@ -89,23 +90,23 @@ export interface IEvent { tags: ITag[]; options: IEventOptions; - metadata: IEventMetadata[]; + metadata: IEventMetadataDescription[]; contacts: IActor[]; language: string; category: string; - toEditJSON(): IEventEditJSON; + toEditJSON?(): IEventEditJSON; } export interface IEditableEvent extends Omit { - beginsOn: Date | null; + beginsOn: string | null; } export class EventModel implements IEvent { id?: string; - beginsOn = new Date(); + beginsOn = new Date().toISOString(); - endsOn: Date | null = new Date(); + endsOn: string | null = new Date().toISOString(); title = ""; @@ -135,7 +136,7 @@ export class EventModel implements IEvent { draft = true; - publishAt = new Date(); + publishAt = new Date().toISOString(); language = "und"; @@ -166,7 +167,7 @@ export class EventModel implements IEvent { options: IEventOptions = new EventOptions(); - metadata: IEventMetadata[] = []; + metadata: IEventMetadataDescription[] = []; category = "MEETING"; @@ -183,15 +184,15 @@ export class EventModel implements IEvent { this.description = hash.description || ""; if (hash.beginsOn) { - this.beginsOn = new Date(hash.beginsOn); + this.beginsOn = new Date(hash.beginsOn).toISOString(); } if (hash.endsOn) { - this.endsOn = new Date(hash.endsOn); + this.endsOn = new Date(hash.endsOn).toISOString(); } else { this.endsOn = null; } - this.publishAt = new Date(hash.publishAt); + this.publishAt = new Date(hash.publishAt).toISOString(); this.status = hash.status; this.visibility = hash.visibility; @@ -227,7 +228,6 @@ export class EventModel implements IEvent { } } -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function removeTypeName(entity: any): any { if (entity?.__typename) { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -242,8 +242,8 @@ export function toEditJSON(event: IEditableEvent): IEventEditJSON { id: event.id, title: event.title, description: event.description, - beginsOn: event.beginsOn ? event.beginsOn.toISOString() : null, - endsOn: event.endsOn ? event.endsOn.toISOString() : null, + beginsOn: event.beginsOn ? event.beginsOn.toString() : null, + endsOn: event.endsOn ? event.endsOn.toString() : null, status: event.status, category: event.category, visibility: event.visibility, @@ -280,6 +280,10 @@ export function organizer(event: IEvent): IActor | null { return null; } +export function organizerAvatarUrl(event: IEvent): string | null { + return organizer(event)?.avatar?.url ?? null; +} + export function organizerDisplayName(event: IEvent): string | null { const organizerActor = organizer(event); if (organizerActor) { @@ -287,3 +291,7 @@ export function organizerDisplayName(event: IEvent): string | null { } return null; } + +export function instanceOfIEvent(object: any): object is IEvent { + return "organizerActor" in object; +} diff --git a/js/src/types/instance.model.ts b/js/src/types/instance.model.ts index 165c4bb56..a26a0be02 100644 --- a/js/src/types/instance.model.ts +++ b/js/src/types/instance.model.ts @@ -12,4 +12,5 @@ export interface IInstance { followingsCount: number; reportsCount: number; mediaSize: number; + eventCount: number; } diff --git a/js/src/types/media.model.ts b/js/src/types/media.model.ts index cc8e42fd4..3a85445b4 100644 --- a/js/src/types/media.model.ts +++ b/js/src/types/media.model.ts @@ -12,6 +12,10 @@ export interface IMediaUpload { alt: string | null; } +export interface IMediaUploadWrapper { + media: IMediaUpload; +} + export interface IMediaMetadata { width?: number; height?: number; diff --git a/js/src/types/picture.ts b/js/src/types/picture.ts new file mode 100644 index 000000000..47ead96a8 --- /dev/null +++ b/js/src/types/picture.ts @@ -0,0 +1,11 @@ +export interface PictureInformation { + url: string; + author: { + name: string; + url: string; + }; + source: { + name: string; + url: string; + }; +} diff --git a/js/src/types/post.model.ts b/js/src/types/post.model.ts index 6b4a34c57..094b3075e 100644 --- a/js/src/types/post.model.ts +++ b/js/src/types/post.model.ts @@ -10,12 +10,14 @@ export interface IPost { local: boolean; title: string; body: string; - tags?: ITag[]; + tags: ITag[]; picture?: IMedia | null; draft: boolean; visibility: PostVisibility; author?: IActor; attributedTo?: IActor; - publishAt?: Date; - insertedAt?: Date; + publishAt?: string; + insertedAt?: string; + language?: string; + updatedAt?: string; } diff --git a/js/src/types/report.model.ts b/js/src/types/report.model.ts index b7a79bdd9..dd54158a3 100644 --- a/js/src/types/report.model.ts +++ b/js/src/types/report.model.ts @@ -10,6 +10,7 @@ export interface IReportNote extends IActionLogObject { id: string; content: string; moderator: IActor; + insertedAt: string; } export interface IReport extends IActionLogObject { id: string; @@ -19,8 +20,8 @@ export interface IReport extends IActionLogObject { comments: IComment[]; content: string; notes: IReportNote[]; - insertedAt: Date; - updatedAt: Date; + insertedAt: string; + updatedAt: string; status: ReportStatusEnum; } diff --git a/js/src/types/resource.ts b/js/src/types/resource.ts index 2fa2dff7a..041ef67c7 100644 --- a/js/src/types/resource.ts +++ b/js/src/types/resource.ts @@ -19,15 +19,16 @@ export interface IResource { id?: string; title: string; summary?: string; - actor?: IActor; + actor?: IGroup; url?: string; resourceUrl: string; path?: string; children: Paginate; parent?: IResource; metadata: IResourceMetadata; - insertedAt?: Date; - updatedAt?: Date; + insertedAt?: string; + updatedAt?: string; + publishedAt?: string; creator?: IActor; type?: string; } diff --git a/js/src/types/stats.model.ts b/js/src/types/stats.model.ts new file mode 100644 index 000000000..21f6d89c3 --- /dev/null +++ b/js/src/types/stats.model.ts @@ -0,0 +1,5 @@ +export interface CategoryStatsModel { + key: string; + number: number; + label?: string; +} diff --git a/js/src/types/todos.ts b/js/src/types/todos.ts index ad3dd8a5c..084bc2b80 100644 --- a/js/src/types/todos.ts +++ b/js/src/types/todos.ts @@ -5,7 +5,7 @@ export interface ITodo { id?: string; title: string; status: boolean; - dueDate?: Date; + dueDate?: string; creator?: IActor; assignedTo?: IPerson; todoList?: ITodoList; diff --git a/js/src/types/user-location.model.ts b/js/src/types/user-location.model.ts new file mode 100644 index 000000000..7b6f4cb71 --- /dev/null +++ b/js/src/types/user-location.model.ts @@ -0,0 +1,10 @@ +import { PictureInformation } from "./picture"; + +export type LocationType = { + lat: number | undefined; + lon: number | undefined; + name: string | undefined; + picture?: PictureInformation; + isIPLocation?: boolean; + accuracy?: number; +}; diff --git a/js/src/utils/asyncForEach.ts b/js/src/utils/asyncForEach.ts index f0f4ee976..ca3b9d27a 100644 --- a/js/src/utils/asyncForEach.ts +++ b/js/src/utils/asyncForEach.ts @@ -1,11 +1,9 @@ async function asyncForEach( array: Array, - // eslint-disable-next-line no-unused-vars callback: (arg0: any, arg1: number, arg2: Array) => void ): Promise { for (let index = 0; index < array.length; index += 1) { - // eslint-disable-next-line no-await-in-loop - await callback(array[index], index, array); + callback(array[index], index, array); } } diff --git a/js/src/utils/auth.ts b/js/src/utils/auth.ts index c0657c2e7..e4c43dfa7 100644 --- a/js/src/utils/auth.ts +++ b/js/src/utils/auth.ts @@ -9,12 +9,11 @@ import { } from "@/constants"; import { ILogin, IToken } from "@/types/login.model"; import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user"; -import { ApolloClient } from "@apollo/client/core/ApolloClient"; -import { IPerson } from "@/types/actor"; -import { IDENTITIES, UPDATE_CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; +import { UPDATE_CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; import { ICurrentUserRole } from "@/types/enums"; -import { NormalizedCacheObject } from "@apollo/client/cache/inmemory/types"; import { LOGOUT } from "@/graphql/auth"; +import { provideApolloClient, useMutation } from "@vue/apollo-composable"; +import { apolloClient } from "@/vue-apollo"; export function saveTokenData(obj: IToken): void { localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken); @@ -37,10 +36,6 @@ export function getLocaleData(): string | null { return localStorage ? localStorage.getItem(USER_LOCALE) : null; } -export function saveActorData(obj: IPerson): void { - localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`); -} - export function deleteUserData(): void { [ AUTH_USER_ID, @@ -54,78 +49,35 @@ export function deleteUserData(): void { }); } -export class NoIdentitiesException extends Error {} +export async function logout(performServerLogout = true): Promise { + const { mutate: logoutMutation } = provideApolloClient(apolloClient)(() => + useMutation(LOGOUT) + ); + const { mutate: cleanUserClient } = provideApolloClient(apolloClient)(() => + useMutation(UPDATE_CURRENT_USER_CLIENT) + ); + const { mutate: cleanActorClient } = provideApolloClient(apolloClient)(() => + useMutation(UPDATE_CURRENT_ACTOR_CLIENT) + ); -export async function changeIdentity( - apollo: ApolloClient, - identity: IPerson -): Promise { - await apollo.mutate({ - mutation: UPDATE_CURRENT_ACTOR_CLIENT, - variables: identity, - }); - saveActorData(identity); -} - -/** - * We fetch from localStorage the latest actor ID used, - * then fetch the current identities to set in cache - * the current identity used - */ -export async function initializeCurrentActor( - apollo: ApolloClient -): Promise { - const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID); - - const result = await apollo.query({ - query: IDENTITIES, - fetchPolicy: "network-only", - }); - const { identities } = result.data; - if (identities.length < 1) { - console.warn("Logged user has no identities!"); - throw new NoIdentitiesException(); - } - const activeIdentity = - identities.find((identity: IPerson) => identity.id === actorId) || - (identities[0] as IPerson); - - if (activeIdentity) { - await changeIdentity(apollo, activeIdentity); - } -} - -export async function logout( - apollo: ApolloClient, - performServerLogout = true -): Promise { if (performServerLogout) { - await apollo.mutate({ - mutation: LOGOUT, - variables: { - refreshToken: localStorage.getItem(AUTH_REFRESH_TOKEN), - }, + logoutMutation({ + refreshToken: localStorage.getItem(AUTH_REFRESH_TOKEN), }); } - await apollo.mutate({ - mutation: UPDATE_CURRENT_USER_CLIENT, - variables: { - id: null, - email: null, - isLoggedIn: false, - role: ICurrentUserRole.USER, - }, + cleanUserClient({ + id: null, + email: null, + isLoggedIn: false, + role: ICurrentUserRole.USER, }); - await apollo.mutate({ - mutation: UPDATE_CURRENT_ACTOR_CLIENT, - variables: { - id: null, - avatar: null, - preferredUsername: null, - name: null, - }, + cleanActorClient({ + id: null, + avatar: null, + preferredUsername: null, + name: null, }); deleteUserData(); diff --git a/js/src/utils/datetime.ts b/js/src/utils/datetime.ts index 4eef7cc7f..ae490fb62 100644 --- a/js/src/utils/datetime.ts +++ b/js/src/utils/datetime.ts @@ -1,3 +1,6 @@ +import type { Locale } from "date-fns"; +import { format } from "date-fns"; + function localeMonthNames(): string[] { const monthNames: string[] = []; for (let i = 0; i < 12; i += 1) { @@ -19,16 +22,57 @@ function localeShortWeekDayNames(): string[] { } // https://stackoverflow.com/a/18650828/10204399 -function formatBytes(bytes: number, decimals = 2, zero = "0 Bytes"): string { - if (bytes === 0) return zero; +function formatBytes( + bytes: number, + decimals = 2, + locale: string | undefined = undefined +): string { + const formatNumber = (value = 0, unit = "byte") => + new Intl.NumberFormat(locale, { + style: "unit", + unit, + unitDisplay: "long", + }).format(value); + + if (bytes === 0) return formatNumber(0); + if (bytes < 0 || bytes > Number.MAX_SAFE_INTEGER) { + throw new RangeError( + "Number mustn't be negative and be inferior to Number.MAX_SAFE_INTEGER" + ); + } const k = 1024; const dm = decimals < 0 ? 0 : decimals; - const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + const sizes = [ + "byte", + "kilobyte", + "megabyte", + "gigabyte", + "terabyte", + "petabyte", + ]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; + return formatNumber(parseFloat((bytes / k ** i).toFixed(dm)), sizes[i]); } -export { localeMonthNames, localeShortWeekDayNames, formatBytes }; +function roundToNearestMinute(date = new Date()) { + const minutes = 1; + const ms = 1000 * 60 * minutes; + + // 👇️ replace Math.round with Math.ceil to always round UP + return new Date(Math.round(date.getTime() / ms) * ms); +} + +function formatDateTimeForEvent(dateTime: Date, locale: Locale): string { + return format(dateTime, "PPp", { locale }); +} + +export { + localeMonthNames, + localeShortWeekDayNames, + formatBytes, + roundToNearestMinute, + formatDateTimeForEvent, +}; diff --git a/js/src/utils/graphics.ts b/js/src/utils/graphics.ts new file mode 100644 index 000000000..34c903ff1 --- /dev/null +++ b/js/src/utils/graphics.ts @@ -0,0 +1,28 @@ +import random from "lodash/random"; + +export const randomGradient = (): string => { + const direction = [ + "bg-gradient-to-t", + "bg-gradient-to-tr", + "bg-gradient-to-r", + "bg-gradient-to-br", + "bg-gradient-to-b", + "bg-gradient-to-bl", + "bg-gradient-to-l", + "bg-gradient-to-tl", + ]; + const gradients = [ + "from-pink-500 via-red-500 to-yellow-500", + "from-green-400 via-blue-500 to-purple-600", + "from-pink-400 via-purple-300 to-indigo-400", + "from-yellow-300 via-yellow-500 to-yellow-700", + "from-yellow-300 via-green-400 to-green-500", + "from-red-400 via-red-600 to-yellow-400", + "from-green-400 via-green-500 to-blue-700", + "from-yellow-400 via-yellow-500 to-yellow-700", + "from-green-300 via-green-400 to-purple-700", + ]; + return `${direction[random(0, direction.length - 1)]} ${ + gradients[random(0, gradients.length - 1)] + }`; +}; diff --git a/js/src/utils/html.ts b/js/src/utils/html.ts index ee6cea891..ffa2dad41 100644 --- a/js/src/utils/html.ts +++ b/js/src/utils/html.ts @@ -1,3 +1,12 @@ export function nl2br(text: string): string { return text.replace(/(?:\r\n|\r|\n)/g, "
"); } + +export function htmlToText(html: string) { + const template = document.createElement("template"); + const trimmedHTML = html.trim(); + template.innerHTML = trimmedHTML; + const text = template.content.textContent; + template.remove(); + return text; +} diff --git a/js/src/utils/i18n.ts b/js/src/utils/i18n.ts index 06c213b4e..c2a7030f5 100644 --- a/js/src/utils/i18n.ts +++ b/js/src/utils/i18n.ts @@ -1,10 +1,9 @@ -import Vue from "vue"; -import VueI18n from "vue-i18n"; -import { DateFnsPlugin } from "@/plugins/dateFns"; +import { createI18n } from "vue-i18n"; import en from "../i18n/en_US.json"; import langs from "../i18n/langs.json"; import { getLocaleData } from "./auth"; import pluralizationRules from "../i18n/pluralRules"; +// import messages from "@intlify/vite-plugin-vue-i18n/messages"; const DEFAULT_LOCALE = "en_US"; @@ -12,17 +11,10 @@ const localeInLocalStorage = getLocaleData(); export const AVAILABLE_LANGUAGES = Object.keys(langs); -console.debug("localeInLocalStorage", localeInLocalStorage); - let language = localeInLocalStorage || (document.documentElement.getAttribute("lang") as string); -console.debug( - "localeInLocalStorage or fallback to lang html attribute", - language -); - language = language || ((window.navigator as any).userLanguage || window.navigator.language).replace( @@ -30,35 +22,29 @@ language = "_" ); -console.debug("language or fallback to window.navigator language", language); - export const locale = language && Object.prototype.hasOwnProperty.call(langs, language) ? language : language.split("-")[0]; -console.debug("chosen locale", locale); - -Vue.use(VueI18n); - -export const i18n = new VueI18n({ - locale: DEFAULT_LOCALE, // set locale +export const i18n = createI18n({ + legacy: false, + locale: locale, // set locale // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore + // messages, // set locale messages messages: en, // set locale messages fallbackLocale: DEFAULT_LOCALE, formatFallbackMessages: true, pluralizationRules, fallbackRootWithEmptyString: true, + globalInjection: true, }); -console.debug("set VueI18n with default locale", DEFAULT_LOCALE); - const loadedLanguages = [DEFAULT_LOCALE]; function setI18nLanguage(lang: string): string { - console.debug("setting i18n locale to", lang); - i18n.locale = lang; + i18n.global.locale = lang; setLanguageInDOM(lang); return lang; } @@ -74,7 +60,6 @@ function setLanguageInDOM(lang: string): void { const direction = ["ar", "ae", "he", "fa", "ku", "ur"].includes(fixedLang) ? "rtl" : "ltr"; - console.debug("setDirection with", [fixedLang, direction]); html.setAttribute("dir", direction); } @@ -93,43 +78,26 @@ function vueI18NfileForLanguage(lang: string) { return fileForLanguage(matches, lang); } -function dateFnsfileForLanguage(lang: string) { - const matches: Record = { - en_US: "en-US", - en: "en-US", - }; - return fileForLanguage(matches, lang); -} - -Vue.use(DateFnsPlugin, { locale: dateFnsfileForLanguage(locale) }); - export async function loadLanguageAsync(lang: string): Promise { // If the same language - if (i18n.locale === lang) { - console.debug("already using language", lang); + if (i18n.global.locale === lang) { return Promise.resolve(setI18nLanguage(lang)); } // If the language was already loaded if (loadedLanguages.includes(lang)) { - console.debug("language already loaded", lang); return Promise.resolve(setI18nLanguage(lang)); } // If the language hasn't been loaded yet - console.debug("loading language", lang); const newMessages = await import( - /* webpackChunkName: "lang-[request]" */ `@/i18n/${vueI18NfileForLanguage( - lang - )}.json` + `../i18n/${vueI18NfileForLanguage(lang)}.json` ); - i18n.setLocaleMessage(lang, newMessages.default); + i18n.global.setLocaleMessage(lang, newMessages.default); loadedLanguages.push(lang); return setI18nLanguage(lang); } -console.debug("loading async locale", locale); loadLanguageAsync(locale); -console.debug("loaded async locale", locale); export function formatList(list: string[]): string { if (window.Intl && Intl.ListFormat) { diff --git a/js/src/utils/identity.ts b/js/src/utils/identity.ts new file mode 100644 index 000000000..fd3e61313 --- /dev/null +++ b/js/src/utils/identity.ts @@ -0,0 +1,58 @@ +import { AUTH_USER_ACTOR_ID } from "@/constants"; +import { UPDATE_CURRENT_ACTOR_CLIENT, IDENTITIES } from "@/graphql/actor"; +import { IPerson } from "@/types/actor"; +import { apolloClient } from "@/vue-apollo"; +import { + provideApolloClient, + useMutation, + useQuery, +} from "@vue/apollo-composable"; +import { computed, watch } from "vue"; + +export class NoIdentitiesException extends Error {} + +export function saveActorData(obj: IPerson): void { + localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`); +} + +export async function changeIdentity(identity: IPerson): Promise { + if (!identity.id) return; + const { mutate: updateCurrentActorClient } = provideApolloClient( + apolloClient + )(() => useMutation(UPDATE_CURRENT_ACTOR_CLIENT)); + + updateCurrentActorClient(identity); + if (identity.id) { + saveActorData(identity); + } +} + +/** + * We fetch from localStorage the latest actor ID used, + * then fetch the current identities to set in cache + * the current identity used + */ +export async function initializeCurrentActor(): Promise { + const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID); + + const { result: identitiesResult } = provideApolloClient(apolloClient)(() => + useQuery<{ identities: IPerson[] }>(IDENTITIES) + ); + + const identities = computed(() => identitiesResult.value?.identities); + + watch(identities, async () => { + if (identities.value && identities.value.length < 1) { + console.warn("Logged user has no identities!"); + throw new NoIdentitiesException(); + } + const activeIdentity = + (identities.value || []).find( + (identity: IPerson | undefined) => identity?.id === actorId + ) || ((identities.value || [])[0] as IPerson); + + if (activeIdentity) { + await changeIdentity(activeIdentity); + } + }); +} diff --git a/js/src/utils/listFormat.ts b/js/src/utils/listFormat.ts new file mode 100644 index 000000000..47cb20787 --- /dev/null +++ b/js/src/utils/listFormat.ts @@ -0,0 +1,17 @@ +const shortConjunctionFormatter = new Intl.ListFormat(undefined, { + style: "short", + type: "conjunction", +}); + +const shortDisjunctionFormatter = new Intl.ListFormat(undefined, { + style: "short", + type: "disjunction", +}); + +export const listShortConjunctionFormatter = (list: Array): string => { + return shortConjunctionFormatter.format(list); +}; + +export const listShortDisjunctionFormatter = (list: Array): string => { + return shortDisjunctionFormatter.format(list); +}; diff --git a/js/src/utils/location.ts b/js/src/utils/location.ts new file mode 100644 index 000000000..8e0ece55d --- /dev/null +++ b/js/src/utils/location.ts @@ -0,0 +1,22 @@ +import ngeohash from "ngeohash"; + +const GEOHASH_DEPTH = 9; // put enough accuracy, radius will be used anyway + +export const coordsToGeoHash = ( + lat: number | undefined, + lon: number | undefined, + depth = GEOHASH_DEPTH +): string | undefined => { + if (lat && lon && depth) { + return ngeohash.encode(lat, lon, GEOHASH_DEPTH); + } + return undefined; +}; + +export const geoHashToCoords = ( + geohash: string | undefined +): { latitude: number; longitude: number } | undefined => { + if (!geohash) return undefined; + const { latitude, longitude } = ngeohash.decode(geohash); + return latitude && longitude ? { latitude, longitude } : undefined; +}; diff --git a/js/src/utils/share.ts b/js/src/utils/share.ts new file mode 100644 index 000000000..a549b9245 --- /dev/null +++ b/js/src/utils/share.ts @@ -0,0 +1,80 @@ +export const twitterShareUrl = ( + url: string | undefined, + text: string | undefined +): string | undefined => { + if (!url || !text) return undefined; + return `https://twitter.com/intent/tweet?url=${encodeURIComponent( + url + )}&text=${text}`; +}; + +export const facebookShareUrl = ( + url: string | undefined +): string | undefined => { + if (!url) return undefined; + return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent( + url + )}`; +}; + +export const linkedInShareUrl = ( + url: string | undefined, + text: string | undefined +): string | undefined => { + if (!url || !text) return undefined; + return `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent( + url + )}&title=${text}`; +}; + +export const whatsAppShareUrl = ( + url: string | undefined, + text: string | undefined +): string | undefined => { + if (!url || !text) return undefined; + return `https://wa.me/?text=${encodeURIComponent( + basicTextToEncode(url, text) + )}`; +}; + +export const telegramShareUrl = ( + url: string | undefined, + text: string | undefined +): string | undefined => { + if (!url || !text) return undefined; + return `https://t.me/share/url?url=${encodeURIComponent( + url + )}&text=${encodeURIComponent(text)}`; +}; + +export const emailShareUrl = ( + url: string | undefined, + text: string | undefined +): string | undefined => { + if (!url || !text) return undefined; + return `mailto:?to=&body=${url}&subject=${text}`; +}; + +export const diasporaShareUrl = ( + url: string | undefined, + text: string | undefined +): string | undefined => { + if (!url || !text) return undefined; + return `https://share.diasporafoundation.org/?title=${encodeURIComponent( + text + )}&url=${encodeURIComponent(url)}`; +}; + +export const mastodonShareUrl = ( + url: string | undefined, + text: string | undefined +): string | undefined => { + if (!url || !text) return undefined; + return `https://toot.kytta.dev/?text=${encodeURIComponent( + basicTextToEncode(url, text) + )}`; +}; + +const basicTextToEncode = (url: string, text: string): string => { + return `${text}\r\n${url}`; +}; diff --git a/js/src/variables.scss b/js/src/variables.scss deleted file mode 100644 index 07fb0e60e..000000000 --- a/js/src/variables.scss +++ /dev/null @@ -1,155 +0,0 @@ -@import "~bulma/sass/utilities/functions.sass"; -@import "~bulma/sass/utilities/initial-variables.sass"; -@import "~bulma/sass/utilities/derived-variables.sass"; - -$bleuvert: #1e7d97; -$jaune: #ffd599; -$violet: #424056; - -/** - * Text body, paragraphs - */ -$violet-1: #3a384c; -$violet-2: #474467; - -/** - * Titles, dark borders, buttons - */ -$violet-3: #3c376e; - -/** - * Borders - */ -$borders: #d7d6de; -$backgrounds: #ecebf2; - -/** - * Text - */ -$purple-1: #757199; - -/** - * Background - */ -$purple-2: #cdcaea; -$purple-3: #e6e4f4; - -$orange-2: #ed8d07; -$orange-3: #d35204; - -$yellow-1: #ffd599; -$yellow-2: #fff1de; -$yellow-3: #fbd5cb; -$yellow-4: #f7ba30; - -$primary: $bleuvert; -$primary-invert: findColorInvert($primary); -$secondary: $jaune; -$secondary-invert: findColorInvert($secondary); - -$background-color: $violet-2; - -$success: #0d8758; -$success-invert: findColorInvert($success); -$info: #36bcd4; -$info-invert: findColorInvert($info); -$danger: #cd2026; -$danger-invert: findColorInvert($danger); -$link: $primary; -$link-invert: $primary-invert; -$text: $violet-1; -$grey: #757575; - -$colors: map-merge( - $colors, - ( - "primary": ( - $primary, - $primary-invert, - ), - "secondary": ( - $secondary, - $secondary-invert, - ), - "success": ( - $success, - $success-invert, - ), - "info": ( - $info, - $info-invert, - ), - "danger": ( - $danger, - $danger-invert, - ), - "link": ( - $link, - $link-invert, - ), - "grey": ( - $grey, - findColorInvert($grey), - ), - ) -); - -// Navbar -$navbar-background-color: $secondary; -$navbar-item-color: $background-color; -$navbar-height: 4rem; - -// Footer -$footer-padding: 3rem 1.5rem 1rem; -$footer-background-color: $background-color; - -$body-background-color: #efeef4; -$fullhd-enabled: false; -$hero-body-padding-medium: 6rem 1.5rem; - -main > .container { - background: $body-background-color; - min-height: 70vh; -} - -$title-color: #3c376e; -$title-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, - serif; -$title-weight: 700; -$title-size: 40px; -$title-sub-size: 45px; -$title-sup-size: 30px; - -$subtitle-color: #3a384c; -$subtitle-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, - serif; -$subtitle-weight: 400; -$subtitle-size: 32px; -$subtitle-sub-size: 30px; -$subtitle-sup-size: 15px; - -.subtitle { - background: $secondary; - display: inline; - padding: 3px 8px; - margin: 15px auto 30px; -} - -//$input-border-color: #dbdbdb; -$breadcrumb-item-color: $primary; -$checkbox-background-color: #fff; -$title-color: $violet-3; - -:root { - --color-primary: 30 125 151; - --color-secondary: 255 213 153; - --color-violet-title: 66 64 86; -} - -@media (prefers-color-scheme: dark) { - :root { - --color-primary: 30 125 151; - --color-secondary: 255 213 153; - --color-violet-title: 66 64 86; - } -} diff --git a/js/src/views/About.vue b/js/src/views/About.vue deleted file mode 100644 index 964811c43..000000000 --- a/js/src/views/About.vue +++ /dev/null @@ -1,164 +0,0 @@ - - - - - diff --git a/js/src/views/About/AboutInstance.vue b/js/src/views/About/AboutInstance.vue deleted file mode 100644 index a301fde04..000000000 --- a/js/src/views/About/AboutInstance.vue +++ /dev/null @@ -1,219 +0,0 @@ - - - - - diff --git a/js/src/views/About/AboutInstanceView.vue b/js/src/views/About/AboutInstanceView.vue new file mode 100644 index 000000000..13a33f1e0 --- /dev/null +++ b/js/src/views/About/AboutInstanceView.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/js/src/views/About/Glossary.vue b/js/src/views/About/Glossary.vue deleted file mode 100644 index 6ef9e7a1c..000000000 --- a/js/src/views/About/Glossary.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - diff --git a/js/src/views/About/GlossaryView.vue b/js/src/views/About/GlossaryView.vue new file mode 100644 index 000000000..7faed36f1 --- /dev/null +++ b/js/src/views/About/GlossaryView.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/js/src/views/About/Privacy.vue b/js/src/views/About/Privacy.vue deleted file mode 100644 index 66e9d5d3d..000000000 --- a/js/src/views/About/Privacy.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - diff --git a/js/src/views/About/PrivacyView.vue b/js/src/views/About/PrivacyView.vue new file mode 100644 index 000000000..2d8e0d42f --- /dev/null +++ b/js/src/views/About/PrivacyView.vue @@ -0,0 +1,47 @@ + + + diff --git a/js/src/views/About/Rules.vue b/js/src/views/About/Rules.vue deleted file mode 100644 index bf58f5f62..000000000 --- a/js/src/views/About/Rules.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - diff --git a/js/src/views/About/RulesView.vue b/js/src/views/About/RulesView.vue new file mode 100644 index 000000000..37c15345c --- /dev/null +++ b/js/src/views/About/RulesView.vue @@ -0,0 +1,31 @@ + + + diff --git a/js/src/views/About/Terms.vue b/js/src/views/About/Terms.vue deleted file mode 100644 index dc20db2ad..000000000 --- a/js/src/views/About/Terms.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - diff --git a/js/src/views/About/TermsView.vue b/js/src/views/About/TermsView.vue new file mode 100644 index 000000000..13df6adfe --- /dev/null +++ b/js/src/views/About/TermsView.vue @@ -0,0 +1,53 @@ + + + diff --git a/js/src/views/AboutView.vue b/js/src/views/AboutView.vue new file mode 100644 index 000000000..8476b2396 --- /dev/null +++ b/js/src/views/AboutView.vue @@ -0,0 +1,141 @@ + + + diff --git a/js/src/views/Account/IdentityPicker.vue b/js/src/views/Account/IdentityPicker.vue index 50c4c9666..254e51f37 100644 --- a/js/src/views/Account/IdentityPicker.vue +++ b/js/src/views/Account/IdentityPicker.vue @@ -1,71 +1,85 @@ - diff --git a/js/src/views/Account/IdentityPickerWrapper.vue b/js/src/views/Account/IdentityPickerWrapper.vue index 5f2293951..80bdba644 100644 --- a/js/src/views/Account/IdentityPickerWrapper.vue +++ b/js/src/views/Account/IdentityPickerWrapper.vue @@ -1,122 +1,111 @@ - - diff --git a/js/src/views/Account/Register.vue b/js/src/views/Account/Register.vue deleted file mode 100644 index 07db71e0d..000000000 --- a/js/src/views/Account/Register.vue +++ /dev/null @@ -1,256 +0,0 @@ - - - - - diff --git a/js/src/views/Account/RegisterView.vue b/js/src/views/Account/RegisterView.vue new file mode 100644 index 000000000..8b52b6cab --- /dev/null +++ b/js/src/views/Account/RegisterView.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/js/src/views/Account/children/EditIdentity.vue b/js/src/views/Account/children/EditIdentity.vue index d0e56dd43..f2d71ea4d 100644 --- a/js/src/views/Account/children/EditIdentity.vue +++ b/js/src/views/Account/children/EditIdentity.vue @@ -3,7 +3,7 @@

- {{ identity.displayName() }} + {{ displayName(identity) }} {{ $t("I create an identity") }}

@@ -14,22 +14,22 @@ class="picture-upload" /> - - - + - - - + @{{ getInstanceHost }}

-
-
+ + - - - + - {{ error }}{{ error }} - +
- +
-
+ - - + {{ $t("Delete this identity") }} - - + +
-
-

{{ $t("Profile feeds") }}

-
+

{{ $t("Profile feeds") }}

{{ $t( @@ -110,60 +108,60 @@

- - {{ $t("RSS/Atom Feed") }}{{ $t("RSS/Atom Feed") }} - - + - {{ $t("ICS/WebCal Feed") }}{{ $t("ICS/WebCal Feed") }} - - + {{ $t("Regenerate new links") }}{{ $t("Regenerate new links") }}
- {{ $t("Create new links") }}{{ $t("Create new links") }}
@@ -196,482 +194,563 @@ h1 { margin-bottom: 0; } -::v-deep .buttons > *:not(:last-child) .button { +:deep(.buttons > *:not(:last-child) .button) { @include margin-right(0.5rem); } - diff --git a/js/src/views/Admin/AdminGroupProfile.vue b/js/src/views/Admin/AdminGroupProfile.vue index 67c6d1b4a..422672e2a 100644 --- a/js/src/views/Admin/AdminGroupProfile.vue +++ b/js/src/views/Admin/AdminGroupProfile.vue @@ -2,10 +2,10 @@
0" class="table is-fullwidth"> + @@ -52,243 +52,248 @@
{{ key }}
-
- + {{ $t("Suspend") }}{{ t("Suspend") }} - {{ $t("Unsuspend") }}{{ t("Unsuspend") }} - {{ $t("Refresh profile") }}{{ t("Refresh profile") }}
-

+

{{ - $tc("{number} members", group.members.total, { - number: group.members.total, - }) + t( + "{number} members", + { + number: group.members.total, + }, + group.members.total + ) }}

- - -
-
- -
- -
-
+
+
+
+ +
+ +
+
+
{{ props.row.actor.name }}@{{ usernameWithDomain(props.row.actor) }}
- @{{ usernameWithDomain(props.row.actor) }}
- - - + + - {{ $t("Administrator") }} - - + - {{ $t("Moderator") }} - - - {{ $t("Member") }} - - + + {{ t("Member") }} + + - {{ $t("Not approved") }} - - + - {{ $t("Rejected") }} - - + - {{ $t("Invited") }} - - - + {{ t("Invited") }} + + + - {{ props.row.insertedAt | formatDateString }}
{{ - props.row.insertedAt | formatTimeString + {{ formatDateString(props.row.insertedAt) }}
{{ + formatTimeString(props.row.insertedAt) }}
-
-