Merge branch 'vue3-compat' into 'main'
Vue 3 and Vite See merge request framasoft/mobilizon!1259
@ -127,14 +127,14 @@ exunit:
|
|||||||
- test-junit-report.xml
|
- test-junit-report.xml
|
||||||
expire_in: 30 days
|
expire_in: 30 days
|
||||||
|
|
||||||
jest:
|
vitest:
|
||||||
stage: test
|
stage: test
|
||||||
needs:
|
needs:
|
||||||
- lint-front
|
- lint-front
|
||||||
before_script:
|
before_script:
|
||||||
- yarn --cwd "js" install --frozen-lockfile
|
- yarn --cwd "js" install --frozen-lockfile
|
||||||
script:
|
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:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
paths:
|
paths:
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
elixir 1.13.4-otp-24
|
elixir 1.14.0-otp-25
|
||||||
erlang 24.3.3
|
erlang 25.0.4
|
||||||
|
@ -54,7 +54,7 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
|||||||
secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM",
|
secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM",
|
||||||
render_errors: [view: Mobilizon.Web.ErrorView, accepts: ~w(html json)],
|
render_errors: [view: Mobilizon.Web.ErrorView, accepts: ~w(html json)],
|
||||||
pubsub_server: Mobilizon.PubSub,
|
pubsub_server: Mobilizon.PubSub,
|
||||||
cache_static_manifest: "priv/static/manifest.json",
|
cache_static_manifest: "priv/static/cache_manifest.json",
|
||||||
has_reverse_proxy: true
|
has_reverse_proxy: true
|
||||||
|
|
||||||
config :mime, :types, %{
|
config :mime, :types, %{
|
||||||
@ -123,6 +123,18 @@ config :mobilizon, Mobilizon.Web.Email.Mailer,
|
|||||||
# can be `true`
|
# can be `true`
|
||||||
no_mx_lookups: false
|
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
|
# Configures Elixir's Logger
|
||||||
config :logger, :console,
|
config :logger, :console,
|
||||||
backends: [:console],
|
backends: [:console],
|
||||||
@ -347,6 +359,23 @@ config :mobilizon, :exports,
|
|||||||
|
|
||||||
config :mobilizon, :analytics, providers: []
|
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
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
@ -15,13 +15,7 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
|||||||
check_origin: false,
|
check_origin: false,
|
||||||
watchers: [
|
watchers: [
|
||||||
node: [
|
node: [
|
||||||
"node_modules/webpack/bin/webpack.js",
|
"node_modules/.bin/vite",
|
||||||
"--mode",
|
|
||||||
"development",
|
|
||||||
"--watch",
|
|
||||||
"--watch-options-stdin",
|
|
||||||
"--config",
|
|
||||||
"node_modules/@vue/cli-service/webpack.config.js",
|
|
||||||
cd: Path.expand("../js", __DIR__)
|
cd: Path.expand("../js", __DIR__)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -102,3 +96,5 @@ config :mobilizon, :anonymous,
|
|||||||
reports: [
|
reports: [
|
||||||
allowed: true
|
allowed: true
|
||||||
]
|
]
|
||||||
|
|
||||||
|
config :unplug, :init_mode, :runtime
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
FROM elixir:latest
|
FROM elixir:latest
|
||||||
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
||||||
|
|
||||||
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 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 curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
|
||||||
RUN npm install -g yarn wait-on
|
RUN npm install -g yarn wait-on
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
|
||||||
@ -6,10 +9,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
extends: [
|
extends: [
|
||||||
"plugin:vue/essential",
|
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"@vue/typescript/recommended",
|
"plugin:vue/vue3-essential",
|
||||||
|
"@vue/eslint-config-typescript/recommended",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
|
"@vue/eslint-config-prettier",
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: ["prettier"],
|
plugins: ["prettier"],
|
||||||
@ -20,12 +24,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
"no-underscore-dangle": [
|
"no-underscore-dangle": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
allow: ["__typename"],
|
allow: ["__typename", "__schema"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
@ -50,4 +53,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
|
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
|
||||||
|
globals: {
|
||||||
|
GeolocationPositionError: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
4
js/.gitignore
vendored
@ -5,6 +5,7 @@ node_modules
|
|||||||
/tests/e2e/videos/
|
/tests/e2e/videos/
|
||||||
/tests/e2e/screenshots/
|
/tests/e2e/screenshots/
|
||||||
/coverage
|
/coverage
|
||||||
|
stats.html
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
@ -23,3 +24,6 @@ yarn-error.log*
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: ["@vue/cli-plugin-babel/preset"],
|
|
||||||
};
|
|
12
js/env.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/// <reference types="histoire/vue" />
|
||||||
|
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_SERVER_URL: string;
|
||||||
|
readonly VITE_HISTOIRE_ENV: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
const fetch = require("node-fetch");
|
import fetch from "node-fetch";
|
||||||
const fs = require("fs");
|
import fs from "fs";
|
||||||
|
|
||||||
fetch(`http://localhost:4000/api`, {
|
fetch(`http://localhost:4000/api`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
51
js/histoire.config.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/// <reference types="@histoire/plugin-vue/components" />
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
@ -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$": "<rootDir>/tests/unit/svgTransform.js",
|
|
||||||
// },
|
|
||||||
// moduleNameMapper: {
|
|
||||||
// "^@/(.*svg)(\\?inline)$": "<rootDir>/src/$1",
|
|
||||||
// "^@/(.*)$": "<rootDir>/src/$1",
|
|
||||||
// },
|
|
||||||
};
|
|
102
js/package.json
@ -3,19 +3,25 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
|
"preview": "vite preview",
|
||||||
"build": "yarn run build:assets && yarn run build:pictures",
|
"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",
|
"lint": "eslint --ext .ts,.vue --ignore-path .gitignore --fix src",
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
"format": "prettier . --write",
|
||||||
"lint": "vue-cli-service lint",
|
"build:assets": "vite build",
|
||||||
"build:assets": "vue-cli-service build --report",
|
"build:pictures": "bash ./scripts/build/pictures.sh",
|
||||||
"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": {
|
"dependencies": {
|
||||||
"@absinthe/socket": "^0.2.1",
|
"@absinthe/socket": "^0.2.1",
|
||||||
"@absinthe/socket-apollo-link": "^0.2.1",
|
"@absinthe/socket-apollo-link": "^0.2.1",
|
||||||
"@apollo/client": "^3.3.16",
|
"@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/tracing": "^7.1",
|
||||||
"@sentry/vue": "^7.1",
|
"@sentry/vue": "^7.1",
|
||||||
"@tailwindcss/line-clamp": "^0.4.0",
|
"@tailwindcss/line-clamp": "^0.4.0",
|
||||||
@ -39,24 +45,32 @@
|
|||||||
"@tiptap/extension-strike": "^2.0.0-beta.26",
|
"@tiptap/extension-strike": "^2.0.0-beta.26",
|
||||||
"@tiptap/extension-text": "^2.0.0-beta.15",
|
"@tiptap/extension-text": "^2.0.0-beta.15",
|
||||||
"@tiptap/extension-underline": "^2.0.0-beta.7",
|
"@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/announcer": "^2.1.0",
|
||||||
"@vue-a11y/skip-to": "^2.1.2",
|
"@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",
|
"apollo-absinthe-upload-link": "^1.5.0",
|
||||||
"autoprefixer": "^10",
|
"autoprefixer": "^10",
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^2.0.0",
|
||||||
"buefy": "^0.9.0",
|
"bulma": "^0.9.4",
|
||||||
"bulma-divider": "^0.2.0",
|
"bulma-divider": "^0.2.0",
|
||||||
"core-js": "^3.6.4",
|
|
||||||
"date-fns": "^2.16.0",
|
"date-fns": "^2.16.0",
|
||||||
"date-fns-tz": "^1.1.6",
|
"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",
|
"graphql-tag": "^2.10.3",
|
||||||
|
"hammerjs": "^2.0.8",
|
||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
"leaflet.locatecontrol": "^0.76.0",
|
"leaflet.locatecontrol": "^0.76.0",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"ngeohash": "^0.6.3",
|
"ngeohash": "^0.6.3",
|
||||||
"p-debounce": "^4.0.0",
|
"p-debounce": "^4.0.0",
|
||||||
@ -67,24 +81,28 @@
|
|||||||
"tailwindcss": "^3",
|
"tailwindcss": "^3",
|
||||||
"tippy.js": "^6.2.3",
|
"tippy.js": "^6.2.3",
|
||||||
"unfetch": "^4.2.0",
|
"unfetch": "^4.2.0",
|
||||||
"v-tooltip": "^2.1.3",
|
"vue": "^3.2.37",
|
||||||
"vue": "^2.6.11",
|
"vue-i18n": "9",
|
||||||
"vue-class-component": "^7.2.3",
|
"vue-material-design-icons": "^5.1.2",
|
||||||
"vue-i18n": "^8.14.0",
|
|
||||||
"vue-matomo": "^4.1.0",
|
"vue-matomo": "^4.1.0",
|
||||||
"vue-meta": "^2.3.1",
|
"vue-meta": "^2.3.1",
|
||||||
"vue-plausible": "^1.3.1",
|
"vue-plausible": "^1.3.1",
|
||||||
"vue-property-decorator": "^9.0.0",
|
"vue-router": "4",
|
||||||
"vue-router": "^3.1.6",
|
|
||||||
"vue-scrollto": "^2.17.1",
|
"vue-scrollto": "^2.17.1",
|
||||||
"vue2-leaflet": "^2.0.3",
|
"vue-use-route-query": "^1.1.0",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.1.0",
|
"@histoire/plugin-vue": "^0.10.0",
|
||||||
"@types/jest": "^28.0.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": "^1.5.2",
|
||||||
"@types/leaflet.locatecontrol": "^0.74",
|
"@types/leaflet.locatecontrol": "^0.74",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.1",
|
||||||
"@types/lodash": "^4.14.141",
|
"@types/lodash": "^4.14.141",
|
||||||
"@types/ngeohash": "^0.6.2",
|
"@types/ngeohash": "^0.6.2",
|
||||||
"@types/phoenix": "^1.5.2",
|
"@types/phoenix": "^1.5.2",
|
||||||
@ -93,37 +111,29 @@
|
|||||||
"@types/prosemirror-state": "^1.2.4",
|
"@types/prosemirror-state": "^1.2.4",
|
||||||
"@types/prosemirror-view": "^1.11.4",
|
"@types/prosemirror-view": "^1.11.4",
|
||||||
"@types/sanitize-html": "^2.5.0",
|
"@types/sanitize-html": "^2.5.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.3.0",
|
"@vitejs/plugin-vue": "^3.0.3",
|
||||||
"@typescript-eslint/parser": "^5.3.0",
|
"@vitest/coverage-c8": "^0.23.4",
|
||||||
"@vue/cli-plugin-babel": "~5.0.6",
|
"@vitest/ui": "^0.23.4",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.6",
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
"@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",
|
|
||||||
"@vue/eslint-config-typescript": "^11.0.0",
|
"@vue/eslint-config-typescript": "^11.0.0",
|
||||||
"@vue/test-utils": "^1.1.0",
|
"@vue/test-utils": "^2.0.2",
|
||||||
"@vue/vue2-jest": "^28.0.0",
|
"eslint": "^8.21.0",
|
||||||
"babel-jest": "^28.1.1",
|
|
||||||
"eslint": "^8.2.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^9.1.1",
|
"eslint-plugin-vue": "^9.3.0",
|
||||||
"flush-promises": "^1.0.2",
|
"flush-promises": "^1.0.2",
|
||||||
"jest": "^28.1.1",
|
"histoire": "^0.10.4",
|
||||||
"jest-junit": "^13.0.0",
|
"jsdom": "^20.0.0",
|
||||||
"mock-apollo-client": "^1.1.0",
|
"mock-apollo-client": "^1.1.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"prettier-eslint": "^15.0.1",
|
"prettier-eslint": "^15.0.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.7.1",
|
||||||
"sass": "^1.34.1",
|
"sass": "^1.34.1",
|
||||||
"sass-loader": "^13.0.0",
|
"typescript": "~4.8.3",
|
||||||
"ts-jest": "28",
|
"vite": "^3.0.9",
|
||||||
"typescript": "~4.5.5",
|
"vite-plugin-pwa": "^0.13.0",
|
||||||
"vue-cli-plugin-tailwind": "~3.0.0",
|
"vitest": "^0.23.3",
|
||||||
"vue-i18n-extract": "^2.0.4",
|
"vue-i18n-extract": "^2.0.4"
|
||||||
"vue-template-compiler": "^2.6.11",
|
|
||||||
"webpack-cli": "^4.7.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
107
js/playwright.config.ts
Normal file
@ -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;
|
BIN
js/public/img/categories/arts-small.webp
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
js/public/img/categories/arts.webp
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
js/public/img/categories/business-small.webp
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
js/public/img/categories/business.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
js/public/img/categories/crafts-small.webp
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
js/public/img/categories/crafts.webp
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
js/public/img/categories/film_media-small.webp
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
js/public/img/categories/film_media.webp
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
js/public/img/categories/food_drink-small.webp
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
js/public/img/categories/food_drink.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
js/public/img/categories/games-small.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
js/public/img/categories/games.webp
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
js/public/img/categories/health-small.webp
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
js/public/img/categories/health.webp
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
js/public/img/categories/lgbtq-small.webp
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
js/public/img/categories/lgbtq.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
js/public/img/categories/movements_politics-small.webp
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
js/public/img/categories/movements_politics.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
js/public/img/categories/music-small.webp
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
js/public/img/categories/music.webp
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
js/public/img/categories/outdoors_adventure-small.webp
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
js/public/img/categories/outdoors_adventure.webp
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
js/public/img/categories/party-small.webp
Normal file
After Width: | Height: | Size: 776 B |
BIN
js/public/img/categories/party.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
js/public/img/categories/photography-small.webp
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
js/public/img/categories/photography.webp
Normal file
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 6.4 KiB |
BIN
js/public/img/categories/spirituality_religion_beliefs.webp
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
js/public/img/categories/sports-small.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
js/public/img/categories/sports.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
js/public/img/categories/theatre-small.webp
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
js/public/img/categories/theatre.webp
Normal file
After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 920 B |
BIN
js/public/img/online-event.webp
Normal file
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 725 KiB |
BIN
js/public/img/pics/error.webp
Normal file
After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.2 MiB |
BIN
js/public/img/pics/event_creation.webp
Normal file
After Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 379 KiB |
BIN
js/public/img/pics/footer_1.webp
Normal file
After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 359 KiB |
BIN
js/public/img/pics/footer_2.webp
Normal file
After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 376 KiB |
BIN
js/public/img/pics/footer_3.webp
Normal file
After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 358 KiB |
BIN
js/public/img/pics/footer_4.webp
Normal file
After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 518 KiB |
BIN
js/public/img/pics/footer_5.webp
Normal file
After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.2 MiB |
BIN
js/public/img/pics/group.webp
Normal file
After Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 1.8 MiB |
BIN
js/public/img/pics/homepage.webp
Normal file
After Width: | Height: | Size: 317 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 1.5 MiB |
BIN
js/public/img/pics/realisation.webp
Normal file
After Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 133 KiB |
BIN
js/public/img/pics/rose.webp
Normal file
After Width: | Height: | Size: 21 KiB |
10
js/public/img/shape-1.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!--?xml version="1.0" standalone="no"?-->
|
||||||
|
<svg id="sw-js-blob-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="sw-gradient" x1="0" x2="1" y1="1" y2="0">
|
||||||
|
<stop id="stop1" stop-color="rgba(255, 231.287, 78.545, 0.3)" offset="0%"></stop>
|
||||||
|
<stop id="stop2" stop-color="rgba(254.848, 165.324, 149.009, 0.25)" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path fill="url(#sw-gradient)" d="M18.2,-13.5C23.5,-7.8,27.8,-0.2,27.7,8.7C27.5,17.6,22.9,27.8,14.1,33.9C5.3,39.9,-7.8,41.6,-17.7,36.8C-27.6,32,-34.2,20.7,-37.1,8.4C-39.9,-3.9,-39,-17.2,-32.2,-23.2C-25.4,-29.3,-12.7,-28.3,-3.1,-25.8C6.4,-23.3,12.8,-19.3,18.2,-13.5Z" width="100%" height="100%" transform="translate(50 50)" style="transition: all 0.3s ease 0s;" stroke-width="0" stroke="url(#sw-gradient)"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1015 B |
10
js/public/img/shape-2.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!--?xml version="1.0" standalone="no"?-->
|
||||||
|
<svg id="sw-js-blob-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="sw-gradient" x1="0" x2="1" y1="1" y2="0">
|
||||||
|
<stop id="stop1" stop-color="rgba(181.058, 255, 167.816, 0.2)" offset="0%"></stop>
|
||||||
|
<stop id="stop2" stop-color="rgba(149.009, 254.848, 251.263, 0.25)" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path fill="url(#sw-gradient)" d="M20.2,-14.3C28.2,-6,38.3,2.5,37.6,9.8C36.9,17.1,25.5,23.1,15.5,25.2C5.6,27.3,-2.9,25.4,-11.2,21.9C-19.6,18.4,-27.9,13.3,-30.8,5.6C-33.7,-2.1,-31.2,-12.5,-25.2,-20.4C-19.1,-28.3,-9.6,-33.7,-1.8,-32.3C6.1,-30.9,12.1,-22.7,20.2,-14.3Z" width="100%" height="100%" transform="translate(50 50)" style="transition: all 0.3s ease 0s;" stroke-width="0" stroke="url(#sw-gradient)"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1016 B |
10
js/public/img/shape-3.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!--?xml version="1.0" standalone="no"?-->
|
||||||
|
<svg id="sw-js-blob-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="sw-gradient" x1="0" x2="1" y1="1" y2="0">
|
||||||
|
<stop id="stop1" stop-color="rgba(172.198, 167.816, 255, 0.2)" offset="0%"></stop>
|
||||||
|
<stop id="stop2" stop-color="rgba(236.8, 149.009, 254.848, 0.25)" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path fill="url(#sw-gradient)" d="M25.3,-21.5C29.4,-15.2,26.8,-4.8,23.6,3.8C20.4,12.5,16.5,19.4,10.2,23.2C3.9,27,-4.8,27.6,-12.6,24.5C-20.3,21.4,-27,14.6,-30.1,5.6C-33.2,-3.4,-32.6,-14.4,-26.9,-21.1C-21.3,-27.8,-10.7,-30.1,0,-30.1C10.7,-30.1,21.3,-27.8,25.3,-21.5Z" width="100%" height="100%" transform="translate(50 50)" style="transition: all 0.3s ease 0s;" stroke-width="0" stroke="url(#sw-gradient)"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1013 B |
@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" dir="auto">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
|
||||||
<meta name="server-injected-data" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong
|
|
||||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
|
|
||||||
properly without JavaScript enabled. Please enable it to
|
|
||||||
continue.</strong
|
|
||||||
>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -30,11 +30,6 @@ convert_image () {
|
|||||||
convert -geometry "$resolution"x $file $output
|
convert -geometry "$resolution"x $file $output
|
||||||
}
|
}
|
||||||
|
|
||||||
produce_webp () {
|
|
||||||
name=$(file_name)
|
|
||||||
output="$output_dir/$name.webp"
|
|
||||||
cwebp $file -quiet -o $output
|
|
||||||
}
|
|
||||||
|
|
||||||
progress() {
|
progress() {
|
||||||
local w=80 p=$1; shift
|
local w=80 p=$1; shift
|
||||||
@ -68,23 +63,3 @@ do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo -e "\nDone!"
|
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!"
|
|
462
js/src/App.vue
@ -1,267 +1,277 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="mobilizon">
|
<div id="mobilizon">
|
||||||
<VueAnnouncer />
|
<!-- <VueAnnouncer />
|
||||||
<VueSkipTo to="#main" :label="$t('Skip to main content')" />
|
<VueSkipTo to="#main" :label="t('Skip to main content')" /> -->
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div v-if="config && config.demoMode">
|
<div v-if="isDemoMode">
|
||||||
<b-message
|
<o-notification
|
||||||
class="container"
|
class="container mx-auto"
|
||||||
type="is-danger"
|
variant="danger"
|
||||||
:title="$t('Warning').toLocaleUpperCase()"
|
:title="t('Warning').toLocaleUpperCase()"
|
||||||
closable
|
closable
|
||||||
:aria-close-label="$t('Close')"
|
:aria-close-label="t('Close')"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{{ $t("This is a demonstration site to test Mobilizon.") }}
|
{{ t("This is a demonstration site to test Mobilizon.") }}
|
||||||
<b>{{ $t("Please do not use it in any real way.") }}</b>
|
<b>{{ t("Please do not use it in any real way.") }}</b>
|
||||||
{{
|
{{
|
||||||
$t(
|
t(
|
||||||
"This website isn't moderated and the data that you enter will be automatically destroyed every day at 00:01 (Paris timezone)."
|
"This website isn't moderated and the data that you enter will be automatically destroyed every day at 00:01 (Paris timezone)."
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</b-message>
|
</o-notification>
|
||||||
</div>
|
</div>
|
||||||
<error v-if="error" :error="error" />
|
<ErrorComponent v-if="error" :error="error" />
|
||||||
|
|
||||||
<main id="main" v-else>
|
<main id="main" class="pt-4" v-else>
|
||||||
<transition name="fade" mode="out-in">
|
<router-view></router-view>
|
||||||
<router-view ref="routerView" />
|
|
||||||
</transition>
|
|
||||||
</main>
|
</main>
|
||||||
<mobilizon-footer />
|
<mobilizon-footer />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
|
import NavBar from "@/components/NavBar.vue";
|
||||||
import NavBar from "./components/NavBar.vue";
|
|
||||||
import {
|
import {
|
||||||
AUTH_ACCESS_TOKEN,
|
AUTH_ACCESS_TOKEN,
|
||||||
AUTH_USER_EMAIL,
|
AUTH_USER_EMAIL,
|
||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
AUTH_USER_ROLE,
|
AUTH_USER_ROLE,
|
||||||
} from "./constants";
|
} from "@/constants";
|
||||||
import {
|
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||||
CURRENT_USER_CLIENT,
|
import MobilizonFooter from "@/components/PageFooter.vue";
|
||||||
UPDATE_CURRENT_USER_CLIENT,
|
|
||||||
} from "./graphql/user";
|
|
||||||
import Footer from "./components/Footer.vue";
|
|
||||||
import Logo from "./components/Logo.vue";
|
|
||||||
import { initializeCurrentActor } from "./utils/auth";
|
|
||||||
import { CONFIG } from "./graphql/config";
|
|
||||||
import { IConfig } from "./types/config.model";
|
|
||||||
import { ICurrentUser } from "./types/current-user.model";
|
|
||||||
import jwt_decode, { JwtPayload } from "jwt-decode";
|
import jwt_decode, { JwtPayload } from "jwt-decode";
|
||||||
import { refreshAccessToken } from "./apollo/utils";
|
import { refreshAccessToken } from "@/apollo/utils";
|
||||||
import { Route } from "vue-router";
|
import {
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
provide,
|
||||||
|
onUnmounted,
|
||||||
|
onMounted,
|
||||||
|
onBeforeMount,
|
||||||
|
inject,
|
||||||
|
defineAsyncComponent,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
|
import { LocationType } from "@/types/user-location.model";
|
||||||
|
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||||
|
import { initializeCurrentActor } from "@/utils/identity";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { Snackbar } from "@/plugins/snackbar";
|
||||||
|
import { Notifier } from "@/plugins/notifier";
|
||||||
|
import { CONFIG } from "@/graphql/config";
|
||||||
|
import { IConfig } from "@/types/config.model";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
@Component({
|
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
||||||
apollo: {
|
|
||||||
currentUser: CURRENT_USER_CLIENT,
|
const config = computed(() => configResult.value?.config);
|
||||||
config: CONFIG,
|
|
||||||
},
|
const ErrorComponent = defineAsyncComponent(
|
||||||
components: {
|
() => import("@/components/ErrorComponent.vue")
|
||||||
Logo,
|
);
|
||||||
NavBar,
|
|
||||||
error: () =>
|
const { t } = useI18n({ useScope: "global" });
|
||||||
import(/* webpackChunkName: "editor" */ "./components/Error.vue"),
|
|
||||||
"mobilizon-footer": Footer,
|
const location = computed(() => config.value?.location);
|
||||||
},
|
|
||||||
metaInfo() {
|
const userLocation = reactive<LocationType>({
|
||||||
return {
|
lon: undefined,
|
||||||
titleTemplate: "%s | Mobilizon",
|
lat: undefined,
|
||||||
|
name: undefined,
|
||||||
|
picture: undefined,
|
||||||
|
isIPLocation: true,
|
||||||
|
accuracy: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateUserLocation = (newLocation: LocationType) => {
|
||||||
|
userLocation.lat = newLocation.lat;
|
||||||
|
userLocation.lon = newLocation.lon;
|
||||||
|
userLocation.name = newLocation.name;
|
||||||
|
userLocation.picture = newLocation.picture;
|
||||||
|
userLocation.isIPLocation = newLocation.isIPLocation;
|
||||||
|
userLocation.accuracy = newLocation.accuracy;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateUserLocation({
|
||||||
|
lat: location.value?.latitude,
|
||||||
|
lon: location.value?.longitude,
|
||||||
|
name: "", // config.ipLocation.country.name,
|
||||||
|
isIPLocation: true,
|
||||||
|
accuracy: 150, // config.ipLocation.location.accuracy_radius * 1.5 || 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
provide("userLocation", {
|
||||||
|
userLocation,
|
||||||
|
updateUserLocation,
|
||||||
|
});
|
||||||
|
|
||||||
|
// const routerView = ref("routerView");
|
||||||
|
const error = ref<Error | null>(null);
|
||||||
|
const online = ref(true);
|
||||||
|
const interval = ref<number>(0);
|
||||||
|
|
||||||
|
const notifier = inject<Notifier>("notifier");
|
||||||
|
|
||||||
|
interval.value = setInterval(async () => {
|
||||||
|
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||||
|
if (accessToken) {
|
||||||
|
const token = jwt_decode<JwtPayload>(accessToken);
|
||||||
|
if (
|
||||||
|
token?.exp !== undefined &&
|
||||||
|
new Date(token.exp * 1000 - 60000) < new Date()
|
||||||
|
) {
|
||||||
|
refreshAccessToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 60000) as unknown as number;
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
if (initializeCurrentUser()) {
|
||||||
|
await initializeCurrentActor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const snackbar = inject<Snackbar>("snackbar");
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
online.value = window.navigator.onLine;
|
||||||
|
window.addEventListener("offline", () => {
|
||||||
|
online.value = false;
|
||||||
|
showOfflineNetworkWarning();
|
||||||
|
console.debug("offline");
|
||||||
|
});
|
||||||
|
window.addEventListener("online", () => {
|
||||||
|
online.value = true;
|
||||||
|
console.debug("online");
|
||||||
|
});
|
||||||
|
document.addEventListener("refreshApp", (event: Event) => {
|
||||||
|
snackbar?.open({
|
||||||
|
queue: false,
|
||||||
|
indefinite: true,
|
||||||
|
variant: "dark",
|
||||||
|
actionText: t("Update app"),
|
||||||
|
cancelText: t("Ignore"),
|
||||||
|
message: t("A new version is available."),
|
||||||
|
onAction: async () => {
|
||||||
|
const registration = event.detail as ServiceWorkerRegistration;
|
||||||
|
try {
|
||||||
|
await refreshApp(registration);
|
||||||
|
window.location.reload();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
notifier?.error(t("An error has occured while refreshing the page."));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(interval.value);
|
||||||
|
interval.value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: updateCurrentUser } = useMutation(UPDATE_CURRENT_USER_CLIENT);
|
||||||
|
|
||||||
|
const initializeCurrentUser = () => {
|
||||||
|
const userId = localStorage.getItem(AUTH_USER_ID);
|
||||||
|
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
|
||||||
|
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||||
|
const role = localStorage.getItem(AUTH_USER_ROLE);
|
||||||
|
|
||||||
|
if (userId && userEmail && accessToken && role) {
|
||||||
|
updateCurrentUser({
|
||||||
|
id: userId,
|
||||||
|
email: userEmail,
|
||||||
|
isLoggedIn: true,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshApp = async (
|
||||||
|
registration: ServiceWorkerRegistration
|
||||||
|
): Promise<any> => {
|
||||||
|
const worker = registration.waiting;
|
||||||
|
if (!worker) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
|
channel.port1.onmessage = (event) => {
|
||||||
|
if (event.data.error) {
|
||||||
|
reject(event.data);
|
||||||
|
} else {
|
||||||
|
resolve(event.data);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
worker?.postMessage({ type: "skip-waiting" }, [channel.port2]);
|
||||||
})
|
});
|
||||||
export default class App extends Vue {
|
};
|
||||||
config!: IConfig;
|
|
||||||
|
|
||||||
currentUser!: ICurrentUser;
|
const showOfflineNetworkWarning = (): void => {
|
||||||
|
notifier?.error(t("You are offline"));
|
||||||
|
};
|
||||||
|
// const extractPageTitleFromRoute = (routeWatched: RouteLocation): string => {
|
||||||
|
// if (routeWatched.meta?.announcer?.message) {
|
||||||
|
// return routeWatched.meta?.announcer?.message();
|
||||||
|
// }
|
||||||
|
// return document.title;
|
||||||
|
// };
|
||||||
|
|
||||||
error: Error | null = null;
|
// watch(route, (routeWatched) => {
|
||||||
|
// const pageTitle = extractPageTitleFromRoute(routeWatched);
|
||||||
|
// if (pageTitle) {
|
||||||
|
// // this.$announcer.polite(
|
||||||
|
// // t("Navigated to {pageTitle}", {
|
||||||
|
// // pageTitle,
|
||||||
|
// // }) as string
|
||||||
|
// // );
|
||||||
|
// }
|
||||||
|
// // Set the focus to the router view
|
||||||
|
// // https://marcus.io/blog/accessible-routing-vuejs
|
||||||
|
// setTimeout(() => {
|
||||||
|
// const focusTarget = (
|
||||||
|
// routerView.value?.$refs?.componentFocusTarget !== undefined
|
||||||
|
// ? routerView.value?.$refs?.componentFocusTarget
|
||||||
|
// : routerView.value?.$el
|
||||||
|
// ) as HTMLElement;
|
||||||
|
// if (focusTarget && focusTarget instanceof Element) {
|
||||||
|
// // Make focustarget programmatically focussable
|
||||||
|
// focusTarget.setAttribute("tabindex", "-1");
|
||||||
|
|
||||||
online = true;
|
// // Focus element
|
||||||
|
// focusTarget.focus();
|
||||||
|
|
||||||
interval: number | undefined = undefined;
|
// // Remove tabindex from focustarget.
|
||||||
|
// // Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk
|
||||||
|
// focusTarget.removeAttribute("tabindex");
|
||||||
|
// }
|
||||||
|
// }, 0);
|
||||||
|
// });
|
||||||
|
|
||||||
@Ref("routerView") routerView!: Vue;
|
const router = useRouter();
|
||||||
|
|
||||||
async created(): Promise<void> {
|
watch(config, async (configWatched: IConfig | undefined) => {
|
||||||
if (await this.initializeCurrentUser()) {
|
if (configWatched) {
|
||||||
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
const { statistics } = await import("@/services/statistics");
|
||||||
}
|
statistics(configWatched?.analytics, {
|
||||||
}
|
router,
|
||||||
|
version: configWatched.version,
|
||||||
errorCaptured(error: Error): void {
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initializeCurrentUser() {
|
|
||||||
const userId = localStorage.getItem(AUTH_USER_ID);
|
|
||||||
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
|
|
||||||
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
|
||||||
const role = localStorage.getItem(AUTH_USER_ROLE);
|
|
||||||
|
|
||||||
if (userId && userEmail && accessToken && role) {
|
|
||||||
return this.$apollo.mutate({
|
|
||||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
|
||||||
variables: {
|
|
||||||
id: userId,
|
|
||||||
email: userEmail,
|
|
||||||
isLoggedIn: true,
|
|
||||||
role,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted(): void {
|
|
||||||
this.online = window.navigator.onLine;
|
|
||||||
window.addEventListener("offline", () => {
|
|
||||||
this.online = false;
|
|
||||||
this.showOfflineNetworkWarning();
|
|
||||||
console.debug("offline");
|
|
||||||
});
|
|
||||||
window.addEventListener("online", () => {
|
|
||||||
this.online = true;
|
|
||||||
console.debug("online");
|
|
||||||
});
|
|
||||||
document.addEventListener("refreshApp", (event: Event) => {
|
|
||||||
this.$buefy.snackbar.open({
|
|
||||||
queue: false,
|
|
||||||
indefinite: true,
|
|
||||||
type: "is-secondary",
|
|
||||||
actionText: this.$t("Update app") as string,
|
|
||||||
cancelText: this.$t("Ignore") as string,
|
|
||||||
message: this.$t("A new version is available.") as string,
|
|
||||||
onAction: async () => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
const detail = event.detail;
|
|
||||||
const registration = detail as ServiceWorkerRegistration;
|
|
||||||
try {
|
|
||||||
await this.refreshApp(registration);
|
|
||||||
window.location.reload();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.$notifier.error(
|
|
||||||
this.$t(
|
|
||||||
"An error has occured while refreshing the page."
|
|
||||||
) as string
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.interval = setInterval(async () => {
|
|
||||||
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
|
||||||
if (accessToken) {
|
|
||||||
const token = jwt_decode<JwtPayload>(accessToken);
|
|
||||||
if (
|
|
||||||
token?.exp !== undefined &&
|
|
||||||
new Date(token.exp * 1000 - 60000) < new Date()
|
|
||||||
) {
|
|
||||||
refreshAccessToken(this.$apollo.getClient());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshApp(
|
|
||||||
registration: ServiceWorkerRegistration
|
|
||||||
): Promise<any> {
|
|
||||||
const worker = registration.waiting;
|
|
||||||
if (!worker) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
console.debug("Doing worker.skipWaiting().");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const channel = new MessageChannel();
|
|
||||||
|
|
||||||
channel.port1.onmessage = (event) => {
|
|
||||||
console.debug("Done worker.skipWaiting().");
|
|
||||||
if (event.data.error) {
|
|
||||||
reject(event.data);
|
|
||||||
} else {
|
|
||||||
resolve(event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
console.debug("calling skip waiting");
|
|
||||||
worker?.postMessage({ type: "skip-waiting" }, [channel.port2]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
showOfflineNetworkWarning(): void {
|
const isDemoMode = computed(() => config.value?.demoMode);
|
||||||
this.$notifier.error(this.$t("You are offline") as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
unmounted(): void {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
this.interval = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("config")
|
|
||||||
async initializeStatistics(config: IConfig) {
|
|
||||||
if (config) {
|
|
||||||
const { statistics } = (await import("./services/statistics")) as {
|
|
||||||
statistics: (config: IConfig, environment: Record<string, any>) => void;
|
|
||||||
};
|
|
||||||
statistics(config, { router: this.$router, version: config.version });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("$route", { immediate: true })
|
|
||||||
updateAnnouncement(route: Route): void {
|
|
||||||
const pageTitle = this.extractPageTitleFromRoute(route);
|
|
||||||
if (pageTitle) {
|
|
||||||
this.$announcer.polite(
|
|
||||||
this.$t("Navigated to {pageTitle}", {
|
|
||||||
pageTitle,
|
|
||||||
}) as string
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Set the focus to the router view
|
|
||||||
// https://marcus.io/blog/accessible-routing-vuejs
|
|
||||||
setTimeout(() => {
|
|
||||||
const focusTarget = (
|
|
||||||
this.routerView?.$refs?.componentFocusTarget !== undefined
|
|
||||||
? this.routerView?.$refs?.componentFocusTarget
|
|
||||||
: this.routerView?.$el
|
|
||||||
) as HTMLElement;
|
|
||||||
if (focusTarget && focusTarget instanceof Element) {
|
|
||||||
// Make focustarget programmatically focussable
|
|
||||||
focusTarget.setAttribute("tabindex", "-1");
|
|
||||||
|
|
||||||
// Focus element
|
|
||||||
focusTarget.focus();
|
|
||||||
|
|
||||||
// Remove tabindex from focustarget.
|
|
||||||
// Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk
|
|
||||||
focusTarget.removeAttribute("tabindex");
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
extractPageTitleFromRoute(route: Route): string {
|
|
||||||
if (route.meta?.announcer?.message) {
|
|
||||||
return route.meta?.announcer?.message();
|
|
||||||
}
|
|
||||||
return document.title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "variables";
|
|
||||||
|
|
||||||
/* Icons */
|
|
||||||
$mdi-font-path: "~@mdi/font/fonts";
|
|
||||||
@import "~@mdi/font/scss/materialdesignicons";
|
|
||||||
@import "common";
|
|
||||||
|
|
||||||
#mobilizon {
|
#mobilizon {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -14,7 +14,8 @@ export const MOBILIZON_INSTANCE_HOST = window.location.hostname;
|
|||||||
*
|
*
|
||||||
* Example: https://framameet.org
|
* Example: https://framameet.org
|
||||||
*/
|
*/
|
||||||
export const GRAPHQL_API_ENDPOINT = window.location.origin;
|
export const GRAPHQL_API_ENDPOINT =
|
||||||
|
import.meta.env.VITE_SERVER_URL ?? window.location.origin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL with path on which the API is. Replaces GRAPHQL_API_ENDPOINT if used
|
* URL with path on which the API is. Replaces GRAPHQL_API_ENDPOINT if used
|
||||||
@ -23,4 +24,4 @@ export const GRAPHQL_API_ENDPOINT = window.location.origin;
|
|||||||
*
|
*
|
||||||
* Example: https://framameet.org/api
|
* Example: https://framameet.org/api
|
||||||
*/
|
*/
|
||||||
export const GRAPHQL_API_FULL_PATH = `${window.location.origin}/api`;
|
export const GRAPHQL_API_FULL_PATH = `${GRAPHQL_API_ENDPOINT}/api`;
|
||||||
|
25
js/src/apollo/absinthe-socket-link.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Socket as PhoenixSocket } from "phoenix";
|
||||||
|
import { create } from "@absinthe/socket";
|
||||||
|
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
|
||||||
|
import { AUTH_ACCESS_TOKEN } from "@/constants";
|
||||||
|
import { GRAPHQL_API_ENDPOINT } from "@/api/_entrypoint";
|
||||||
|
|
||||||
|
const httpServer = GRAPHQL_API_ENDPOINT || "http://localhost:4000";
|
||||||
|
|
||||||
|
const webSocketPrefix = import.meta.env.PROD ? "wss" : "ws";
|
||||||
|
const wsEndpoint = `${webSocketPrefix}${httpServer.substring(
|
||||||
|
httpServer.indexOf(":")
|
||||||
|
)}/graphql_socket`;
|
||||||
|
|
||||||
|
const phoenixSocket = new PhoenixSocket(wsEndpoint, {
|
||||||
|
params: () => {
|
||||||
|
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||||
|
if (token) {
|
||||||
|
return { token };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const absintheSocket = create(phoenixSocket);
|
||||||
|
export default createAbsintheSocketLink(absintheSocket);
|
20
js/src/apollo/absinthe-upload-socket-link.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import fetch from "unfetch";
|
||||||
|
import { createLink } from "apollo-absinthe-upload-link";
|
||||||
|
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from "@/api/_entrypoint";
|
||||||
|
|
||||||
|
// Endpoints
|
||||||
|
const httpServer = GRAPHQL_API_ENDPOINT || "http://localhost:4000";
|
||||||
|
const httpEndpoint = GRAPHQL_API_FULL_PATH || `${httpServer}/api`;
|
||||||
|
|
||||||
|
const customFetch = async (uri: string, options: any) => {
|
||||||
|
const response = await fetch(uri, options);
|
||||||
|
if (response.status >= 400) {
|
||||||
|
return Promise.reject(response.status);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadLink = createLink({
|
||||||
|
uri: httpEndpoint,
|
||||||
|
fetch: customFetch,
|
||||||
|
});
|
23
js/src/apollo/auth.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { AUTH_ACCESS_TOKEN } from "@/constants";
|
||||||
|
import { ApolloLink } from "@apollo/client/core";
|
||||||
|
|
||||||
|
export function generateTokenHeader() {
|
||||||
|
const token = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||||
|
|
||||||
|
return token ? `Bearer ${token}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authMiddleware = new ApolloLink((operation, forward) => {
|
||||||
|
// add the authorization to the headers
|
||||||
|
operation.setContext({
|
||||||
|
headers: {
|
||||||
|
authorization: generateTokenHeader(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (forward) return forward(operation);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export { authMiddleware };
|
101
js/src/apollo/error-link.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { logout } from "@/utils/auth";
|
||||||
|
import { onError } from "@apollo/client/link/error";
|
||||||
|
import { fromPromise } from "@apollo/client/core";
|
||||||
|
import { refreshAccessToken } from "./utils";
|
||||||
|
import { GraphQLError } from "graphql";
|
||||||
|
import { generateTokenHeader } from "./auth";
|
||||||
|
|
||||||
|
let isRefreshing = false;
|
||||||
|
let pendingRequests: any[] = [];
|
||||||
|
|
||||||
|
const resolvePendingRequests = () => {
|
||||||
|
pendingRequests.map((callback) => callback());
|
||||||
|
pendingRequests = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAuthError = (graphQLError: GraphQLError | undefined) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
return graphQLError && [403, 401].includes(graphQLError.status_code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorLink = onError(
|
||||||
|
({ graphQLErrors, networkError, forward, operation }) => {
|
||||||
|
console.debug("We have an apollo error", [graphQLErrors, networkError]);
|
||||||
|
if (
|
||||||
|
graphQLErrors?.some((graphQLError) => isAuthError(graphQLError)) ||
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
networkError === 401
|
||||||
|
) {
|
||||||
|
console.debug("It's a authorization error (statusCode 401)");
|
||||||
|
let forwardOperation;
|
||||||
|
|
||||||
|
if (!isRefreshing) {
|
||||||
|
console.debug("Setting isRefreshing to true");
|
||||||
|
isRefreshing = true;
|
||||||
|
|
||||||
|
forwardOperation = fromPromise(
|
||||||
|
refreshAccessToken()
|
||||||
|
.then((res) => {
|
||||||
|
if (res !== true) {
|
||||||
|
// failed to refresh the token
|
||||||
|
throw "Failed to refresh the token";
|
||||||
|
}
|
||||||
|
resolvePendingRequests();
|
||||||
|
|
||||||
|
const context = operation.getContext();
|
||||||
|
const oldHeaders = context.headers;
|
||||||
|
|
||||||
|
operation.setContext({
|
||||||
|
headers: {
|
||||||
|
...oldHeaders,
|
||||||
|
authorization: generateTokenHeader(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.debug("Something failed, let's logout", e);
|
||||||
|
pendingRequests = [];
|
||||||
|
// don't perform a logout since we don't have any working access/refresh tokens
|
||||||
|
logout(false);
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isRefreshing = false;
|
||||||
|
})
|
||||||
|
).filter((value) => Boolean(value));
|
||||||
|
} else {
|
||||||
|
forwardOperation = fromPromise(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
pendingRequests.push(() => resolve());
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return forwardOperation.flatMap(() => forward(operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphQLErrors) {
|
||||||
|
graphQLErrors.map(
|
||||||
|
(graphQLError: GraphQLError & { status_code?: number }) => {
|
||||||
|
if (graphQLError?.status_code !== 401) {
|
||||||
|
console.debug(
|
||||||
|
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkError) {
|
||||||
|
console.error(`[Network error]: ${networkError}`);
|
||||||
|
console.debug(JSON.stringify(networkError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default errorLink;
|
40
js/src/apollo/link.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { split } from "@apollo/client/core";
|
||||||
|
import { RetryLink } from "@apollo/client/link/retry";
|
||||||
|
import { getMainDefinition } from "@apollo/client/utilities";
|
||||||
|
import absintheSocketLink from "./absinthe-socket-link";
|
||||||
|
import { authMiddleware } from "./auth";
|
||||||
|
import errorLink from "./error-link";
|
||||||
|
import { uploadLink } from "./absinthe-upload-socket-link";
|
||||||
|
|
||||||
|
let link;
|
||||||
|
|
||||||
|
// The Absinthe socket Apollo link relies on an old library
|
||||||
|
// (@jumpn/utils-composite) which itself relies on an old
|
||||||
|
// Babel version, which is incompatible with Histoire.
|
||||||
|
// We just don't use the absinthe apollo socket link
|
||||||
|
// in this case.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
if (!import.meta.env.VITE_HISTOIRE_ENV) {
|
||||||
|
// const absintheSocketLink = await import("./absinthe-socket-link");
|
||||||
|
|
||||||
|
link = split(
|
||||||
|
// split based on operation type
|
||||||
|
({ query }) => {
|
||||||
|
const definition = getMainDefinition(query);
|
||||||
|
return (
|
||||||
|
definition.kind === "OperationDefinition" &&
|
||||||
|
definition.operation === "subscription"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
absintheSocketLink,
|
||||||
|
uploadLink
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryLink = new RetryLink();
|
||||||
|
|
||||||
|
export const fullLink = authMiddleware
|
||||||
|
.concat(retryLink)
|
||||||
|
.concat(errorLink)
|
||||||
|
.concat(link ?? uploadLink);
|
14
js/src/apollo/memory.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defaultDataIdFromObject, InMemoryCache } from "@apollo/client/core";
|
||||||
|
import { possibleTypes, typePolicies } from "./utils";
|
||||||
|
|
||||||
|
export const cache = new InMemoryCache({
|
||||||
|
addTypename: true,
|
||||||
|
typePolicies,
|
||||||
|
possibleTypes,
|
||||||
|
dataIdFromObject: (object: any) => {
|
||||||
|
if (object.__typename === "Address") {
|
||||||
|
return object.origin_id;
|
||||||
|
}
|
||||||
|
return defaultDataIdFromObject(object);
|
||||||
|
},
|
||||||
|
});
|
@ -1,4 +1,5 @@
|
|||||||
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||||
|
import { CURRENT_USER_LOCATION_CLIENT } from "@/graphql/location";
|
||||||
import { CURRENT_USER_CLIENT } from "@/graphql/user";
|
import { CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||||
import { ICurrentUserRole } from "@/types/enums";
|
import { ICurrentUserRole } from "@/types/enums";
|
||||||
import { ApolloCache, NormalizedCacheObject } from "@apollo/client/cache";
|
import { ApolloCache, NormalizedCacheObject } from "@apollo/client/cache";
|
||||||
@ -7,7 +8,7 @@ import { Resolvers } from "@apollo/client/core/types";
|
|||||||
export default function buildCurrentUserResolver(
|
export default function buildCurrentUserResolver(
|
||||||
cache: ApolloCache<NormalizedCacheObject>
|
cache: ApolloCache<NormalizedCacheObject>
|
||||||
): Resolvers {
|
): Resolvers {
|
||||||
cache.writeQuery({
|
cache?.writeQuery({
|
||||||
query: CURRENT_USER_CLIENT,
|
query: CURRENT_USER_CLIENT,
|
||||||
data: {
|
data: {
|
||||||
currentUser: {
|
currentUser: {
|
||||||
@ -20,7 +21,7 @@ export default function buildCurrentUserResolver(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
cache.writeQuery({
|
cache?.writeQuery({
|
||||||
query: CURRENT_ACTOR_CLIENT,
|
query: CURRENT_ACTOR_CLIENT,
|
||||||
data: {
|
data: {
|
||||||
currentActor: {
|
currentActor: {
|
||||||
@ -33,6 +34,20 @@ export default function buildCurrentUserResolver(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cache?.writeQuery({
|
||||||
|
query: CURRENT_USER_LOCATION_CLIENT,
|
||||||
|
data: {
|
||||||
|
currentUserLocation: {
|
||||||
|
lat: null,
|
||||||
|
lon: null,
|
||||||
|
accuracy: null,
|
||||||
|
isIPLocation: null,
|
||||||
|
name: null,
|
||||||
|
picture: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
updateCurrentUser: (
|
updateCurrentUser: (
|
||||||
@ -84,6 +99,39 @@ export default function buildCurrentUserResolver(
|
|||||||
|
|
||||||
localCache.writeQuery({ data, query: CURRENT_ACTOR_CLIENT });
|
localCache.writeQuery({ data, query: CURRENT_ACTOR_CLIENT });
|
||||||
},
|
},
|
||||||
|
updateCurrentUserLocation: (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
accuracy,
|
||||||
|
isIPLocation,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
}: {
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
accuracy: number;
|
||||||
|
isIPLocation: boolean;
|
||||||
|
name: string;
|
||||||
|
picture: any;
|
||||||
|
},
|
||||||
|
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
|
||||||
|
) => {
|
||||||
|
const data = {
|
||||||
|
currentUserLocation: {
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
accuracy,
|
||||||
|
isIPLocation,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
__typename: "CurrentUserLocation",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
localCache.writeQuery({ data, query: CURRENT_USER_LOCATION_CLIENT });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,19 +4,16 @@ import { IFollower } from "@/types/actor/follower.model";
|
|||||||
import { IParticipant } from "@/types/participant.model";
|
import { IParticipant } from "@/types/participant.model";
|
||||||
import { Paginate } from "@/types/paginate";
|
import { Paginate } from "@/types/paginate";
|
||||||
import { saveTokenData } from "@/utils/auth";
|
import { saveTokenData } from "@/utils/auth";
|
||||||
import {
|
import { FieldPolicy, Reference, TypePolicies } from "@apollo/client/core";
|
||||||
ApolloClient,
|
|
||||||
FieldPolicy,
|
|
||||||
NormalizedCacheObject,
|
|
||||||
Reference,
|
|
||||||
TypePolicies,
|
|
||||||
} from "@apollo/client/core";
|
|
||||||
import introspectionQueryResultData from "../../fragmentTypes.json";
|
import introspectionQueryResultData from "../../fragmentTypes.json";
|
||||||
import { IMember } from "@/types/actor/member.model";
|
import { IMember } from "@/types/actor/member.model";
|
||||||
import { IComment } from "@/types/comment.model";
|
import { IComment } from "@/types/comment.model";
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent } from "@/types/event.model";
|
||||||
import { IActivity } from "@/types/activity.model";
|
import { IActivity } from "@/types/activity.model";
|
||||||
import uniqBy from "lodash/uniqBy";
|
import uniqBy from "lodash/uniqBy";
|
||||||
|
import { provideApolloClient, useMutation } from "@vue/apollo-composable";
|
||||||
|
import { apolloClient } from "@/vue-apollo";
|
||||||
|
import { IToken } from "@/types/login.model";
|
||||||
|
|
||||||
type possibleTypes = { name: string };
|
type possibleTypes = { name: string };
|
||||||
type schemaType = {
|
type schemaType = {
|
||||||
@ -73,6 +70,12 @@ export const typePolicies: TypePolicies = {
|
|||||||
Instance: {
|
Instance: {
|
||||||
keyFields: ["domain"],
|
keyFields: ["domain"],
|
||||||
},
|
},
|
||||||
|
Config: {
|
||||||
|
merge: true,
|
||||||
|
},
|
||||||
|
Address: {
|
||||||
|
keyFields: ["id"],
|
||||||
|
},
|
||||||
RootQueryType: {
|
RootQueryType: {
|
||||||
fields: {
|
fields: {
|
||||||
relayFollowers: paginatedLimitPagination<IFollower>(),
|
relayFollowers: paginatedLimitPagination<IFollower>(),
|
||||||
@ -99,9 +102,7 @@ export const typePolicies: TypePolicies = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function refreshAccessToken(
|
export async function refreshAccessToken(): Promise<boolean> {
|
||||||
apolloClient: ApolloClient<NormalizedCacheObject>
|
|
||||||
): Promise<boolean> {
|
|
||||||
// Remove invalid access token, so the next request is not authenticated
|
// Remove invalid access token, so the next request is not authenticated
|
||||||
localStorage.removeItem(AUTH_ACCESS_TOKEN);
|
localStorage.removeItem(AUTH_ACCESS_TOKEN);
|
||||||
|
|
||||||
@ -112,23 +113,30 @@ export async function refreshAccessToken(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Refreshing access token.");
|
console.debug("Refreshing access token.");
|
||||||
|
|
||||||
try {
|
return new Promise((resolve, reject) => {
|
||||||
const res = await apolloClient.mutate({
|
const { mutate, onDone, onError } = provideApolloClient(apolloClient)(() =>
|
||||||
mutation: REFRESH_TOKEN,
|
useMutation<{ refreshToken: IToken }>(REFRESH_TOKEN)
|
||||||
variables: {
|
);
|
||||||
refreshToken,
|
|
||||||
},
|
mutate({
|
||||||
|
refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
saveTokenData(res.data.refreshToken);
|
onDone(({ data }) => {
|
||||||
|
if (data?.refreshToken) {
|
||||||
|
saveTokenData(data?.refreshToken);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
reject(false);
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
onError((err) => {
|
||||||
} catch (err) {
|
console.debug("Failed to refresh token", err);
|
||||||
console.debug("Failed to refresh token");
|
reject(false);
|
||||||
return false;
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyArgs = FieldPolicy<any>["keyArgs"];
|
type KeyArgs = FieldPolicy<any>["keyArgs"];
|
||||||
|
280
js/src/assets/oruga-tailwindcss.css
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
body {
|
||||||
|
@apply bg-body-background-color dark:bg-zinc-800 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.btn {
|
||||||
|
@apply font-bold py-2 px-4 bg-mbz-bluegreen hover:bg-mbz-bluegreen-600 text-white rounded h-10 outline-none focus:ring ring-offset-1 ring-offset-slate-50 ring-blue-300;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
@apply text-slate-200;
|
||||||
|
}
|
||||||
|
.btn-rounded {
|
||||||
|
@apply rounded-full;
|
||||||
|
}
|
||||||
|
.btn-outlined-,
|
||||||
|
.btn-outlined-primary {
|
||||||
|
@apply bg-transparent text-black dark:text-white font-semibold py-2 px-4 border border-mbz-bluegreen dark:border-violet-3;
|
||||||
|
}
|
||||||
|
.btn-outlined-:hover,
|
||||||
|
.btn-outlined-primary:hover {
|
||||||
|
@apply font-bold py-2 px-4 bg-mbz-bluegreen dark:bg-violet-3 text-white rounded;
|
||||||
|
}
|
||||||
|
.btn-size-large {
|
||||||
|
@apply text-2xl py-6;
|
||||||
|
}
|
||||||
|
.btn-disabled {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
@apply bg-mbz-danger hover:bg-mbz-danger/90;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
@apply bg-mbz-success;
|
||||||
|
}
|
||||||
|
.btn-text {
|
||||||
|
@apply bg-transparent border-transparent text-black dark:text-white font-normal underline hover:bg-zinc-200 hover:text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field */
|
||||||
|
.field {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
@apply block text-gray-700 dark:text-gray-100 text-base font-bold mb-2;
|
||||||
|
}
|
||||||
|
.field-danger {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o-field.o-field--addons .control:last-child:not(:only-child) .button {
|
||||||
|
@apply inline-flex text-gray-800 bg-gray-200 h-9 mt-[1px] rounded text-center px-2 py-1.5;
|
||||||
|
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-message-info {
|
||||||
|
@apply text-mbz-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-message-danger {
|
||||||
|
@apply text-mbz-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
.input {
|
||||||
|
@apply appearance-none border w-full py-2 px-3 text-black leading-tight dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50;
|
||||||
|
}
|
||||||
|
.input-danger {
|
||||||
|
@apply border-red-500;
|
||||||
|
}
|
||||||
|
.input-icon-right {
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
.input[type="text"]:disabled,
|
||||||
|
.input[type="email"]:disabled {
|
||||||
|
@apply bg-zinc-200 dark:bg-zinc-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-warning {
|
||||||
|
@apply text-amber-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-danger {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
.icon-success {
|
||||||
|
@apply text-mbz-success;
|
||||||
|
}
|
||||||
|
.icon-grey {
|
||||||
|
@apply text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o-input__icon-left {
|
||||||
|
@apply dark:text-black h-10 w-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o-input-iconspace-left {
|
||||||
|
@apply pl-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* InputItems */
|
||||||
|
.inputitems-item {
|
||||||
|
@apply bg-primary mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputitems-item:first-child {
|
||||||
|
@apply ml-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Autocomplete */
|
||||||
|
.autocomplete-menu {
|
||||||
|
@apply max-h-[200px] drop-shadow-md text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-item {
|
||||||
|
@apply py-1.5 px-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.dropdown {
|
||||||
|
@apply inline-flex relative;
|
||||||
|
}
|
||||||
|
.dropdown-menu {
|
||||||
|
min-width: 12em;
|
||||||
|
@apply bg-white dark:bg-zinc-700 shadow-lg rounded text-start py-2;
|
||||||
|
}
|
||||||
|
.dropdown-item {
|
||||||
|
@apply relative inline-flex gap-1 no-underline p-2 cursor-pointer w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item-active {
|
||||||
|
@apply bg-white text-black;
|
||||||
|
}
|
||||||
|
.dropdown-button {
|
||||||
|
@apply inline-flex gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
@apply appearance-none bg-primary border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-checked {
|
||||||
|
@apply bg-primary text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
@apply bg-white dark:bg-zinc-800 rounded px-2 py-4 w-full z-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Switch */
|
||||||
|
.switch {
|
||||||
|
@apply cursor-pointer inline-flex items-center relative mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
@apply pl-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-check-checked {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select */
|
||||||
|
.select {
|
||||||
|
@apply dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50 rounded pl-2 pr-6 border-2 border-transparent h-10 shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Radio */
|
||||||
|
.form-radio {
|
||||||
|
@apply bg-none text-primary accent-primary;
|
||||||
|
}
|
||||||
|
.radio-label {
|
||||||
|
@apply pl-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
button.menubar__button {
|
||||||
|
@apply dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification */
|
||||||
|
.notification {
|
||||||
|
@apply p-7 bg-mbz-yellow-alt-200 dark:bg-mbz-purple-600 text-black dark:text-white rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-primary {
|
||||||
|
@apply bg-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-info {
|
||||||
|
@apply bg-mbz-info text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-warning {
|
||||||
|
@apply bg-amber-600 text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-danger {
|
||||||
|
@apply bg-mbz-danger text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.table tr {
|
||||||
|
@apply odd:bg-white dark:odd:bg-zinc-600 last:border-b-0 even:bg-gray-50 dark:even:bg-zinc-700 border-b rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-td {
|
||||||
|
@apply py-4 px-2 whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-th {
|
||||||
|
@apply p-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-root {
|
||||||
|
@apply mt-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Snackbar */
|
||||||
|
.notification-dark {
|
||||||
|
@apply text-white;
|
||||||
|
background: #363636;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pagination */
|
||||||
|
.pagination {
|
||||||
|
@apply flex items-center text-center justify-between;
|
||||||
|
}
|
||||||
|
.pagination-link {
|
||||||
|
@apply inline-flex items-center relative justify-center cursor-pointer rounded h-10 m-1 p-2 bg-white dark:bg-zinc-300 text-lg text-black;
|
||||||
|
}
|
||||||
|
.pagination-list {
|
||||||
|
@apply flex items-center text-center list-none flex-wrap grow shrink justify-start;
|
||||||
|
}
|
||||||
|
.pagination-next,
|
||||||
|
.pagination-previous {
|
||||||
|
@apply px-3 dark:text-black;
|
||||||
|
}
|
||||||
|
.pagination-link-current {
|
||||||
|
@apply bg-primary cursor-not-allowed pointer-events-none border-primary text-white;
|
||||||
|
}
|
||||||
|
.pagination-ellipsis {
|
||||||
|
@apply text-center m-1 text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tabs */
|
||||||
|
.tabs-nav {
|
||||||
|
@apply flex items-center justify-start pb-0.5;
|
||||||
|
}
|
||||||
|
.tabs-nav-item-boxed {
|
||||||
|
@apply flex items-center justify-center px-2 py-2 rounded-t border-transparent;
|
||||||
|
}
|
||||||
|
.tabs-nav-item-active-boxed {
|
||||||
|
@apply bg-white border-gray-300 text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tooltip */
|
||||||
|
.tooltip-content {
|
||||||
|
@apply bg-zinc-800 text-white dark:bg-zinc-300 dark:text-black rounded py-1 px-2;
|
||||||
|
}
|
||||||
|
.tooltip-arrow {
|
||||||
|
@apply text-zinc-800 dark:text-zinc-200;
|
||||||
|
}
|
||||||
|
.tooltip-content-success {
|
||||||
|
@apply bg-mbz-success text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tiptap editor */
|
||||||
|
.menubar__button {
|
||||||
|
@apply hover:bg-[rgba(0,0,0,.05)];
|
||||||
|
}
|
@ -3,3 +3,45 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-white dark:bg-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-4xl lg:text-5xl leading-none font-extrabold tracking-tight mt-5 mb-4 sm:mt-7 sm:mb-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-xl mt-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.mbz-card {
|
||||||
|
@apply block bg-mbz-yellow-alt-300 hover:bg-mbz-yellow-alt-200 text-violet-title dark:text-white dark:hover:text-white rounded-lg dark:border-violet-title shadow-md dark:bg-mbz-purple dark:hover:dark:bg-mbz-purple-400 dark:text-white dark:hover:text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--oruga-variant-primary: #1e7d97;
|
||||||
|
|
||||||
|
--oruga-field-label-color: white;
|
||||||
|
|
||||||
|
--oruga-table-background-color: #111827;
|
||||||
|
--oruga-table-th-color: white;
|
||||||
|
|
||||||
|
--oruga-modal-content-background-color: #111827;
|
||||||
|
|
||||||
|
--oruga-dropdown-item-color: white;
|
||||||
|
--oruga-dropdown-menu-background: #111827;
|
||||||
|
--oruga-dropdown-item-hover-color: white;
|
||||||
|
--oruga-dropdown-item-hover-background-color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,188 +0,0 @@
|
|||||||
@use "@/styles/_mixins" as *;
|
|
||||||
@import "variables.scss";
|
|
||||||
|
|
||||||
@import "~bulma";
|
|
||||||
@import "~bulma-divider";
|
|
||||||
@import "~buefy/src/scss/buefy";
|
|
||||||
@import "styles/vue-announcer.scss";
|
|
||||||
@import "styles/vue-skip-to.scss";
|
|
||||||
|
|
||||||
a.out,
|
|
||||||
.content a,
|
|
||||||
.ProseMirror a {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-decoration-color: #ed8d07;
|
|
||||||
text-decoration-thickness: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: 1rem 1% 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
$color-black: #000;
|
|
||||||
|
|
||||||
.mention {
|
|
||||||
background: rgba($color-black, 0.1);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0.2rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
@include margin-right(0.2rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mention-suggestion {
|
|
||||||
color: rgba($color-black, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mention .mention {
|
|
||||||
background: initial;
|
|
||||||
@include margin-right(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select select {
|
|
||||||
border-color: $borders;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
}
|
|
||||||
.fade-enter,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: $body-background-color;
|
|
||||||
font-family: BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, "Segoe UI",
|
|
||||||
"Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mobilizon > .container > .message {
|
|
||||||
margin: 1rem auto auto;
|
|
||||||
.message-header {
|
|
||||||
button.delete {
|
|
||||||
background: #4a4a4a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-description {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: $violet-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$list-background-color: $scheme-main !default;
|
|
||||||
$list-shadow: 0 2px 3px rgba($scheme-invert, 0.1),
|
|
||||||
0 0 0 1px rgba($scheme-invert, 0.1) !default;
|
|
||||||
$list-radius: $radius !default;
|
|
||||||
|
|
||||||
$list-item-border: 1px solid $border !default;
|
|
||||||
$list-item-color: $text !default;
|
|
||||||
$list-item-active-background-color: $link !default;
|
|
||||||
$list-item-active-color: $link-invert !default;
|
|
||||||
$list-item-hover-background-color: $background !default;
|
|
||||||
|
|
||||||
.list-item {
|
|
||||||
display: block;
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
&:not(a) {
|
|
||||||
color: $list-item-color;
|
|
||||||
}
|
|
||||||
&:first-child {
|
|
||||||
border-top-left-radius: $list-radius;
|
|
||||||
border-top-right-radius: $list-radius;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
border-bottom-left-radius: $list-radius;
|
|
||||||
border-bottom-right-radius: $list-radius;
|
|
||||||
}
|
|
||||||
&:not(:last-child) {
|
|
||||||
border-bottom: $list-item-border;
|
|
||||||
}
|
|
||||||
&.is-active {
|
|
||||||
background-color: $list-item-active-background-color;
|
|
||||||
color: $list-item-active-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.list-item {
|
|
||||||
background-color: $list-item-hover-background-color;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-title {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
display: inline;
|
|
||||||
background: $secondary;
|
|
||||||
padding: 2px 7.5px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin focus() {
|
|
||||||
&:focus {
|
|
||||||
border: 2px solid black;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.menu-list > li,
|
|
||||||
p {
|
|
||||||
@include focus;
|
|
||||||
}
|
|
||||||
.navbar-item {
|
|
||||||
@include focus;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-dropdown span.navbar-item:hover {
|
|
||||||
background-color: whitesmoke;
|
|
||||||
color: #0a0a0a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bulma/Buefy fixes
|
|
||||||
*/
|
|
||||||
.icon {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags .tag:not(:last-child) {
|
|
||||||
margin-right: unset;
|
|
||||||
@include margin-right(0.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button .icon {
|
|
||||||
&:first-child:not(:last-child) {
|
|
||||||
@include margin-left(calc(-0.5em - 1px));
|
|
||||||
@include margin-right(0.25em);
|
|
||||||
}
|
|
||||||
&:last-child:not(:first-child) {
|
|
||||||
@include margin-right(calc(-0.5em - 1px));
|
|
||||||
@include margin-left(0.25em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons .button:not(:last-child):not(.is-fullwidth) {
|
|
||||||
margin-right: unset;
|
|
||||||
@include margin-right(0.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb li:first-child a {
|
|
||||||
padding-left: unset;
|
|
||||||
@include padding-left(0);
|
|
||||||
@include padding-right(0.75em);
|
|
||||||
}
|
|
||||||
.media-left {
|
|
||||||
@include margin-left(1rem);
|
|
||||||
}
|
|
||||||
a.dropdown-item {
|
|
||||||
@include padding-right(3rem);
|
|
||||||
}
|
|
20
js/src/components/About/InstanceContactLink.story.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<Story>
|
||||||
|
<Variant title="empty">
|
||||||
|
<InstanceContactLink />
|
||||||
|
</Variant>
|
||||||
|
<Variant title="string">
|
||||||
|
<InstanceContactLink contact="someone" />
|
||||||
|
</Variant>
|
||||||
|
<Variant title="email">
|
||||||
|
<InstanceContactLink contact="someone@somewhere.tld" />
|
||||||
|
</Variant>
|
||||||
|
<Variant title="url">
|
||||||
|
<InstanceContactLink contact="https://somewhere.com" />
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import InstanceContactLink from "./InstanceContactLink.vue";
|
||||||
|
</script>
|
@ -4,49 +4,49 @@
|
|||||||
configLink.text
|
configLink.text
|
||||||
}}</a>
|
}}</a>
|
||||||
<span dir="auto" v-else-if="contact">{{ contact }}</span>
|
<span dir="auto" v-else-if="contact">{{ contact }}</span>
|
||||||
<span v-else>{{ $t("contact uninformed") }}</span>
|
<span v-else>{{ t("contact uninformed") }}</span>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
@Component
|
const props = defineProps<{
|
||||||
export default class InstanceContactLink extends Vue {
|
contact?: string;
|
||||||
@Prop({ required: true, type: String }) contact!: string;
|
}>();
|
||||||
|
|
||||||
get configLink(): { uri: string; text: string } | null {
|
const { t } = useI18n({ useScope: "global" });
|
||||||
if (!this.contact) return null;
|
|
||||||
if (this.isContactEmail) {
|
const configLink = computed((): { uri: string; text: string } | null => {
|
||||||
return {
|
if (!props.contact) return null;
|
||||||
uri: `mailto:${this.contact}`,
|
if (isContactEmail.value) {
|
||||||
text: this.contact,
|
return {
|
||||||
};
|
uri: `mailto:${props.contact}`,
|
||||||
}
|
text: props.contact,
|
||||||
if (this.isContactURL) {
|
};
|
||||||
return {
|
}
|
||||||
uri: this.contact,
|
if (isContactURL.value) {
|
||||||
text:
|
return {
|
||||||
InstanceContactLink.urlToHostname(this.contact) ||
|
uri: props.contact,
|
||||||
(this.$t("Contact") as string),
|
text: urlToHostname(props.contact) ?? "Contact",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isContactEmail = computed((): boolean => {
|
||||||
|
return (props.contact ?? "").includes("@");
|
||||||
|
});
|
||||||
|
|
||||||
|
const isContactURL = computed((): boolean => {
|
||||||
|
return (props.contact ?? "").match(/^https?:\/\//g) !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlToHostname = (url: string): string | null => {
|
||||||
|
try {
|
||||||
|
return new URL(url).hostname;
|
||||||
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
get isContactEmail(): boolean {
|
|
||||||
return this.contact.includes("@");
|
|
||||||
}
|
|
||||||
|
|
||||||
get isContactURL(): boolean {
|
|
||||||
return this.contact.match(/^https?:\/\//g) !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static urlToHostname(url: string): string | null {
|
|
||||||
try {
|
|
||||||
return new URL(url).hostname;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|