diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 66a805a2..739e569f 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 3162337d..73654c14 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 f5dc26cb..1ebf3e65 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 9a37eef8..77c55e54 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 4c87c709..6342d684 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 10dcd95e..101b3607 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 b3a5de1e..5c5176c4 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 162a3ea9..00000000 --- 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 00000000..f284c30a --- /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 287cc998..9a36881d 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 00000000..349d7757 --- /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 a87688fe..00000000 --- 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 79073992..4287e202 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 00000000..c3da6c26 --- /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 00000000..2d5f4b41 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 00000000..a9fab278 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 00000000..967246ba 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 00000000..06d4d2c9 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 00000000..97c743d6 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 00000000..6dbe7a67 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 00000000..deafb6b0 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 00000000..c50ef433 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 00000000..71d98490 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 00000000..0225ca51 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 00000000..8095816a 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 00000000..56a06944 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 00000000..1514b3ee 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 00000000..fcec8cea 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 00000000..d3f70c9b 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 00000000..72b46015 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 00000000..e7c47b4c 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 00000000..987dbbdd 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 00000000..856ec718 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 00000000..4efa3fef 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 00000000..79a2027c 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 00000000..1eac571b 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 00000000..a52d1c40 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 00000000..8e3bc6b3 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 00000000..0ac0863b 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 00000000..082401ed 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 00000000..bff9c379 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 00000000..3022135b 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 00000000..0f636743 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 00000000..304eff1c 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 00000000..ff0c6575 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 00000000..766e608c 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 4ef920d2..00000000 --- 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 00000000..c20e9765 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 46752640..00000000 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 00000000..d807645e 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 57c87926..00000000 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 887137e1..96f31769 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 004c872c..00000000 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 7b6c6fda..15f14324 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 227c4aff..00000000 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 00000000..7406af41 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 15a7a887..00000000 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 00000000..e6685dec 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 389a1c88..00000000 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 00000000..e438b2cb 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 d126878b..00000000 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 00000000..be647fe1 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 50a35c9a..00000000 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 00000000..1d498275 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 e67a5bf6..00000000 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 00000000..3fd2a6b9 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 5e7e9c32..00000000 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 3e9463f9..fea1fde5 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 b003dd50..00000000 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 8e838062..319d1257 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 039149cd..00000000 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 00000000..26ef0744 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 8fd9d583..00000000 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 00000000..0d0791a5 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 9f63b55e..00000000 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 ecc4610f..00000000 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 00000000..739b6df8 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 1689c50e..00000000 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 00000000..9d832886 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 00000000..c8a8128d --- /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 00000000..8c0b8749 --- /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 00000000..dba085f3 --- /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 08395187..00000000 --- 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 d19fec20..999d136e 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 30933124..175d22d4 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 00000000..40195eb8 --- /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 4857b860..a6e712a8 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 00000000..4f06faf1 --- /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 4f75672b..b649075e 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 e10a2277..00000000 --- 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 00000000..7f755fd4 --- /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 71c18fc6..00000000 --- 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 00000000..e6b55dcb --- /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 59a55e3f..ff450dc8 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 00000000..82020d4c --- /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 8ef70a2f..bcf92c9b 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 4ca50ee9..1958f579 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 00000000..2ecca3b8 --- /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 9a890f7f..0924b6af 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 adb332f4..0a3d050b 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 313fda30..d3fb4abd 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 e42fe2d3..640e3ce6 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 00000000..e1e16b51 --- /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 48fd0b93..198c9673 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 a2d2c6e1..59b1f47a 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 00000000..c153fdc5 --- /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 c6afd931..bec5c4a9 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 00000000..079d01ba --- /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 00000000..d29223e8 --- /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 00000000..ed30e63e --- /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 9d4a6671..dc88546e 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 392581c5..4d3a7087 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 00000000..4958f4e2 --- /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 00000000..bd95e33c --- /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 a4017a5f..fd53a33e 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 9bfcbf85..0a2a40da 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 03820764..b30a027a 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 00000000..6d63a329 --- /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 00000000..0426739a --- /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 00000000..0d614d00 --- /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 00000000..bd6808b4 --- /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 00000000..f11bcbfc --- /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 00000000..c2b93623 --- /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 00000000..734e892a --- /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 00000000..fe3a0a56 --- /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 30a56104..00000000 --- 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 5a8369cf..00000000 --- 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 00000000..6a36caab --- /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 92c1d47e..c179e0e8 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 0d8d6ed2..8a85f7bd 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 00000000..2ae331cc --- /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 b27b4970..935daad0 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 96badea3..a0ef1d24 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 9d9e5a4f..df8b97fb 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 bf18cc5d..8b9a2466 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 1457f459..be78ce52 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 53baa91e..3812e0f8 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 00000000..9531800e --- /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 d75919e4..d784646d 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 b06aeabc..d8a56782 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 00000000..c1643741 --- /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 b6376f74..ab9ca782 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 264eb19b..17383956 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 3be7a96a..90e3ecbb 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 4eaa5614..fa936c5a 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 feb25578..aed851fe 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 2fcf2869..00000000 --- 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 00000000..cde862ae --- /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 00000000..86a21dfc --- /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 4d745c93..0eccab10 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 7719b3cd..ca8295c7 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 f3817ef5..bd586e31 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 00000000..ee88b54a --- /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 cd04d09d..2e894205 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 e1b685fd..9b937cb6 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 909b9f6f..86db7b0e 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 8122314a..616edc04 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 49f24678..00000000 --- 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 00000000..7bf41e14 --- /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 4cb6fedc..755744db 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 acf311ad..00000000 --- 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 5e1dbe2e..c73b9f72 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 00000000..7c5e87e9 --- /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 00000000..f0193db2 --- /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 00000000..b624fa68 --- /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 00000000..1b6a20a5 --- /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 00000000..491e45fd --- /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 00000000..a5295104 --- /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 00000000..ca246345 --- /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 00000000..407031a8 --- /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 00000000..0f933bd5 --- /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 00000000..cbd15627 --- /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 00000000..63c238eb --- /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 00000000..37ecaece --- /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 00000000..252cd101 --- /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 00000000..daa130a2 --- /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 00000000..2c5899a1 --- /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 00000000..e69de29b diff --git a/js/src/filters/datetime.ts b/js/src/filters/datetime.ts index bdc14e7d..c58799b6 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 89850cd3..de899ef2 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 163621e1..7b301afd 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 49034606..c2dcd10c 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 605d9ddc..6cffea1f 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 706dd7f0..d284ecc1 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 c72f67e0..565fc380 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 00000000..13106de4 --- /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 d2543618..acf0c5de 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 22e2d9a3..29a61271 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 e229eafc..feda64c3 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 eadd78ab..ff84a0a7 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 6c9c1bb9..7c3aefd7 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 00000000..b9757b41 --- /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 0abe22d3..8f692132 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 9a3b13bf..85b360c9 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 18e8a1f0..a5e32557 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 d59273eb..35b8c5b4 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 8d43a6a5..431719b8 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 a55304c5..a1cc30bf 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 787c9b5a..7f95e336 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 4a015065..3df97a0c 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 b2dde7e3..c85f96c8 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 f1c7a102..e695929f 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 af3debc6..05f57f0d 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 874697a2..28a2a6eb 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 3a3614de..9550ada1 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 eb757f34..81affe51 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 9e643acb..97f133d1 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 b95d1955..f6181305 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 98ffd57d..88cfa87b 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 832adb8e..ea618089 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 0b245156..1137039b 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 b748a74a..83ddd8b7 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 79b28cc2..7f9acdda 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 335edcbe..411c6910 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 8c001ec7..b84a0ef7 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 d7d9769e..57b7aad9 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 002f6907..b1d78ec3 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 353f45d2..76dfbb6c 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 66364b6f..94cdd209 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 76f1ae20..799dd817 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 3943e4cf..c8fa6a1d 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 ad49389d..5d6f7781 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 7e9ac92c..b7e3b310 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 e578aa6f..3ce80bde 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 a98ac992..cb49101a 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 0e669519..a3bfaf08 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 b5074b1a..00000000 --- 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 fa8fa4f3..00000000 --- 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 66e31af4..00000000 --- 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 f0f42652..00000000 --- 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 e7d8d720..00000000 --- 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 f3e0a228..00000000 --- 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 a4cbd608..00000000 --- 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 cfd7968e..00000000 --- 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 f4c6bbb3..00000000 --- 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 00000000..f3101b62 --- /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 78c48dd0..545428f0 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 00000000..42eefc50 --- /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 5000bf98..0b5e9c3f 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 00000000..cb425b17 --- /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 ee0de1d9..a8483746 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 6ac74ef9..59bb89a9 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 ec538e46..d3109288 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 9f1f563e..d33973a2 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 1851b9e0..c413326a 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 067b3587..3a782431 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 ac9ec65d..9cde41b9 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 14f880ed..57999952 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 37c3f8c6..5c3de342 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 c71730ba..8e49755e 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 2089df9a..978cb9c8 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 af230659..c7aee7a2 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 ab03f3a6..53a49d48 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 efe6f0ae..53ab340d 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 ef4734b6..0abb0d3c 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 c7e3a1db..b4c051cc 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 7b636f96..92385376 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 51712fdc..fed34985 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 ff165a30..2dbc4602 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 19bacf77..f94bf328 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 4854de21..00000000 --- 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 fdb62d8f..222be9ed 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 984c7f33..088fd0da 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 6f6a2b07..26e20449 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 dc8451a4..c0a0ee21 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 e75d18f2..4de385f1 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 00d16542..5c8d62a6 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 244b5600..d0dd85e0 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 5776bb9b..80cc3366 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 97544c2e..e2ed7599 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 428e3619..f2dda9ad 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 1061d196..871ee46d 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 1f8dcf7e..95e8f2a1 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 776ed7fd..586652ff 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 e0670366..11216edc 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 165c4bb5..a26a0be0 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 cc8e42fd..3a85445b 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 00000000..47ead96a --- /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 6b4a34c5..094b3075 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 b7a79bdd..dd54158a 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 2fa2dff7..041ef67c 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 00000000..21f6d89c --- /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 ad3dd8a5..084bc2b8 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 00000000..7b6f4cb7 --- /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 f0f4ee97..ca3b9d27 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 c0657c2e..e4c43dfa 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 4eef7cc7..ae490fb6 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 00000000..34c903ff --- /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 ee6cea89..ffa2dad4 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 06c213b4..c2a7030f 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 00000000..fd3e6131 --- /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 00000000..47cb2078 --- /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 00000000..8e0ece55 --- /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 00000000..a549b924 --- /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 07fb0e60..00000000 --- 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 964811c4..00000000 --- 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 a301fde0..00000000 --- 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 00000000..13a33f1e --- /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 6ef9e7a1..00000000 --- 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 00000000..7faed36f --- /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 66e9d5d3..00000000 --- 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 00000000..2d8e0d42 --- /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 bf58f5f6..00000000 --- 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 00000000..37c15345 --- /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 dc20db2a..00000000 --- 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 00000000..13df6adf --- /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 00000000..8476b239 --- /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 50c4c966..254e51f3 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 5f229395..80bdba64 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 07db71e0..00000000 --- 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 00000000..8b52b6ca --- /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 d0e56dd4..f2d71ea4 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 67c6d1b4..422672e2 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) }}
-
-