Add global search
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
bfc936f57c
commit
48935e2168
@ -365,6 +365,14 @@ 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"
|
||||
|
||||
# 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"
|
||||
|
@ -11,7 +11,7 @@ module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-typescript/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
@ -24,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",
|
||||
|
3
js/.gitignore
vendored
3
js/.gitignore
vendored
@ -24,3 +24,6 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
@ -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",
|
||||
|
@ -51,6 +51,7 @@
|
||||
"@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",
|
||||
"@xiaoshuapp/draggable": "^4.1.0",
|
||||
@ -93,6 +94,7 @@
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
|
107
js/playwright.config.ts
Normal file
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;
|
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 920 B |
@ -32,17 +32,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import NavBar from "./components/NavBar.vue";
|
||||
import NavBar from "@/components/NavBar.vue";
|
||||
import {
|
||||
AUTH_ACCESS_TOKEN,
|
||||
AUTH_USER_EMAIL,
|
||||
AUTH_USER_ID,
|
||||
AUTH_USER_ROLE,
|
||||
} from "./constants";
|
||||
import { UPDATE_CURRENT_USER_CLIENT } from "./graphql/user";
|
||||
import MobilizonFooter from "./components/Footer.vue";
|
||||
} from "@/constants";
|
||||
import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
|
||||
import MobilizonFooter from "@/components/PageFooter.vue";
|
||||
import jwt_decode, { JwtPayload } from "jwt-decode";
|
||||
import { refreshAccessToken } from "./apollo/utils";
|
||||
import { refreshAccessToken } from "@/apollo/utils";
|
||||
import {
|
||||
reactive,
|
||||
ref,
|
||||
@ -52,25 +52,30 @@ import {
|
||||
onBeforeMount,
|
||||
inject,
|
||||
defineAsyncComponent,
|
||||
computed,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { LocationType } from "./types/user-location.model";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { initializeCurrentActor } from "./utils/identity";
|
||||
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 {
|
||||
useIsDemoMode,
|
||||
useServerProvidedLocation,
|
||||
} from "./composition/apollo/config";
|
||||
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";
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
||||
|
||||
const config = computed(() => configResult.value?.config);
|
||||
|
||||
const ErrorComponent = defineAsyncComponent(
|
||||
() => import("./components/ErrorComponent.vue")
|
||||
() => import("@/components/ErrorComponent.vue")
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const { location } = useServerProvidedLocation();
|
||||
const location = computed(() => config.value?.location);
|
||||
|
||||
const userLocation = reactive<LocationType>({
|
||||
lon: undefined,
|
||||
@ -251,16 +256,19 @@ const showOfflineNetworkWarning = (): void => {
|
||||
// }, 0);
|
||||
// });
|
||||
|
||||
// watch(config, async (configWatched: IConfig) => {
|
||||
// if (configWatched) {
|
||||
// const { statistics } = (await import("./services/statistics")) as {
|
||||
// statistics: (config: IConfig, environment: Record<string, any>) => void;
|
||||
// };
|
||||
// statistics(configWatched, { router, version: configWatched.version });
|
||||
// }
|
||||
// });
|
||||
const router = useRouter();
|
||||
|
||||
const { isDemoMode } = useIsDemoMode();
|
||||
watch(config, async (configWatched: IConfig | undefined) => {
|
||||
if (configWatched) {
|
||||
const { statistics } = await import("@/services/statistics");
|
||||
statistics(configWatched?.analytics, {
|
||||
router,
|
||||
version: configWatched.version,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const isDemoMode = computed(() => config.value?.demoMode);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -83,7 +83,7 @@ const errorLink = onError(
|
||||
graphQLErrors.map(
|
||||
(graphQLError: GraphQLError & { status_code?: number }) => {
|
||||
if (graphQLError?.status_code !== 401) {
|
||||
console.log(
|
||||
console.debug(
|
||||
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
|
||||
);
|
||||
}
|
||||
|
@ -6,22 +6,35 @@ import { authMiddleware } from "./auth";
|
||||
import errorLink from "./error-link";
|
||||
import { uploadLink } from "./absinthe-upload-socket-link";
|
||||
|
||||
// const link = split(
|
||||
// // split based on operation type
|
||||
// ({ query }) => {
|
||||
// const definition = getMainDefinition(query);
|
||||
// return (
|
||||
// definition.kind === "OperationDefinition" &&
|
||||
// definition.operation === "subscription"
|
||||
// );
|
||||
// },
|
||||
// absintheSocketLink,
|
||||
// uploadLink
|
||||
// );
|
||||
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(uploadLink);
|
||||
.concat(link ?? uploadLink);
|
||||
|
@ -8,7 +8,7 @@ import { Resolvers } from "@apollo/client/core/types";
|
||||
export default function buildCurrentUserResolver(
|
||||
cache: ApolloCache<NormalizedCacheObject>
|
||||
): Resolvers {
|
||||
cache.writeQuery({
|
||||
cache?.writeQuery({
|
||||
query: CURRENT_USER_CLIENT,
|
||||
data: {
|
||||
currentUser: {
|
||||
@ -21,7 +21,7 @@ export default function buildCurrentUserResolver(
|
||||
},
|
||||
});
|
||||
|
||||
cache.writeQuery({
|
||||
cache?.writeQuery({
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
data: {
|
||||
currentActor: {
|
||||
@ -34,7 +34,7 @@ export default function buildCurrentUserResolver(
|
||||
},
|
||||
});
|
||||
|
||||
cache.writeQuery({
|
||||
cache?.writeQuery({
|
||||
query: CURRENT_USER_LOCATION_CLIENT,
|
||||
data: {
|
||||
currentUserLocation: {
|
||||
@ -70,8 +70,6 @@ export default function buildCurrentUserResolver(
|
||||
},
|
||||
};
|
||||
|
||||
console.debug("updating current user", data);
|
||||
|
||||
localCache.writeQuery({ data, query: CURRENT_USER_CLIENT });
|
||||
},
|
||||
updateCurrentActor: (
|
||||
|
@ -73,6 +73,9 @@ export const typePolicies: TypePolicies = {
|
||||
Config: {
|
||||
merge: true,
|
||||
},
|
||||
Address: {
|
||||
keyFields: ["id"],
|
||||
},
|
||||
RootQueryType: {
|
||||
fields: {
|
||||
relayFollowers: paginatedLimitPagination<IFollower>(),
|
||||
@ -110,7 +113,7 @@ export async function refreshAccessToken(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Refreshing access token.");
|
||||
console.debug("Refreshing access token.");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { mutate, onDone, onError } = provideApolloClient(apolloClient)(() =>
|
||||
@ -130,7 +133,7 @@ export async function refreshAccessToken(): Promise<boolean> {
|
||||
});
|
||||
|
||||
onError((err) => {
|
||||
console.debug("Failed to refresh token");
|
||||
console.debug("Failed to refresh token", err);
|
||||
reject(false);
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,10 @@
|
||||
body {
|
||||
@apply bg-body-background-color dark:bg-gray-700 dark:text-white;
|
||||
@apply bg-body-background-color dark:bg-zinc-800 dark:text-white;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
.btn {
|
||||
outline: none !important;
|
||||
@apply font-bold py-2 px-4 bg-mbz-bluegreen dark:bg-violet-3 text-white rounded h-10;
|
||||
@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;
|
||||
@ -28,11 +27,14 @@ body {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
.btn-danger {
|
||||
@apply bg-mbz-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 {
|
||||
@ -62,7 +64,7 @@ body {
|
||||
|
||||
/* Input */
|
||||
.input {
|
||||
@apply appearance-none border w-full py-2 px-3 text-black leading-tight;
|
||||
@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;
|
||||
@ -70,6 +72,10 @@ body {
|
||||
.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;
|
||||
@ -78,6 +84,12 @@ body {
|
||||
.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;
|
||||
@ -111,25 +123,27 @@ body {
|
||||
}
|
||||
.dropdown-menu {
|
||||
min-width: 12em;
|
||||
@apply bg-white dark:bg-gray-700 shadow-lg rounded text-start py-2;
|
||||
@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-violet-2; */
|
||||
@apply bg-white;
|
||||
@apply bg-white text-black;
|
||||
}
|
||||
.dropdown-button {
|
||||
@apply inline-flex gap-1;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
|
||||
.checkbox {
|
||||
@apply appearance-none bg-blue-500 border-blue-500;
|
||||
@apply appearance-none bg-primary border-primary;
|
||||
}
|
||||
|
||||
.checkbox-checked {
|
||||
@apply bg-blue-500;
|
||||
@apply bg-primary text-primary;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
@ -139,7 +153,7 @@ body {
|
||||
/* Modal */
|
||||
|
||||
.modal-content {
|
||||
@apply bg-white dark:bg-gray-700 rounded px-2 py-4 w-full;
|
||||
@apply bg-white dark:bg-zinc-800 rounded px-2 py-4 w-full;
|
||||
}
|
||||
|
||||
/* Switch */
|
||||
@ -151,14 +165,18 @@ body {
|
||||
@apply pl-2;
|
||||
}
|
||||
|
||||
.switch-check-checked {
|
||||
@apply bg-primary;
|
||||
}
|
||||
|
||||
/* Select */
|
||||
.select {
|
||||
@apply dark:bg-white dark:text-black rounded pl-2 pr-6 border-2 border-transparent h-10 shadow-none;
|
||||
@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;
|
||||
@apply bg-none text-primary accent-primary;
|
||||
}
|
||||
.radio-label {
|
||||
@apply pl-2;
|
||||
@ -171,7 +189,7 @@ button.menubar__button {
|
||||
|
||||
/* Notification */
|
||||
.notification {
|
||||
@apply p-7 bg-secondary text-black rounded;
|
||||
@apply p-7 bg-mbz-yellow-alt-200 dark:bg-mbz-purple-600 text-black dark:text-white rounded;
|
||||
}
|
||||
|
||||
.notification-primary {
|
||||
@ -187,18 +205,26 @@ button.menubar__button {
|
||||
}
|
||||
|
||||
.notification-danger {
|
||||
@apply bg-mbz-danger;
|
||||
@apply bg-mbz-danger text-white;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.table tr {
|
||||
@apply odd:bg-white dark:odd:bg-gray-800 even:bg-gray-50 dark:even:bg-gray-900 border-b;
|
||||
@apply odd:bg-white dark:odd:bg-zinc-600 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;
|
||||
@ -210,14 +236,14 @@ button.menubar__button {
|
||||
@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 text-lg;
|
||||
@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;
|
||||
@apply px-3 dark:text-black;
|
||||
}
|
||||
.pagination-link-current {
|
||||
@apply bg-primary cursor-not-allowed pointer-events-none border-primary text-white;
|
||||
@ -236,3 +262,19 @@ button.menubar__button {
|
||||
.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)];
|
||||
}
|
||||
|
@ -22,13 +22,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.mbz-card {
|
||||
@apply block bg-mbz-yellow hover:bg-mbz-yellow/90 text-violet-title dark:text-white dark:hover:text-white/90 rounded-lg dark:border-violet-title shadow-md dark:bg-gray-700 dark:hover:bg-gray-700/90 dark:text-white dark:hover:text-white;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
>
|
||||
{{ displayName(actor) }}
|
||||
</h5>
|
||||
<p class="text-gray-500 truncate" v-if="actor.name">
|
||||
<p class="text-gray-500 dark:text-gray-200 truncate" v-if="actor.name">
|
||||
<span dir="ltr">@{{ usernameWithDomain(actor) }}</span>
|
||||
</p>
|
||||
<div
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="activity-item">
|
||||
<o-icon :icon="'chat'" :type="iconColor" />
|
||||
<div class="subject">
|
||||
<o-icon :icon="'chat'" :variant="iconColor" custom-size="24" />
|
||||
<div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
|
||||
<i18n-t :keypath="translation" tag="p">
|
||||
<template #discussion>
|
||||
<router-link
|
||||
@ -102,12 +102,12 @@ const iconColor = computed((): string | undefined => {
|
||||
switch (props.activity.subject) {
|
||||
case ActivityDiscussionSubject.DISCUSSION_CREATED:
|
||||
case ActivityDiscussionSubject.DISCUSSION_REPLIED:
|
||||
return "is-success";
|
||||
return "success";
|
||||
case ActivityDiscussionSubject.DISCUSSION_RENAMED:
|
||||
case ActivityDiscussionSubject.DISCUSSION_ARCHIVED:
|
||||
return "is-grey";
|
||||
return "grey";
|
||||
case ActivityDiscussionSubject.DISCUSSION_DELETED:
|
||||
return "is-danger";
|
||||
return "danger";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="activity-item">
|
||||
<o-icon :icon="'calendar'" :type="iconColor" />
|
||||
<div class="subject">
|
||||
<o-icon :icon="'calendar'" :variant="iconColor" custom-size="24" />
|
||||
<div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
|
||||
<i18n-t :keypath="translation" tag="p">
|
||||
<template #event>
|
||||
<router-link
|
||||
@ -93,11 +93,11 @@ const iconColor = computed((): string | undefined => {
|
||||
switch (props.activity.subject) {
|
||||
case ActivityEventSubject.EVENT_CREATED:
|
||||
case ActivityEventCommentSubject.COMMENT_POSTED:
|
||||
return "is-success";
|
||||
return "success";
|
||||
case ActivityEventSubject.EVENT_UPDATED:
|
||||
return "is-grey";
|
||||
return "grey";
|
||||
case ActivityEventSubject.EVENT_DELETED:
|
||||
return "is-danger";
|
||||
return "danger";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="activity-item">
|
||||
<o-icon :icon="'cog'" :type="iconColor" />
|
||||
<div class="subject">
|
||||
<o-icon :icon="'cog'" :variant="iconColor" custom-size="24" />
|
||||
<div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
|
||||
<i18n-t :keypath="translation" tag="p">
|
||||
<template #group>
|
||||
<router-link
|
||||
@ -28,13 +28,7 @@
|
||||
></template
|
||||
></i18n-t
|
||||
>
|
||||
<i18n-t
|
||||
:keypath="detail"
|
||||
v-for="detail in details"
|
||||
:key="detail"
|
||||
tag="p"
|
||||
class="has-text-grey-dark"
|
||||
>
|
||||
<i18n-t :keypath="detail" v-for="detail in details" :key="detail" tag="p">
|
||||
<template #profile>
|
||||
<popover-actor-card :actor="activity.author" :inline="true">
|
||||
<b>
|
||||
@ -63,9 +57,7 @@
|
||||
}}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<small class="has-text-grey-dark activity-date">{{
|
||||
formatTimeString(activity.insertedAt)
|
||||
}}</small>
|
||||
<small>{{ formatTimeString(activity.insertedAt) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -110,9 +102,9 @@ const translation = computed((): string | undefined => {
|
||||
const iconColor = computed((): string | undefined => {
|
||||
switch (props.activity.subject) {
|
||||
case ActivityGroupSubject.GROUP_CREATED:
|
||||
return "is-success";
|
||||
return "success";
|
||||
case ActivityGroupSubject.GROUP_UPDATED:
|
||||
return "is-grey";
|
||||
return "grey";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="activity-item">
|
||||
<o-icon :icon="icon" :type="iconColor" />
|
||||
<div class="subject">
|
||||
<o-icon :icon="icon" :variant="iconColor" custom-size="24" />
|
||||
<div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
|
||||
<i18n-t :keypath="translation" tag="p">
|
||||
<template #member>
|
||||
<popover-actor-card
|
||||
@ -144,14 +144,14 @@ const iconColor = computed((): string | undefined => {
|
||||
case ActivityMemberSubject.MEMBER_JOINED:
|
||||
case ActivityMemberSubject.MEMBER_APPROVED:
|
||||
case ActivityMemberSubject.MEMBER_ACCEPTED_INVITATION:
|
||||
return "is-success";
|
||||
return "success";
|
||||
case ActivityMemberSubject.MEMBER_REQUEST:
|
||||
case ActivityMemberSubject.MEMBER_UPDATED:
|
||||
return "is-grey";
|
||||
return "grey";
|
||||
case ActivityMemberSubject.MEMBER_REMOVED:
|
||||
case ActivityMemberSubject.MEMBER_REJECTED_INVITATION:
|
||||
case ActivityMemberSubject.MEMBER_QUIT:
|
||||
return "is-danger";
|
||||
return "danger";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="activity-item">
|
||||
<o-icon :icon="'bullhorn'" :type="iconColor" />
|
||||
<div class="subject">
|
||||
<o-icon :icon="'bullhorn'" :variant="iconColor" custom-size="24" />
|
||||
<div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
|
||||
<i18n-t :keypath="translation" tag="p">
|
||||
<template #post>
|
||||
<router-link
|
||||
@ -78,11 +78,11 @@ const translation = computed((): string | undefined => {
|
||||
const iconColor = computed((): string | undefined => {
|
||||
switch (props.activity.subject) {
|
||||
case ActivityPostSubject.POST_CREATED:
|
||||
return "is-success";
|
||||
return "success";
|
||||
case ActivityPostSubject.POST_UPDATED:
|
||||
return "is-grey";
|
||||
return "grey";
|
||||
case ActivityPostSubject.POST_DELETED:
|
||||
return "is-danger";
|
||||
return "danger";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="activity-item">
|
||||
<o-icon :icon="'link'" :type="iconColor" />
|
||||
<div class="subject">
|
||||
<o-icon :icon="'link'" :variant="iconColor" custom-size="24" />
|
||||
<div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
|
||||
<i18n-t :keypath="translation" tag="p">
|
||||
<template #resource>
|
||||
<router-link v-if="activity.object" :to="path">{{
|
||||
@ -142,12 +142,12 @@ const translation = computed((): string | undefined => {
|
||||
const iconColor = computed((): string | undefined => {
|
||||
switch (props.activity.subject) {
|
||||
case ActivityResourceSubject.RESOURCE_CREATED:
|
||||
return "is-success";
|
||||
return "success";
|
||||
case ActivityResourceSubject.RESOURCE_MOVED:
|
||||
case ActivityResourceSubject.RESOURCE_UPDATED:
|
||||
return "is-grey";
|
||||
return "grey";
|
||||
case ActivityResourceSubject.RESOURCE_DELETED:
|
||||
return "is-danger";
|
||||
return "danger";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.activity-item {
|
||||
display: flex;
|
||||
span.icon {
|
||||
span.o-icon {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
box-sizing: border-box;
|
||||
@ -10,8 +10,4 @@
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
.subject {
|
||||
padding: 0.25rem 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<o-icon
|
||||
v-if="showIcon"
|
||||
:icon="poiInfos?.poiIcon.icon"
|
||||
size="is-medium"
|
||||
size="medium"
|
||||
class="icon"
|
||||
/>
|
||||
<p>
|
||||
|
@ -1,104 +1,3 @@
|
||||
export const eventCategories = (t) => {
|
||||
return [
|
||||
{
|
||||
id: "ARTS",
|
||||
icon: "palette",
|
||||
},
|
||||
{
|
||||
id: "BOOK_CLUBS",
|
||||
icon: "favourite-book",
|
||||
},
|
||||
{
|
||||
id: "BUSINESS",
|
||||
},
|
||||
{
|
||||
id: "CAUSES",
|
||||
},
|
||||
{
|
||||
id: "COMEDY",
|
||||
},
|
||||
{
|
||||
id: "CRAFTS",
|
||||
},
|
||||
{
|
||||
id: "FOOD_DRINK",
|
||||
},
|
||||
{
|
||||
id: "HEALTH",
|
||||
},
|
||||
{
|
||||
id: "MUSIC",
|
||||
},
|
||||
{
|
||||
id: "AUTO_BOAT_AIR",
|
||||
},
|
||||
{
|
||||
id: "COMMUNITY",
|
||||
},
|
||||
{
|
||||
id: "FAMILY_EDUCATION",
|
||||
},
|
||||
{
|
||||
id: "FASHION_BEAUTY",
|
||||
},
|
||||
{
|
||||
id: "FILM_MEDIA",
|
||||
},
|
||||
{
|
||||
id: "GAMES",
|
||||
},
|
||||
{
|
||||
id: "LANGUAGE_CULTURE",
|
||||
},
|
||||
{
|
||||
id: "LEARNING",
|
||||
},
|
||||
{
|
||||
id: "LGBTQ",
|
||||
},
|
||||
{
|
||||
id: "MOVEMENTS_POLITICS",
|
||||
},
|
||||
{
|
||||
id: "NETWORKING",
|
||||
},
|
||||
{
|
||||
id: "PARTY",
|
||||
},
|
||||
{
|
||||
id: "PERFORMING_VISUAL_ARTS",
|
||||
},
|
||||
{
|
||||
id: "PETS",
|
||||
},
|
||||
{
|
||||
id: "PHOTOGRAPHY",
|
||||
},
|
||||
{
|
||||
id: "OUTDOORS_ADVENTURE",
|
||||
},
|
||||
{
|
||||
id: "SPIRITUALITY_RELIGION_BELIEFS",
|
||||
},
|
||||
{
|
||||
id: "SCIENCE_TECH",
|
||||
},
|
||||
{
|
||||
id: "SPORTS",
|
||||
},
|
||||
{
|
||||
id: "THEATRE",
|
||||
},
|
||||
{
|
||||
id: "MEETING",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const eventCategoryLabel = (category: string, t): string | undefined => {
|
||||
return eventCategories(t).find(({ id }) => id === category)?.label;
|
||||
};
|
||||
|
||||
export type CategoryPictureLicencingElement = { name: string; url: string };
|
||||
export type CategoryPictureLicencing = {
|
||||
author: CategoryPictureLicencingElement;
|
||||
|
@ -85,7 +85,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Comment from "@/components/Comment/Comment.vue";
|
||||
import Comment from "@/components/Comment/EventComment.vue";
|
||||
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
|
||||
import { CommentModeration } from "@/types/enums";
|
||||
import { CommentModel, IComment } from "../../types/comment.model";
|
||||
@ -122,7 +122,9 @@ const props = defineProps<{
|
||||
newComment?: IComment;
|
||||
}>();
|
||||
|
||||
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("@/components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const newComment = ref<IComment>(props.newComment ?? new CommentModel());
|
||||
|
||||
@ -284,7 +286,7 @@ const { mutate: deleteComment, onError: deleteCommentMutationError } =
|
||||
replies: updatedReplies,
|
||||
totalReplies: parentComment.totalReplies - 1,
|
||||
});
|
||||
console.log("updatedComments", updatedComments);
|
||||
console.debug("updatedComments", updatedComments);
|
||||
} else {
|
||||
// we have deleted a thread itself
|
||||
updatedComments = updatedComments.map((reply) => {
|
||||
|
@ -23,7 +23,7 @@
|
||||
</Story>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { IActor } from "@/types/actor";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { IComment } from "@/types/comment.model";
|
||||
import {
|
||||
ActorType,
|
||||
@ -34,7 +34,7 @@ import {
|
||||
} from "@/types/enums";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { reactive } from "vue";
|
||||
import Comment from "./Comment.vue";
|
||||
import Comment from "./EventComment.vue";
|
||||
import FloatingVue from "floating-vue";
|
||||
import "floating-vue/dist/style.css";
|
||||
import { hstEvent } from "histoire/client";
|
||||
@ -51,7 +51,7 @@ const baseActorAvatar = {
|
||||
url: "https://social.tcit.fr/system/accounts/avatars/000/000/001/original/a28c50ce5f2b13fd.jpg",
|
||||
};
|
||||
|
||||
const baseActor: IActor = {
|
||||
const baseActor: IPerson = {
|
||||
name: "Thomas Citharel",
|
||||
preferredUsername: "tcit",
|
||||
avatar: baseActorAvatar,
|
||||
@ -67,8 +67,8 @@ const baseEvent: IEvent = {
|
||||
uuid: "",
|
||||
title: "A very interesting event",
|
||||
description: "Things happen",
|
||||
beginsOn: new Date(),
|
||||
endsOn: new Date(),
|
||||
beginsOn: new Date().toISOString(),
|
||||
endsOn: new Date().toISOString(),
|
||||
physicalAddress: {
|
||||
description: "Somewhere",
|
||||
street: "",
|
||||
@ -88,7 +88,7 @@ const baseEvent: IEvent = {
|
||||
url: "",
|
||||
local: true,
|
||||
slug: "",
|
||||
publishAt: new Date(),
|
||||
publishAt: new Date().toISOString(),
|
||||
status: EventStatus.CONFIRMED,
|
||||
visibility: EventVisibility.PUBLIC,
|
||||
joinOptions: EventJoinOptions.FREE,
|
||||
@ -151,7 +151,7 @@ const comment = reactive<IComment>({
|
||||
text: "a reply!",
|
||||
id: "90",
|
||||
actor: baseActor,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
url: "http://somewhere.tld",
|
||||
replies: [],
|
||||
totalReplies: 0,
|
||||
@ -162,7 +162,7 @@ const comment = reactive<IComment>({
|
||||
text: "a reply to another reply!",
|
||||
id: "92",
|
||||
actor: baseActor,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
url: "http://somewhere.tld",
|
||||
replies: [],
|
||||
totalReplies: 0,
|
||||
@ -171,7 +171,7 @@ const comment = reactive<IComment>({
|
||||
},
|
||||
],
|
||||
isAnnouncement: false,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
url: "http://somewhere.tld",
|
||||
});
|
||||
</script>
|
@ -175,7 +175,7 @@
|
||||
</li>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import EditorComponent from "@/components/Editor.vue";
|
||||
import EditorComponent from "@/components/TextEditor.vue";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { CommentModeration } from "@/types/enums";
|
||||
import { CommentModel, IComment } from "../../types/comment.model";
|
||||
@ -200,7 +200,9 @@ import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
||||
import Reply from "vue-material-design-icons/Reply.vue";
|
||||
import type { Locale } from "date-fns";
|
||||
|
||||
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("@/components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -257,7 +259,7 @@ const replyToComment = (): void => {
|
||||
newComment.value.inReplyToComment = props.comment;
|
||||
newComment.value.originComment = props.comment.originComment ?? props.comment;
|
||||
newComment.value.actor = props.currentActor;
|
||||
console.log(newComment.value);
|
||||
console.debug(newComment.value);
|
||||
emit("create-comment", newComment.value);
|
||||
newComment.value = new CommentModel();
|
||||
replyTo.value = false;
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<article class="flex gap-2">
|
||||
<article class="flex gap-2 bg-white dark:bg-transparent">
|
||||
<div class="">
|
||||
<figure class="" v-if="comment.actor && comment.actor.avatar">
|
||||
<img
|
||||
@ -32,7 +32,7 @@
|
||||
comment.actor.id === currentActor?.id
|
||||
"
|
||||
>
|
||||
<o-dropdown aria-role="list">
|
||||
<o-dropdown aria-role="list" position="bottom-left">
|
||||
<template #trigger>
|
||||
<o-icon role="button" icon="dots-horizontal" />
|
||||
</template>
|
||||
@ -133,7 +133,9 @@ import { formatDateTimeString } from "@/filters/datetime";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import type { Locale } from "date-fns";
|
||||
|
||||
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("@/components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: IComment;
|
||||
|
@ -68,7 +68,7 @@
|
||||
</p>
|
||||
|
||||
<details>
|
||||
<summary class="is-size-5">{{ t("Technical details") }}</summary>
|
||||
<summary>{{ t("Technical details") }}</summary>
|
||||
<p>{{ t("Error message") }}</p>
|
||||
<pre>{{ error }}</pre>
|
||||
<p>{{ t("Error stacktrace") }}</p>
|
||||
|
@ -14,6 +14,9 @@
|
||||
<Variant title="cancelled">
|
||||
<EventCard :event="cancelledEvent" />
|
||||
</Variant>
|
||||
<Variant title="Row mode">
|
||||
<EventCard :event="longEvent" mode="row" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
@ -53,8 +56,8 @@ const baseEvent: IEvent = {
|
||||
uuid: "",
|
||||
title: "A very interesting event",
|
||||
description: "Things happen",
|
||||
beginsOn: new Date(),
|
||||
endsOn: new Date(),
|
||||
beginsOn: new Date().toISOString(),
|
||||
endsOn: new Date().toISOString(),
|
||||
physicalAddress: {
|
||||
description: "Somewhere",
|
||||
street: "",
|
||||
@ -74,7 +77,7 @@ const baseEvent: IEvent = {
|
||||
url: "",
|
||||
local: true,
|
||||
slug: "",
|
||||
publishAt: new Date(),
|
||||
publishAt: new Date().toISOString(),
|
||||
status: EventStatus.CONFIRMED,
|
||||
visibility: EventVisibility.PUBLIC,
|
||||
joinOptions: EventJoinOptions.FREE,
|
||||
@ -130,7 +133,7 @@ const event = reactive<IEvent>(baseEvent);
|
||||
const longEvent = reactive<IEvent>({
|
||||
...baseEvent,
|
||||
title:
|
||||
"A very long title that will have trouble to display because it will take multiple lines but where will it stop ?! Maybe after 3 lines is enough. Let's say so.",
|
||||
"A very long title that will have trouble to display because it will take multiple lines but where will it stop ?! Maybe after 3 lines is enough. Let's say so. But if it doesn't work, we really need to truncate it at some point. Definitively.",
|
||||
});
|
||||
|
||||
const tentativeEvent = reactive<IEvent>({
|
||||
|
@ -1,16 +1,25 @@
|
||||
<template>
|
||||
<router-link
|
||||
class="mbz-card max-w-xs shrink-0 w-[18rem] snap-center dark:bg-mbz-purple"
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||
<LinkOrRouterLink
|
||||
class="mbz-card snap-center dark:bg-mbz-purple"
|
||||
:class="{
|
||||
'sm:flex sm:items-start': mode === 'row',
|
||||
'max-w-xs w-[18rem] shrink-0 flex flex-col': mode === 'column',
|
||||
}"
|
||||
:to="to"
|
||||
:isInternal="isInternal"
|
||||
>
|
||||
<div class="bg-secondary rounded-lg">
|
||||
<div
|
||||
class="bg-secondary rounded-lg"
|
||||
:class="{ 'sm:w-full sm:max-w-[20rem]': mode === 'row' }"
|
||||
>
|
||||
<figure class="block relative pt-40">
|
||||
<lazy-image-wrapper
|
||||
:picture="event.picture"
|
||||
style="height: 100%; position: absolute; top: 0; left: 0; width: 100%"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-3 right-0 ltr:-mr-1 rtl:-ml-1 z-10 max-w-xs no-underline flex flex-col gap-1"
|
||||
class="absolute top-3 right-0 ltr:-mr-1 rtl:-ml-1 z-10 max-w-xs no-underline flex flex-col gap-1 items-end"
|
||||
v-show="mode === 'column'"
|
||||
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
|
||||
>
|
||||
<mobilizon-tag
|
||||
@ -30,30 +39,39 @@
|
||||
v-for="tag in (event.tags || []).slice(0, 3)"
|
||||
:key="tag.slug"
|
||||
>
|
||||
<mobilizon-tag dir="auto">{{ tag.title }}</mobilizon-tag>
|
||||
<mobilizon-tag dir="auto" :with-hash-tag="true">{{
|
||||
tag.title
|
||||
}}</mobilizon-tag>
|
||||
</router-link>
|
||||
</div>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<div class="p-2 flex-auto" :class="{ 'sm:flex-1': mode === 'row' }">
|
||||
<div class="relative flex flex-col h-full">
|
||||
<div class="-mt-3 h-0 flex mb-3 ltr:ml-0 rtl:mr-0 items-end self-start">
|
||||
<div
|
||||
class="-mt-3 h-0 flex mb-3 ltr:ml-0 rtl:mr-0 items-end self-start"
|
||||
:class="{ 'sm:hidden': mode === 'row' }"
|
||||
>
|
||||
<date-calendar-icon
|
||||
:small="true"
|
||||
v-if="!mergedOptions.hideDate"
|
||||
:date="event.beginsOn.toString()"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full flex flex-col justify-between">
|
||||
<h3
|
||||
class="text-lg leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
|
||||
:title="event.title"
|
||||
<span
|
||||
class="text-gray-700 dark:text-white font-semibold hidden"
|
||||
:class="{ 'sm:block': mode === 'row' }"
|
||||
>{{ formatDateTimeWithCurrentLocale }}</span
|
||||
>
|
||||
<div class="w-full flex flex-col justify-between h-full">
|
||||
<h2
|
||||
class="mt-0 mb-2 text-2xl line-clamp-3 font-bold text-violet-3 dark:text-white"
|
||||
dir="auto"
|
||||
:lang="event.language"
|
||||
>
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<div class="pt-3">
|
||||
</h2>
|
||||
<div class="">
|
||||
<div
|
||||
class="flex items-center text-violet-3 dark:text-white"
|
||||
dir="auto"
|
||||
@ -68,7 +86,7 @@
|
||||
/>
|
||||
</figure>
|
||||
<account-circle v-else />
|
||||
<span class="text-sm font-semibold ltr:pl-2 rtl:pr-2">
|
||||
<span class="font-semibold ltr:pl-2 rtl:pr-2">
|
||||
{{ organizerDisplayName(event) }}
|
||||
</span>
|
||||
</div>
|
||||
@ -84,11 +102,38 @@
|
||||
<Video />
|
||||
<span class="ltr:pl-2 rtl:pr-2">{{ $t("Online") }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 no-underline gap-1 items-center hidden"
|
||||
:class="{ 'sm:flex': mode === 'row' }"
|
||||
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
|
||||
>
|
||||
<mobilizon-tag
|
||||
variant="info"
|
||||
v-if="event.status === EventStatus.TENTATIVE"
|
||||
>
|
||||
{{ $t("Tentative") }}
|
||||
</mobilizon-tag>
|
||||
<mobilizon-tag
|
||||
variant="danger"
|
||||
v-if="event.status === EventStatus.CANCELLED"
|
||||
>
|
||||
{{ $t("Cancelled") }}
|
||||
</mobilizon-tag>
|
||||
<router-link
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
v-for="tag in (event.tags || []).slice(0, 3)"
|
||||
:key="tag.slug"
|
||||
>
|
||||
<mobilizon-tag :with-hash-tag="true" dir="auto">{{
|
||||
tag.title
|
||||
}}</mobilizon-tag>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</LinkOrRouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -104,17 +149,29 @@ import { EventStatus } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||
|
||||
import { computed } from "vue";
|
||||
import MobilizonTag from "../Tag.vue";
|
||||
import { computed, inject } from "vue";
|
||||
import MobilizonTag from "@/components/Tag.vue";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Video from "vue-material-design-icons/Video.vue";
|
||||
import { formatDateTimeForEvent } from "@/utils/datetime";
|
||||
import type { Locale } from "date-fns";
|
||||
import LinkOrRouterLink from "../core/LinkOrRouterLink.vue";
|
||||
|
||||
const props = defineProps<{ event: IEvent; options?: IEventCardOptions }>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
event: IEvent;
|
||||
options?: IEventCardOptions;
|
||||
mode?: "row" | "column";
|
||||
}>(),
|
||||
{ mode: "column" }
|
||||
);
|
||||
const defaultOptions: IEventCardOptions = {
|
||||
hideDate: false,
|
||||
loggedPerson: false,
|
||||
hideDetails: false,
|
||||
organizerActor: null,
|
||||
isRemoteEvent: false,
|
||||
isLoggedIn: true,
|
||||
};
|
||||
|
||||
const mergedOptions = computed<IEventCardOptions>(() => ({
|
||||
@ -132,4 +189,31 @@ const mergedOptions = computed<IEventCardOptions>(() => ({
|
||||
const actorAvatarURL = computed<string | null>(() =>
|
||||
organizerAvatarUrl(props.event)
|
||||
);
|
||||
|
||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
||||
|
||||
const formatDateTimeWithCurrentLocale = computed(() => {
|
||||
if (!dateFnsLocale) return;
|
||||
return formatDateTimeForEvent(new Date(props.event.beginsOn), dateFnsLocale);
|
||||
});
|
||||
|
||||
const isInternal = computed(() => {
|
||||
return (
|
||||
mergedOptions.value.isRemoteEvent &&
|
||||
mergedOptions.value.isLoggedIn === false
|
||||
);
|
||||
});
|
||||
|
||||
const to = computed(() => {
|
||||
if (mergedOptions.value.isRemoteEvent) {
|
||||
if (mergedOptions.value.isLoggedIn === false) {
|
||||
return props.event.url;
|
||||
}
|
||||
return {
|
||||
name: RouteName.INTERACT,
|
||||
query: { uri: encodeURI(props.event.url) },
|
||||
};
|
||||
}
|
||||
return { name: RouteName.EVENT, params: { uuid: props.event.uuid } };
|
||||
});
|
||||
</script>
|
||||
|
@ -14,7 +14,7 @@
|
||||
</p>
|
||||
<p v-else-if="isSameDay() && showStartTime && showEndTime">
|
||||
<span>{{
|
||||
$t("On {date} from {startTime} to {endTime}", {
|
||||
t("On {date} from {startTime} to {endTime}", {
|
||||
date: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn, timezoneToShow),
|
||||
endTime: formatTime(endsOn, timezoneToShow),
|
||||
@ -31,27 +31,24 @@
|
||||
</p>
|
||||
<p v-else-if="isSameDay() && showStartTime && !showEndTime">
|
||||
{{
|
||||
$t("On {date} starting at {startTime}", {
|
||||
t("On {date} starting at {startTime}", {
|
||||
date: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-else-if="isSameDay()">
|
||||
{{ $t("On {date}", { date: formatDate(beginsOn) }) }}
|
||||
{{ t("On {date}", { date: formatDate(beginsOn) }) }}
|
||||
</p>
|
||||
<p v-else-if="endsOn && showStartTime && showEndTime">
|
||||
<span>
|
||||
{{
|
||||
$t(
|
||||
"From the {startDate} at {startTime} to the {endDate} at {endTime}",
|
||||
{
|
||||
startDate: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn, timezoneToShow),
|
||||
endDate: formatDate(endsOn),
|
||||
endTime: formatTime(endsOn, timezoneToShow),
|
||||
}
|
||||
)
|
||||
t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
||||
startDate: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn, timezoneToShow),
|
||||
endDate: formatDate(endsOn),
|
||||
endTime: formatTime(endsOn, timezoneToShow),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
@ -66,7 +63,7 @@
|
||||
<p v-else-if="endsOn && showStartTime">
|
||||
<span>
|
||||
{{
|
||||
$t("From the {startDate} at {startTime} to the {endDate}", {
|
||||
t("From the {startDate} at {startTime} to the {endDate}", {
|
||||
startDate: formatDate(beginsOn),
|
||||
startTime: formatTime(beginsOn, timezoneToShow),
|
||||
endDate: formatDate(endsOn),
|
||||
@ -169,22 +166,22 @@ const differentFromUserTimezone = computed((): boolean => {
|
||||
const singleTimeZone = computed((): string => {
|
||||
if (showLocalTimezone.value) {
|
||||
return t("Local time ({timezone})", {
|
||||
timezone: timezoneToShow,
|
||||
}) as string;
|
||||
timezone: timezoneToShow.value,
|
||||
});
|
||||
}
|
||||
return t("Time in your timezone ({timezone})", {
|
||||
timezone: timezoneToShow,
|
||||
}) as string;
|
||||
timezone: timezoneToShow.value,
|
||||
});
|
||||
});
|
||||
|
||||
const multipleTimeZones = computed((): string => {
|
||||
if (showLocalTimezone.value) {
|
||||
return t("Local time ({timezone})", {
|
||||
timezone: timezoneToShow,
|
||||
}) as string;
|
||||
return t("Local times ({timezone})", {
|
||||
timezone: timezoneToShow.value,
|
||||
});
|
||||
}
|
||||
return t("Times in your timezone ({timezone})", {
|
||||
timezone: timezoneToShow,
|
||||
}) as string;
|
||||
timezone: timezoneToShow.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -87,7 +87,7 @@ const RoutingParamType = {
|
||||
},
|
||||
};
|
||||
|
||||
const MapLeaflet = import("../../components/Map.vue");
|
||||
const MapLeaflet = import("@/components/LeafletMap.vue");
|
||||
|
||||
const props = defineProps<{
|
||||
address: IAddress;
|
||||
|
@ -136,10 +136,10 @@ const metadata = computed({
|
||||
};
|
||||
}) as any[];
|
||||
},
|
||||
set(metadata: IEventMetadataDescription[]) {
|
||||
set(newMetadata: IEventMetadataDescription[]) {
|
||||
emit(
|
||||
"update:modelValue",
|
||||
metadata.filter((elem) => elem)
|
||||
newMetadata.filter((elem) => elem)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div class="address" v-if="physicalAddress">
|
||||
<address-info :address="physicalAddress" />
|
||||
<o-button
|
||||
type="is-text"
|
||||
variant="text"
|
||||
class="map-show-button"
|
||||
@click="$emit('showMapModal', true)"
|
||||
v-if="physicalAddress.geom"
|
||||
|
@ -22,27 +22,23 @@
|
||||
:lang="event.language"
|
||||
dir="auto"
|
||||
>
|
||||
<b-tag
|
||||
<tag
|
||||
variant="info"
|
||||
class="mr-1"
|
||||
v-if="event.status === EventStatus.TENTATIVE"
|
||||
>
|
||||
{{ $t("Tentative") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
</tag>
|
||||
<tag
|
||||
variant="danger"
|
||||
class="mr-1"
|
||||
v-if="event.status === EventStatus.CANCELLED"
|
||||
>
|
||||
{{ $t("Cancelled") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
class="mr-2"
|
||||
variant="warning"
|
||||
size="is-medium"
|
||||
v-if="event.draft"
|
||||
>{{ $t("Draft") }}</b-tag
|
||||
>
|
||||
</tag>
|
||||
<tag class="mr-2" variant="warning" size="medium" v-if="event.draft">{{
|
||||
$t("Draft")
|
||||
}}</tag>
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<inline-address
|
||||
@ -99,7 +95,7 @@
|
||||
</span>
|
||||
<span v-if="event.participantStats.notApproved > 0">
|
||||
<o-button
|
||||
type="is-text"
|
||||
variant="text"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.PARTICIPATIONS,
|
||||
@ -134,6 +130,7 @@ import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||
import Video from "vue-material-design-icons/Video.vue";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
@ -1,12 +1,17 @@
|
||||
<template>
|
||||
<article class="bg-white dark:bg-mbz-purple mb-5 mt-4 pb-2 md:p-0">
|
||||
<div class="bg-yellow-2 flex p-2 text-violet-title rounded-t-lg" dir="auto">
|
||||
<article
|
||||
class="bg-white dark:bg-mbz-purple dark:hover:bg-mbz-purple-400 mb-5 mt-4 pb-2 md:p-0 rounded-t-lg"
|
||||
>
|
||||
<div
|
||||
class="bg-mbz-yellow-alt-100 flex p-2 text-violet-title rounded-t-lg"
|
||||
dir="auto"
|
||||
>
|
||||
<figure
|
||||
class="image is-24x24 ltr:pr-1 rtl:pl-1"
|
||||
v-if="participation.actor.avatar"
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
class="rounded"
|
||||
:src="participation.actor.avatar.url"
|
||||
alt=""
|
||||
height="24"
|
||||
@ -157,7 +162,7 @@
|
||||
</span>
|
||||
<o-button
|
||||
v-if="participation.event.participantStats.notApproved > 0"
|
||||
type="is-text"
|
||||
variant="text"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.PARTICIPATIONS,
|
||||
@ -330,7 +335,7 @@ const defaultOptions: IEventCardOptions = {
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
participation: IParticipant;
|
||||
options: IEventCardOptions;
|
||||
options?: IEventCardOptions;
|
||||
}>(),
|
||||
{
|
||||
options: () => ({
|
||||
|
@ -7,14 +7,11 @@
|
||||
:message="fieldErrors"
|
||||
:type="{ 'is-danger': fieldErrors }"
|
||||
class="!-mt-2"
|
||||
:labelClass="labelClass"
|
||||
>
|
||||
<template #label>
|
||||
{{ actualLabel }}
|
||||
<span
|
||||
class="is-size-6 has-text-weight-normal"
|
||||
v-if="gettingLocation"
|
||||
>{{ t("Getting location") }}</span
|
||||
>
|
||||
<span v-if="gettingLocation">{{ t("Getting location") }}</span>
|
||||
</template>
|
||||
<p class="control" v-if="canShowLocateMeButton">
|
||||
<o-loading
|
||||
@ -54,7 +51,7 @@
|
||||
</template>
|
||||
<template #empty>
|
||||
<span v-if="isFetching">{{ t("Searching…") }}</span>
|
||||
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
||||
<div v-else-if="queryText.length >= 3" class="enabled">
|
||||
<span>{{
|
||||
t('No results for "{queryText}"', { queryText })
|
||||
}}</span>
|
||||
@ -121,12 +118,16 @@ import { useGeocodingAutocomplete } from "@/composition/apollo/config";
|
||||
import { ADDRESS } from "@/graphql/address";
|
||||
import { useReverseGeocode } from "@/composition/apollo/address";
|
||||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
const MapLeaflet = defineAsyncComponent(() => import("../Map.vue"));
|
||||
const MapLeaflet = defineAsyncComponent(
|
||||
() => import("@/components/LeafletMap.vue")
|
||||
);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: IAddress | null;
|
||||
defaultText?: string | null;
|
||||
label?: string;
|
||||
labelClass?: string;
|
||||
userTimezone?: string;
|
||||
disabled?: boolean;
|
||||
hideMap?: boolean;
|
||||
@ -134,7 +135,8 @@ const props = withDefaults(
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
label: "",
|
||||
labelClass: "",
|
||||
defaultText: "",
|
||||
disabled: false,
|
||||
hideMap: false,
|
||||
hideSelected: false,
|
||||
@ -204,7 +206,7 @@ const checkCurrentPosition = (e: LatLng): boolean => {
|
||||
const { t, locale } = useI18n({ useScope: "global" });
|
||||
|
||||
const actualLabel = computed((): string => {
|
||||
return props.label ?? (t("Find an address") as string);
|
||||
return props.label ?? t("Find an address");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
@ -253,11 +255,14 @@ const asyncData = async (query: string): Promise<void> => {
|
||||
|
||||
const queryText = computed({
|
||||
get() {
|
||||
return selected.value ? addressFullName(selected.value) : "";
|
||||
return (
|
||||
(selected.value ? addressFullName(selected.value) : props.defaultText) ??
|
||||
""
|
||||
);
|
||||
},
|
||||
set(text) {
|
||||
if (text === "" && selected.value?.id) {
|
||||
console.log("doing reset");
|
||||
console.debug("doing reset");
|
||||
resetAddress();
|
||||
}
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="events-wrapper">
|
||||
<div class="flex flex-col gap-4" v-for="key of keys" :key="key">
|
||||
<h2 class="is-size-5 month-name">
|
||||
<h2 class="month-name">
|
||||
{{ monthName(groupEvents(key)[0]) }}
|
||||
</h2>
|
||||
<event-minimalist-card
|
||||
|
@ -27,10 +27,6 @@ const videoDetails = computed((): { host: string; uuid: string } | null => {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const origin = computed((): string => {
|
||||
return window.location.hostname;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.peertube {
|
@ -28,10 +28,6 @@ const videoID = computed((): string | null => {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const origin = computed((): string => {
|
||||
return window.location.hostname;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.youtube {
|
@ -34,9 +34,9 @@
|
||||
class="flex flex-wrap p-3 bg-white hover:bg-gray-50 dark:bg-violet-3 dark:hover:bg-violet-3/60 border border-gray-300 rounded-lg cursor-pointer peer-checked:ring-primary peer-checked:ring-2 peer-checked:border-transparent"
|
||||
:for="`availableActor-${availableActor?.id}`"
|
||||
>
|
||||
<figure class="" v-if="availableActor?.avatar">
|
||||
<figure class="h-12 w-12" v-if="availableActor?.avatar">
|
||||
<img
|
||||
class="rounded"
|
||||
class="rounded-full h-full w-full object-cover"
|
||||
:src="availableActor.avatar.url"
|
||||
alt=""
|
||||
width="48"
|
||||
|
@ -12,9 +12,9 @@
|
||||
>
|
||||
<div class="flex gap-1 p-4">
|
||||
<div class="">
|
||||
<figure class="" v-if="selectedActor.avatar">
|
||||
<figure class="h-12 w-12" v-if="selectedActor.avatar">
|
||||
<img
|
||||
class="rounded"
|
||||
class="rounded-full h-full w-full object-cover"
|
||||
:src="selectedActor.avatar.url"
|
||||
:alt="selectedActor.avatar.alt ?? ''"
|
||||
height="48"
|
||||
@ -207,7 +207,7 @@ const props = withDefaults(
|
||||
{ inline: true, contacts: () => [] }
|
||||
);
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:Contacts"]);
|
||||
const emit = defineEmits(["update:modelValue", "update:contacts"]);
|
||||
|
||||
const selectedActor = computed({
|
||||
get(): IActor | undefined {
|
||||
@ -252,7 +252,7 @@ const actualContacts = computed({
|
||||
},
|
||||
set(contactsIds: (string | undefined)[]) {
|
||||
emit(
|
||||
"update:Contacts",
|
||||
"update:contacts",
|
||||
actorMembers.value.filter(({ id }) => contactsIds.includes(id))
|
||||
);
|
||||
},
|
||||
|
@ -68,7 +68,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { IActor, IPerson } from "@/types/actor";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import ParticipationButton from "./ParticipationButton.vue";
|
||||
|
@ -37,7 +37,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import ShareModal from "@/components/Share/ShareModal.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
event: IEvent;
|
||||
eventCapacityOK?: boolean;
|
||||
|
@ -16,8 +16,8 @@ import TagInput from "./TagInput.vue";
|
||||
|
||||
const tags = reactive<ITag[]>([{ title: "Hello", slug: "hello" }]);
|
||||
|
||||
const fetchTags = async (text: string) =>
|
||||
new Promise<ITag[]>((resolve, reject) => {
|
||||
const fetchTags = async () =>
|
||||
new Promise<ITag[]>((resolve) => {
|
||||
resolve([{ title: "Welcome", slug: "welcome" }]);
|
||||
});
|
||||
</script>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<template #label>
|
||||
{{ $t("Add some tags") }}
|
||||
<o-tooltip
|
||||
type="dark"
|
||||
variant="dark"
|
||||
:label="
|
||||
$t('You can add tags by hitting the Enter key or by adding a comma')
|
||||
"
|
||||
@ -77,9 +77,9 @@ const tagsStrings = computed({
|
||||
get(): string[] {
|
||||
return props.modelValue.map((tag: ITag) => tag.title);
|
||||
},
|
||||
set(tagsStrings: string[]) {
|
||||
console.debug("tagsStrings", tagsStrings);
|
||||
const tagEntities = tagsStrings.map((tag: string | ITag) => {
|
||||
set(newTagsStrings: string[]) {
|
||||
console.debug("tagsStrings", newTagsStrings);
|
||||
const tagEntities = newTagsStrings.map((tag: string | ITag) => {
|
||||
if (typeof tag !== "string") {
|
||||
return tag;
|
||||
}
|
||||
|
@ -15,14 +15,17 @@
|
||||
<GroupCard :group="groupWithFollowersOrMembers" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Row mode">
|
||||
<GroupCard :group="groupWithFollowersOrMembers" mode="row" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { IActor } from "@/types/actor";
|
||||
import { IGroup } from "@/types/actor";
|
||||
import GroupCard from "./GroupCard.vue";
|
||||
|
||||
const basicGroup: IActor = {
|
||||
const basicGroup: IGroup = {
|
||||
name: "Framasoft",
|
||||
preferredUsername: "framasoft",
|
||||
avatar: null,
|
||||
@ -34,7 +37,7 @@ const basicGroup: IActor = {
|
||||
followers: { total: 0, elements: [] },
|
||||
};
|
||||
|
||||
const groupWithMedia = {
|
||||
const groupWithMedia: IGroup = {
|
||||
...basicGroup,
|
||||
banner: {
|
||||
url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg",
|
||||
@ -44,9 +47,14 @@ const groupWithMedia = {
|
||||
},
|
||||
};
|
||||
|
||||
const groupWithFollowersOrMembers = {
|
||||
const groupWithFollowersOrMembers: IGroup = {
|
||||
...groupWithMedia,
|
||||
members: { total: 2, elements: [] },
|
||||
followers: { total: 5, elements: [] },
|
||||
summary:
|
||||
"You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:h-full to apply the h-full utility at only medium screen sizes and above.",
|
||||
physicalAddress: {
|
||||
description: "Nantes",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,29 +1,31 @@
|
||||
<template>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
<LinkOrRouterLink
|
||||
:to="to"
|
||||
:isInternal="isInternal"
|
||||
class="mbz-card shrink-0 dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg my-4 flex items-center flex-col"
|
||||
:class="{
|
||||
'sm:flex-row': mode === 'row',
|
||||
'max-w-xs w-[18rem] shrink-0 flex flex-col': mode === 'column',
|
||||
}"
|
||||
class="card flex flex-col shrink-0 w-[18rem] bg-white dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg"
|
||||
>
|
||||
<figure class="rounded-t-lg flex justify-center h-40">
|
||||
<lazy-image-wrapper :picture="group.banner" :rounded="true" />
|
||||
</figure>
|
||||
<div class="py-2 pl-2">
|
||||
<div class="flex-none p-2 md:p-4">
|
||||
<figure class="" v-if="group.avatar">
|
||||
<img
|
||||
class="rounded-full"
|
||||
:src="group.avatar.url"
|
||||
alt=""
|
||||
height="128"
|
||||
width="128"
|
||||
/>
|
||||
</figure>
|
||||
<AccountGroup v-else :size="128" />
|
||||
</div>
|
||||
<div
|
||||
class="py-2 px-2 md:px-4 flex flex-col h-full justify-between w-full"
|
||||
:class="{ 'sm:flex-1': mode === 'row' }"
|
||||
>
|
||||
<div class="flex gap-1 mb-2">
|
||||
<div class="">
|
||||
<figure class="" v-if="group.avatar">
|
||||
<img
|
||||
class="rounded-xl"
|
||||
:src="group.avatar.url"
|
||||
alt=""
|
||||
height="64"
|
||||
width="64"
|
||||
/>
|
||||
</figure>
|
||||
<AccountGroup v-else :size="64" />
|
||||
</div>
|
||||
<div class="px-1 overflow-hidden">
|
||||
<div class="px-1 overflow-hidden flex-auto">
|
||||
<h3
|
||||
class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
|
||||
dir="auto"
|
||||
@ -46,7 +48,10 @@
|
||||
v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
|
||||
:physicalAddress="group.physicalAddress"
|
||||
/>
|
||||
<p class="flex gap-1">
|
||||
<p
|
||||
class="flex gap-1"
|
||||
v-if="group?.members?.total && group?.followers?.total"
|
||||
>
|
||||
<Account />
|
||||
{{
|
||||
t(
|
||||
@ -58,14 +63,28 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p
|
||||
class="flex gap-1"
|
||||
v-else-if="group?.membersCount || group?.followersCount"
|
||||
>
|
||||
<Account />
|
||||
{{
|
||||
t(
|
||||
"{count} members or followers",
|
||||
{
|
||||
count: (group.membersCount ?? 0) + (group.followersCount ?? 0),
|
||||
},
|
||||
(group.membersCount ?? 0) + (group.followersCount ?? 0)
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</LinkOrRouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { displayName, IGroup, usernameWithDomain } from "@/types/actor";
|
||||
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||
import { addressFullName } from "@/types/address.model";
|
||||
@ -74,16 +93,40 @@ import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||
import Account from "vue-material-design-icons/Account.vue";
|
||||
import { htmlToText } from "@/utils/html";
|
||||
import { computed } from "vue";
|
||||
import LinkOrRouterLink from "../core/LinkOrRouterLink.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
group: IGroup;
|
||||
showSummary: boolean;
|
||||
showSummary?: boolean;
|
||||
isRemoteGroup?: boolean;
|
||||
isLoggedIn?: boolean;
|
||||
mode?: "row" | "column";
|
||||
}>(),
|
||||
{ showSummary: true }
|
||||
{ showSummary: true, isRemoteGroup: false, isLoggedIn: true, mode: "column" }
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const saneSummary = computed(() => htmlToText(props.group.summary));
|
||||
const saneSummary = computed(() => htmlToText(props.group.summary ?? ""));
|
||||
|
||||
const isInternal = computed(() => {
|
||||
return props.isRemoteGroup && props.isLoggedIn === false;
|
||||
});
|
||||
|
||||
const to = computed(() => {
|
||||
if (props.isRemoteGroup) {
|
||||
if (props.isLoggedIn === false) {
|
||||
return props.group.url;
|
||||
}
|
||||
return {
|
||||
name: RouteName.INTERACT,
|
||||
query: { uri: encodeURI(props.group.url) },
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(props.group) },
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -73,19 +73,19 @@ const adminMember: IMember = {
|
||||
role: MemberRole.ADMINISTRATOR,
|
||||
};
|
||||
|
||||
const groupWithMedia = {
|
||||
...basicGroup,
|
||||
banner: {
|
||||
url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg",
|
||||
},
|
||||
avatar: {
|
||||
url: "https://mobilizon.fr/media/ff5b2d425fb73e17fcbb56a1a032359ee0b21453c11af59e103e783817a32fdf.png?name=framasoft%27s%20avatar.png",
|
||||
},
|
||||
};
|
||||
// const groupWithMedia = {
|
||||
// ...basicGroup,
|
||||
// banner: {
|
||||
// url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg",
|
||||
// },
|
||||
// avatar: {
|
||||
// url: "https://mobilizon.fr/media/ff5b2d425fb73e17fcbb56a1a032359ee0b21453c11af59e103e783817a32fdf.png?name=framasoft%27s%20avatar.png",
|
||||
// },
|
||||
// };
|
||||
|
||||
const groupWithFollowersOrMembers = {
|
||||
...groupWithMedia,
|
||||
members: { total: 2, elements: [] },
|
||||
followers: { total: 5, elements: [] },
|
||||
};
|
||||
// const groupWithFollowersOrMembers = {
|
||||
// ...groupWithMedia,
|
||||
// members: { total: 2, elements: [] },
|
||||
// followers: { total: 5, elements: [] },
|
||||
// };
|
||||
</script>
|
||||
|
@ -41,19 +41,19 @@
|
||||
}"
|
||||
>
|
||||
<h2 class="mt-0">{{ member.parent.name }}</h2>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-sm">{{
|
||||
`@${usernameWithDomain(member.parent)}`
|
||||
}}</span>
|
||||
<tag
|
||||
variant="info"
|
||||
v-if="member.role === MemberRole.ADMINISTRATOR"
|
||||
>{{ $t("Administrator") }}</tag
|
||||
>{{ t("Administrator") }}</tag
|
||||
>
|
||||
<tag
|
||||
variant="info"
|
||||
v-else-if="member.role === MemberRole.MODERATOR"
|
||||
>{{ $t("Moderator") }}</tag
|
||||
>{{ t("Moderator") }}</tag
|
||||
>
|
||||
</div>
|
||||
</router-link>
|
||||
@ -77,7 +77,7 @@
|
||||
@click="emit('leave')"
|
||||
>
|
||||
<ExitToApp />
|
||||
{{ $t("Leave") }}
|
||||
{{ t("Leave") }}
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</div>
|
||||
@ -96,10 +96,13 @@ import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Tag from "@/components/Tag.vue";
|
||||
import { htmlToText } from "@/utils/html";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
defineProps<{
|
||||
member: IMember;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["leave"]);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content media">
|
||||
<div class="media-content">
|
||||
<div class="">
|
||||
<div class="">
|
||||
<div class="">
|
||||
<div class="prose dark:prose-invert">
|
||||
<i18n-t
|
||||
tag="p"
|
||||
@ -12,12 +12,18 @@
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="media subfield">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="member.parent.avatar">
|
||||
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
|
||||
<div class="">
|
||||
<div class="">
|
||||
<figure v-if="member.parent.avatar">
|
||||
<img
|
||||
class="rounded"
|
||||
:src="member.parent.avatar.url"
|
||||
alt=""
|
||||
height="48"
|
||||
width="48"
|
||||
/>
|
||||
</figure>
|
||||
<o-icon v-else size="large" icon="account-group" />
|
||||
<AccountGroup :size="48" v-else />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="level">
|
||||
@ -31,8 +37,8 @@
|
||||
},
|
||||
}"
|
||||
>
|
||||
<h3 class="is-size-5">{{ member.parent.name }}</h3>
|
||||
<p class="is-size-7 has-text-grey-dark">
|
||||
<h3 class="">{{ member.parent.name }}</h3>
|
||||
<p class="">
|
||||
<span v-if="member.parent.domain">
|
||||
{{
|
||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||
@ -45,8 +51,8 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<div class="">
|
||||
<div class="">
|
||||
<o-button
|
||||
variant="success"
|
||||
@click="$emit('accept', member.id)"
|
||||
@ -54,7 +60,7 @@
|
||||
{{ $t("Accept") }}
|
||||
</o-button>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<div class="">
|
||||
<o-button
|
||||
variant="danger"
|
||||
@click="$emit('reject', member.id)"
|
||||
@ -75,6 +81,7 @@
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import RouteName from "../../router/name";
|
||||
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||
|
||||
defineProps<{
|
||||
member: IMember;
|
||||
|
18
js/src/components/Group/SkeletonGroupResult.vue
Normal file
18
js/src/components/Group/SkeletonGroupResult.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 shadow rounded-md max-w-sm w-full mx-auto"
|
||||
>
|
||||
<div class="animate-pulse flex flex-col space-3-4 items-center">
|
||||
<div
|
||||
class="object-cover h-40 w-40 rounded-full bg-slate-700 p-2 md:p-4"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex gap-3 flex self-start flex-col justify-between p-2 md:p-4 w-full"
|
||||
>
|
||||
<div class="h-5 bg-slate-700"></div>
|
||||
<div class="h-3 bg-slate-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -28,8 +28,7 @@ import { CATEGORY_STATISTICS } from "@/graphql/statistics";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import shuffle from "lodash/shuffle";
|
||||
import { categoriesWithPictures } from "../Categories/constants";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { useEventCategories } from "@/composition/apollo/config";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@ -40,14 +39,10 @@ const categoryStats = computed(
|
||||
() => categoryStatsResult.value?.categoryStatistics ?? []
|
||||
);
|
||||
|
||||
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
|
||||
|
||||
const config = computed(() => configResult.value?.config);
|
||||
|
||||
const eventCategories = computed(() => config.value?.eventCategories ?? []);
|
||||
const { eventCategories } = useEventCategories();
|
||||
|
||||
const eventCategoryLabel = (categoryId: string): string | undefined => {
|
||||
return eventCategories.value.find(({ id }) => categoryId == id)?.label;
|
||||
return eventCategories.value?.find(({ id }) => categoryId == id)?.label;
|
||||
};
|
||||
|
||||
const promotedCategories = computed((): CategoryStatsModel[] => {
|
||||
|
@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<form
|
||||
id="search-anchor"
|
||||
class="container mx-auto my-3 px-2 flex flex-wrap flex-col sm:flex-row items-stretch gap-2 text-center items-center justify-center dark:text-slate-100"
|
||||
class="container mx-auto my-3 flex flex-wrap flex-col sm:flex-row items-stretch gap-2 text-center items-center justify-center dark:text-slate-100"
|
||||
role="search"
|
||||
@submit.prevent="emit('submit')"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<label class="sr-only" for="search_field_input">{{
|
||||
t("Keyword, event title, group name, etc.")
|
||||
}}</label>
|
||||
<o-input
|
||||
class="flex-1"
|
||||
v-model="search"
|
||||
:placeholder="t('Keyword, event title, group name, etc.')"
|
||||
id="search_field_input"
|
||||
autofocus
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
@ -21,8 +24,10 @@
|
||||
v-model="location"
|
||||
:hide-map="true"
|
||||
:hide-selected="true"
|
||||
:default-text="locationDefaultText"
|
||||
labelClass="sr-only"
|
||||
/>
|
||||
<o-button type="submit" icon-left="magnify">
|
||||
<o-button native-type="submit" icon-left="magnify">
|
||||
<template v-if="search">{{ t("Go!") }}</template>
|
||||
<template v-else>{{ t("Explore!") }}</template>
|
||||
</o-button>
|
||||
@ -35,23 +40,28 @@ import { AddressSearchType } from "@/types/enums";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
const props = defineProps<{
|
||||
location: IAddress;
|
||||
location: IAddress | null;
|
||||
locationDefaultText?: string | null;
|
||||
search: string;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:location", location: IAddress): void;
|
||||
(event: "update:location", location: IAddress | null): void;
|
||||
(event: "update:search", newSearch: string): void;
|
||||
(event: "submit"): void;
|
||||
}>();
|
||||
|
||||
const location = computed({
|
||||
get(): IAddress {
|
||||
get(): IAddress | null {
|
||||
return props.location;
|
||||
},
|
||||
set(newLocation: IAddress) {
|
||||
set(newLocation: IAddress | null) {
|
||||
emit("update:location", newLocation);
|
||||
},
|
||||
});
|
||||
@ -65,6 +75,25 @@ const search = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
emit("submit");
|
||||
const lat = location.value?.geom
|
||||
? parseFloat(location.value?.geom?.split(";")?.[1])
|
||||
: undefined;
|
||||
const lon = location.value?.geom
|
||||
? parseFloat(location.value?.geom?.split(";")?.[0])
|
||||
: undefined;
|
||||
router.push({
|
||||
name: RouteName.SEARCH,
|
||||
query: {
|
||||
locationName: location.value?.locality ?? location.value?.region,
|
||||
lat,
|
||||
lon,
|
||||
search: search.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
</script>
|
||||
<style scoped>
|
||||
|
@ -26,7 +26,7 @@
|
||||
>{{ t("Create an account") }}</o-button
|
||||
>
|
||||
<!-- We don't invite to find other instances yet -->
|
||||
<!-- <o-button v-else type="is-link" tag="a" href="https://joinmastodon.org">{{ t('Find an instance') }}</o-button> -->
|
||||
<!-- <o-button v-else variant="link" tag="a" href="https://joinmastodon.org">{{ t('Find an instance') }}</o-button> -->
|
||||
<router-link
|
||||
:to="{ name: RouteName.ABOUT }"
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-violet-title focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
@ -41,7 +41,12 @@ import { IConfig } from "@/types/config.model";
|
||||
import RouteName from "@/router/name";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
defineProps<{ config: IConfig }>();
|
||||
defineProps<{
|
||||
config: Pick<
|
||||
IConfig,
|
||||
"name" | "description" | "slogan" | "registrationsOpen"
|
||||
>;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
</script>
|
||||
|
@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<div
|
||||
class="relative w-full snap-x snap-always snap-mandatory overflow-x-auto flex pb-6 gap-x-5 gap-y-8"
|
||||
class="relative w-full snap-x snap-always snap-mandatory overflow-x-auto flex pb-6 gap-x-5 gap-y-8 p-1"
|
||||
ref="scrollContainer"
|
||||
@scroll="scrollHandler"
|
||||
>
|
||||
|
@ -29,7 +29,7 @@
|
||||
<more-content
|
||||
v-if="userLocationName && userLocation?.lat && userLocation?.lon"
|
||||
:to="{
|
||||
name: 'SEARCH',
|
||||
name: RouteName.SEARCH,
|
||||
query: {
|
||||
locationName: userLocationName,
|
||||
lat: userLocation.lat?.toString(),
|
||||
@ -63,6 +63,8 @@ import { Paginate } from "@/types/paginate";
|
||||
import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { coordsToGeoHash } from "@/utils/location";
|
||||
import { roundToNearestMinute } from "@/utils/datetime";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
const props = defineProps<{ userLocation: LocationType }>();
|
||||
const emit = defineEmits(["doGeoLoc"]);
|
||||
@ -77,17 +79,27 @@ const userLocationName = computed(() => {
|
||||
});
|
||||
const suggestGeoloc = computed(() => props.userLocation?.isIPLocation);
|
||||
|
||||
const geoHash = computed(() =>
|
||||
coordsToGeoHash(props.userLocation.lat, props.userLocation.lon)
|
||||
);
|
||||
|
||||
const { result: eventsResult, loading: loadingEvents } = useQuery<{
|
||||
searchEvents: Paginate<IEvent>;
|
||||
}>(SEARCH_EVENTS, () => ({
|
||||
location: coordsToGeoHash(props.userLocation.lat, props.userLocation.lon),
|
||||
beginsOn: new Date(),
|
||||
endsOn: undefined,
|
||||
radius: 25,
|
||||
eventPage: 1,
|
||||
limit: EVENT_PAGE_LIMIT,
|
||||
type: "IN_PERSON",
|
||||
}));
|
||||
}>(
|
||||
SEARCH_EVENTS,
|
||||
() => ({
|
||||
location: geoHash.value,
|
||||
beginsOn: roundToNearestMinute(new Date()),
|
||||
endsOn: undefined,
|
||||
radius: 25,
|
||||
eventPage: 1,
|
||||
limit: EVENT_PAGE_LIMIT,
|
||||
type: "IN_PERSON",
|
||||
}),
|
||||
() => ({
|
||||
enabled: geoHash.value !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const events = computed(
|
||||
() => eventsResult.value?.searchEvents ?? { elements: [], total: 0 }
|
||||
|
@ -18,12 +18,12 @@
|
||||
</template>
|
||||
</template>
|
||||
<template #content>
|
||||
<!-- <skeleton-group-result
|
||||
<skeleton-group-result
|
||||
v-for="i in [...Array(6).keys()]"
|
||||
class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4"
|
||||
:key="i"
|
||||
v-show="loadingGroups"
|
||||
/> -->
|
||||
/>
|
||||
<group-card
|
||||
v-for="group in selectedGroups"
|
||||
:key="group.id"
|
||||
@ -37,7 +37,7 @@
|
||||
<more-content
|
||||
v-if="userLocationName"
|
||||
:to="{
|
||||
name: 'SEARCH',
|
||||
name: RouteName.SEARCH,
|
||||
query: {
|
||||
locationName: userLocationName,
|
||||
lat: userLocation.lat?.toString(),
|
||||
@ -59,9 +59,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// import SkeletonGroupResult from "../../components/result/SkeletonGroupResult.vue";
|
||||
import SkeletonGroupResult from "@/components/Group/SkeletonGroupResult.vue";
|
||||
import sampleSize from "lodash/sampleSize";
|
||||
import { LocationType } from "../../types/user-location.model";
|
||||
import { LocationType } from "@/types/user-location.model";
|
||||
import MoreContent from "./MoreContent.vue";
|
||||
import CloseContent from "./CloseContent.vue";
|
||||
import { IGroup } from "@/types/actor";
|
||||
@ -72,6 +72,7 @@ import { computed } from "vue";
|
||||
import GroupCard from "@/components/Group/GroupCard.vue";
|
||||
import { coordsToGeoHash } from "@/utils/location";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
const props = defineProps<{ userLocation: LocationType }>();
|
||||
const emit = defineEmits(["doGeoLoc"]);
|
||||
|
@ -33,7 +33,7 @@
|
||||
/>
|
||||
<more-content
|
||||
:to="{
|
||||
name: 'SEARCH',
|
||||
name: RouteName.SEARCH,
|
||||
query: {
|
||||
contentType: 'EVENTS',
|
||||
},
|
||||
@ -57,6 +57,7 @@ import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
|
||||
import { EventSortField, SortDirection } from "@/types/enums";
|
||||
import { FETCH_EVENTS } from "@/graphql/event";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
defineProps<{
|
||||
instanceName: string;
|
||||
|
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<close-content
|
||||
class="container mx-auto px-2"
|
||||
:suggest-geoloc="false"
|
||||
v-show="loadingEvents || events.length > 0"
|
||||
v-show="loadingEvents || (events?.elements && events?.elements.length > 0)"
|
||||
>
|
||||
<template #title>
|
||||
{{ $t("Online upcoming events") }}
|
||||
@ -15,7 +16,7 @@
|
||||
/>
|
||||
<event-card
|
||||
class="scroll-ml-6 snap-center shrink-0 first:pl-8 last:pr-8 w-[18rem]"
|
||||
v-for="event in events"
|
||||
v-for="event in events?.elements"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
view-mode="column"
|
||||
@ -24,7 +25,7 @@
|
||||
/>
|
||||
<more-content
|
||||
:to="{
|
||||
name: 'SEARCH',
|
||||
name: RouteName.SEARCH,
|
||||
query: {
|
||||
contentType: 'EVENTS',
|
||||
isOnline: 'true',
|
||||
@ -50,25 +51,27 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import SkeletonEventResult from "../result/SkeletonEventResult.vue";
|
||||
import SkeletonEventResult from "@/components/Event/SkeletonEventResult.vue";
|
||||
import MoreContent from "./MoreContent.vue";
|
||||
import CloseContent from "./CloseContent.vue";
|
||||
import { SEARCH_EVENTS } from "@/graphql/search";
|
||||
import EventCard from "../../components/Event/EventCard.vue";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import RouteName from "@/router/name";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
|
||||
const EVENT_PAGE_LIMIT = 12;
|
||||
|
||||
const { result: searchEventResult, loading: loadingEvents } = useQuery(
|
||||
SEARCH_EVENTS,
|
||||
() => ({
|
||||
beginsOn: new Date(),
|
||||
endsOn: undefined,
|
||||
eventPage: 1,
|
||||
limit: EVENT_PAGE_LIMIT,
|
||||
type: "ONLINE",
|
||||
})
|
||||
);
|
||||
const { result: searchEventResult, loading: loadingEvents } = useQuery<{
|
||||
searchEvents: Paginate<IEvent>;
|
||||
}>(SEARCH_EVENTS, () => ({
|
||||
beginsOn: new Date(),
|
||||
endsOn: undefined,
|
||||
eventPage: 1,
|
||||
limit: EVENT_PAGE_LIMIT,
|
||||
type: "ONLINE",
|
||||
}));
|
||||
|
||||
const events = computed(() => searchEventResult.value.searchEvents);
|
||||
const events = computed(() => searchEventResult.value?.searchEvents);
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<svg
|
||||
class="bg-white dark:bg-gray-900 dark:fill-white"
|
||||
class="bg-white dark:bg-zinc-900 dark:fill-white"
|
||||
:class="{ 'bg-gray-900': invert }"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 248.16 46.78"
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<nav class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-gray-900">
|
||||
<nav class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-zinc-900">
|
||||
<div class="container mx-auto flex flex-wrap items-center mx-auto gap-4">
|
||||
<router-link :to="{ name: RouteName.HOME }" class="flex items-center">
|
||||
<MobilizonLogo class="w-40" />
|
||||
</router-link>
|
||||
<div class="flex items-center md:order-2 ml-auto" v-if="currentActor?.id">
|
||||
<o-dropdown>
|
||||
<o-dropdown position="bottom-left">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
@ -14,33 +14,80 @@
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">{{ t("Open user menu") }}</span>
|
||||
<figure class="" v-if="currentActor?.avatar">
|
||||
<figure class="h-8 w-8" v-if="currentActor?.avatar">
|
||||
<img
|
||||
class="rounded-full"
|
||||
class="rounded-full w-full h-full object-cover"
|
||||
alt=""
|
||||
:src="currentActor?.avatar.url"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
</figure>
|
||||
<AccountCircle :size="32" />
|
||||
<AccountCircle v-else :size="32" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
class="z-50 mt-4 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
class="z-50 text-base list-none bg-white rounded divide-y divide-gray-100 dark:bg-zinc-700 dark:divide-gray-600 max-w-xs"
|
||||
position="bottom-left"
|
||||
>
|
||||
<o-dropdown-item aria-role="listitem">
|
||||
<div class="">
|
||||
<span class="block text-sm text-gray-900 dark:text-white">{{
|
||||
<div class="px-4">
|
||||
<span class="block text-sm text-zinc-900 dark:text-white">{{
|
||||
displayName(currentActor)
|
||||
}}</span>
|
||||
<span
|
||||
class="block text-sm font-medium text-gray-500 truncate dark:text-gray-400"
|
||||
>{{ currentUser?.role }}</span
|
||||
class="block text-sm font-medium text-zinc-500 truncate dark:text-zinc-400"
|
||||
v-if="currentUser?.role === ICurrentUserRole.ADMINISTRATOR"
|
||||
>{{ t("Administrator") }}</span
|
||||
>
|
||||
<span
|
||||
class="block text-sm font-medium text-zinc-500 truncate dark:text-zinc-400"
|
||||
v-if="currentUser?.role === ICurrentUserRole.MODERATOR"
|
||||
>{{ t("Moderator") }}</span
|
||||
>
|
||||
</div>
|
||||
</o-dropdown-item>
|
||||
<o-dropdown-item
|
||||
v-for="identity in identities"
|
||||
:active="identity.id === currentActor.id"
|
||||
:key="identity.id"
|
||||
tabindex="0"
|
||||
@click="
|
||||
setIdentity({
|
||||
preferredUsername: identity.preferredUsername,
|
||||
})
|
||||
"
|
||||
@keyup.enter="
|
||||
setIdentity({
|
||||
preferredUsername: identity.preferredUsername,
|
||||
})
|
||||
"
|
||||
>
|
||||
<div class="flex gap-1 items-center">
|
||||
<div class="flex-none">
|
||||
<figure class="" v-if="identity.avatar">
|
||||
<img
|
||||
class="rounded-full h-8 w-8"
|
||||
loading="lazy"
|
||||
:src="identity.avatar.url"
|
||||
alt=""
|
||||
height="32"
|
||||
width="32"
|
||||
/>
|
||||
</figure>
|
||||
<AccountCircle v-else :size="32" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-base text-zinc-700 dark:text-zinc-100 flex flex-col flex-auto overflow-hidden items-start"
|
||||
>
|
||||
<p class="truncate">{{ displayName(identity) }}</p>
|
||||
<p class="truncate text-sm" v-if="identity.name">
|
||||
@{{ identity.preferredUsername }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</o-dropdown-item>
|
||||
<o-dropdown-item
|
||||
@ -49,7 +96,7 @@
|
||||
:to="{ name: RouteName.SETTINGS }"
|
||||
>
|
||||
<span
|
||||
class="block py-2 px-4 text-sm text-gray-700 dark:text-gray-200 dark:hover:text-white"
|
||||
class="block py-2 px-4 text-sm text-zinc-700 dark:text-zinc-200 dark:hover:text-white"
|
||||
>{{ t("My account") }}</span
|
||||
>
|
||||
</o-dropdown-item>
|
||||
@ -60,7 +107,7 @@
|
||||
:to="{ name: RouteName.ADMIN_DASHBOARD }"
|
||||
>
|
||||
<span
|
||||
class="block py-2 px-4 text-sm text-gray-700 dark:text-gray-200 dark:hover:text-white"
|
||||
class="block py-2 px-4 text-sm text-zinc-700 dark:text-zinc-200 dark:hover:text-white"
|
||||
>{{ t("Administration") }}</span
|
||||
>
|
||||
</o-dropdown-item>
|
||||
@ -70,7 +117,7 @@
|
||||
@keyup.enter="logout"
|
||||
>
|
||||
<span
|
||||
class="block py-2 px-4 text-sm text-gray-700 dark:text-gray-200 dark:hover:text-white"
|
||||
class="block py-2 px-4 text-sm text-zinc-700 dark:text-zinc-200 dark:hover:text-white"
|
||||
>{{ t("Log out") }}</span
|
||||
>
|
||||
</o-dropdown-item>
|
||||
@ -80,7 +127,7 @@
|
||||
<button
|
||||
@click="showMobileMenu = !showMobileMenu"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
class="inline-flex items-center p-2 ml-1 text-sm text-zinc-500 rounded-lg md:hidden hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:focus:ring-gray-600"
|
||||
aria-controls="mobile-menu-2"
|
||||
aria-expanded="false"
|
||||
>
|
||||
@ -105,33 +152,33 @@
|
||||
:class="{ hidden: !showMobileMenu }"
|
||||
>
|
||||
<ul
|
||||
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:text-sm md:font-medium"
|
||||
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold"
|
||||
>
|
||||
<li v-if="currentActor?.id">
|
||||
<router-link
|
||||
:to="{ name: RouteName.MY_EVENTS }"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("My events") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li v-if="currentActor?.id">
|
||||
<router-link
|
||||
:to="{ name: RouteName.MY_GROUPS }"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("My groups") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li v-if="!currentActor?.id">
|
||||
<router-link
|
||||
:to="{ name: RouteName.LOGIN }"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Login") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li v-if="!currentActor?.id">
|
||||
<router-link
|
||||
:to="{ name: RouteName.REGISTER }"
|
||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
>{{ t("Register") }}</router-link
|
||||
>
|
||||
</li>
|
||||
@ -327,6 +374,9 @@ import {
|
||||
useCurrentActorClient,
|
||||
useCurrentUserIdentities,
|
||||
} from "@/composition/apollo/actor";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
|
||||
import { changeIdentity } from "@/utils/identity";
|
||||
// import { useRestrictions } from "@/composition/apollo/config";
|
||||
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
@ -400,11 +450,17 @@ watch(identities, () => {
|
||||
// await router.push({ name: RouteName.HOME });
|
||||
// };
|
||||
|
||||
// const { onDone, mutate: setIdentity } = useMutation(UPDATE_DEFAULT_ACTOR);
|
||||
const { onDone, mutate: setIdentity } = useMutation<{
|
||||
changeDefaultActor: { id: string; defaultActor: { id: string } };
|
||||
}>(UPDATE_DEFAULT_ACTOR);
|
||||
|
||||
// onDone(() => {
|
||||
// changeIdentity(identity);
|
||||
// });
|
||||
onDone(({ data }) => {
|
||||
const identity = identities.value?.find(
|
||||
({ id }) => id === data?.changeDefaultActor?.defaultActor?.id
|
||||
);
|
||||
if (!identity) return;
|
||||
changeIdentity(identity);
|
||||
});
|
||||
|
||||
// const hideCreateEventsButton = computed((): boolean => {
|
||||
// return !!restrictions.value?.onlyGroupsCanCreateEvents;
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<section class="container mx-auto">
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ $t("Your participation request is being validated") }}
|
||||
{{ t("Your participation request is being validated") }}
|
||||
</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed && participation === undefined">
|
||||
<o-notification
|
||||
:title="$t('Error while validating participation request')"
|
||||
:title="t('Error while validating participation request')"
|
||||
variant="danger"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"Either the participation request has already been validated, either the validation token is incorrect."
|
||||
)
|
||||
}}
|
||||
@ -18,27 +18,25 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">
|
||||
{{ $t("Your participation request has been validated") }}
|
||||
{{ t("Your participation request has been validated") }}
|
||||
</h1>
|
||||
<p
|
||||
class="prose dark:prose-invert"
|
||||
v-if="participation?.event.joinOptions == EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
{{
|
||||
$t("Your participation still has to be approved by the organisers.")
|
||||
t("Your participation still has to be approved by the organisers.")
|
||||
}}
|
||||
</p>
|
||||
<div v-if="failed">
|
||||
<o-notification
|
||||
:title="
|
||||
$t(
|
||||
'Error while updating participation status inside this browser'
|
||||
)
|
||||
t('Error while updating participation status inside this browser')
|
||||
"
|
||||
variant="warning"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
t(
|
||||
"We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue."
|
||||
)
|
||||
}}
|
||||
@ -46,15 +44,15 @@
|
||||
</div>
|
||||
<div class="columns has-text-centered">
|
||||
<div class="column">
|
||||
<router-link
|
||||
native-type="button"
|
||||
tag="a"
|
||||
class="button is-primary is-large"
|
||||
<o-button
|
||||
tag="router-link"
|
||||
variant="primary"
|
||||
size="large"
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: participation?.event.uuid },
|
||||
}"
|
||||
>{{ $t("Go to the event page") }}</router-link
|
||||
>{{ t("Go to the event page") }}</o-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,10 +26,7 @@
|
||||
<template #popper>
|
||||
{{ t("Click for more information") }}
|
||||
</template>
|
||||
<span
|
||||
class="is-clickable"
|
||||
@click="isAnonymousParticipationModalOpen = true"
|
||||
>
|
||||
<span @click="isAnonymousParticipationModalOpen = true">
|
||||
<InformationOutline :size="16" />
|
||||
</span>
|
||||
</VTooltip>
|
||||
@ -102,7 +99,8 @@
|
||||
</p>
|
||||
<div class="buttons" v-if="isSecureContext()">
|
||||
<o-button
|
||||
type="is-danger is-outlined"
|
||||
variant="danger"
|
||||
outlined
|
||||
@click="clearEventParticipationData"
|
||||
>
|
||||
{{ t("Clear participation data for this event") }}
|
||||
@ -197,7 +195,7 @@ const isEventNotAlreadyPassed = computed((): boolean => {
|
||||
return new Date(endDate.value) > new Date();
|
||||
});
|
||||
|
||||
const endDate = computed((): Date => {
|
||||
const endDate = computed((): string => {
|
||||
return props.event.endsOn !== null &&
|
||||
props.event.endsOn > props.event.beginsOn
|
||||
? props.event.endsOn
|
||||
|
@ -33,7 +33,7 @@
|
||||
}}
|
||||
</small>
|
||||
<o-tooltip
|
||||
type="is-dark"
|
||||
variant="dark"
|
||||
:label="
|
||||
$t(
|
||||
'Mobilizon is a federated network. You can interact with this event from a different server.'
|
||||
@ -90,7 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-text-centered">
|
||||
<o-button tag="a" type="is-text" @click="router.go(-1)">{{
|
||||
<o-button tag="a" variant="text" @click="router.go(-1)">{{
|
||||
$t("Back to previous page")
|
||||
}}</o-button>
|
||||
</div>
|
||||
|
@ -41,7 +41,7 @@
|
||||
</o-upload>
|
||||
</o-field>
|
||||
<o-button
|
||||
type="is-text"
|
||||
variant="text"
|
||||
v-if="imageSrc"
|
||||
@click="removeOrClearPicture"
|
||||
@keyup.enter="removeOrClearPicture"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="" v-if="report">
|
||||
<div class="dark:bg-zinc-700 p-2 rounded" v-if="report">
|
||||
<div class="flex gap-1">
|
||||
<figure class="" v-if="report.reported.avatar">
|
||||
<img
|
||||
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="preview">
|
||||
<div class="preview text-mbz-purple dark:text-mbz-purple-300">
|
||||
<Folder :size="48" />
|
||||
</div>
|
||||
<div class="body">
|
||||
@ -39,7 +39,7 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import Draggable, { ChangeEvent } from "@xiaoshuapp/draggable";
|
||||
// import Draggable, { ChangeEvent } from "@xiaoshuapp/draggable";
|
||||
// import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { IResource } from "@/types/resource";
|
||||
import RouteName from "@/router/name";
|
||||
@ -110,8 +110,8 @@ onMovedResource(({ data }) => {
|
||||
onMovedResourceError((e) => {
|
||||
// Snackbar.open({
|
||||
// message: e.message,
|
||||
// type: "is-danger",
|
||||
// position: "is-bottom",
|
||||
// variant: "danger",
|
||||
// position: "bottom",
|
||||
// });
|
||||
return undefined;
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-1 items-center w-full" dir="auto">
|
||||
<a :href="resource.resourceUrl" target="_blank">
|
||||
<div class="preview">
|
||||
<div class="preview text-mbz-purple dark:text-mbz-purple-300">
|
||||
<div
|
||||
v-if="
|
||||
resource.type &&
|
||||
@ -79,7 +79,7 @@ const emit = defineEmits<{
|
||||
(e: "delete", resourceID: string): void;
|
||||
}>();
|
||||
|
||||
const list = ref([]);
|
||||
// const list = ref([]);
|
||||
|
||||
const urlHostname = computed((): string | undefined => {
|
||||
if (props.resource?.resourceUrl) {
|
||||
|
@ -69,7 +69,7 @@
|
||||
/>
|
||||
</article>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<o-button type="is-text" @click="emit('close-move-modal')">{{
|
||||
<o-button variant="text" @click="emit('close-move-modal')">{{
|
||||
$t("Cancel")
|
||||
}}</o-button>
|
||||
<o-button
|
||||
|
@ -56,8 +56,8 @@ const updateSetting = async (
|
||||
} catch (e: any) {
|
||||
// Snackbar.open({
|
||||
// message: e.message,
|
||||
// type: "is-danger",
|
||||
// position: "is-bottom",
|
||||
// variant: "danger",
|
||||
// position: "bottom",
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<li class="setting-menu-item" :class="{ active: isActive }">
|
||||
<li
|
||||
class="setting-menu-item"
|
||||
:class="{
|
||||
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-500': isActive,
|
||||
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-300 dark:hover:bg-mbz-purple-400 dark:text-white':
|
||||
!isActive,
|
||||
}"
|
||||
>
|
||||
<router-link v-if="to" :to="to">
|
||||
<span>{{ title }}</span>
|
||||
</router-link>
|
||||
@ -31,7 +38,7 @@ const isActive = computed((): boolean => {
|
||||
<style lang="scss" scoped>
|
||||
li.setting-menu-item {
|
||||
font-size: 1.05rem;
|
||||
background-color: #fff1de;
|
||||
// background-color: #fff1de;
|
||||
margin: auto;
|
||||
|
||||
span {
|
||||
@ -47,7 +54,7 @@ li.setting-menu-item {
|
||||
&:hover,
|
||||
&.active {
|
||||
cursor: pointer;
|
||||
background-color: lighten(#fea72b, 10%);
|
||||
// background-color: lighten(#fea72b, 10%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<li class="bg-yellow-1 text-violet-2 text-xl">
|
||||
<li
|
||||
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-500 dark:text-zinc-100 text-xl"
|
||||
>
|
||||
<router-link
|
||||
class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline"
|
||||
v-if="to"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside>
|
||||
<aside class="mb-6">
|
||||
<ul>
|
||||
<SettingMenuSection
|
||||
:title="t('Account')"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span class="icon has-text-primary is-large">
|
||||
<span class="text-black dark:text-white dark:fill-white">
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 65.131 65.131"
|
||||
@ -12,7 +12,7 @@
|
||||
/>
|
||||
<path
|
||||
d="m23.631 51.953c-2.348-1.5418-6.9154-5.1737-7.0535-5.6088-0.06717-0.21164 0.45125-0.99318 3.3654-5.0734 2.269-3.177 3.7767-5.3581 3.7767-5.4637 0-0.03748-1.6061-0.60338-3.5691-1.2576-6.1342-2.0442-8.3916-2.9087-8.5288-3.2663-0.03264-0.08506 0.09511-0.68598 0.28388-1.3354 0.643-2.212 2.7038-8.4123 2.7959-8.4123 0.05052 0 2.6821 0.85982 5.848 1.9107 3.1659 1.0509 5.897 1.9222 6.0692 1.9362 0.3089 0.02514 0.31402 0.01925 0.38295-0.44107 0.09851-0.65784 0.26289-5.0029 0.2633-6.9599 1.87e-4 -0.90267 0.02801-2.5298 0.06184-3.6158l0.0615-1.9746h10.392l0.06492 4.4556c0.06287 4.3148 0.18835 7.8236 0.29865 8.3513 0.0295 0.14113 0.11236 0.2566 0.18412 0.2566 0.07176 0 1.6955-0.50861 3.6084-1.1303 4.5213-1.4693 6.2537-2.0038 7.3969-2.2822 0.87349-0.21269 0.94061-0.21704 1.0505-0.06806 0.45169 0.61222 3.3677 9.2365 3.1792 9.4025-0.33681 0.29628-2.492 1.1048-6.9823 2.6194-5.3005 1.7879-5.1321 1.7279-5.1321 1.8283 0 0.13754 0.95042 1.522 3.5468 5.1666 1.3162 1.8475 2.6802 3.7905 3.0311 4.3176l0.63804 0.95842-0.27216 0.28519c-1.1112 1.1644-7.3886 5.8693-7.8309 5.8693-0.22379 0-1.2647-1.2321-2.9284-3.4663-0.90374-1.2137-2.264-3.0402-3.0228-4.059-0.75878-1.0188-1.529-2.0203-1.7116-2.2256l-0.33201-0.37324-0.32674 0.37324c-0.43918 0.50169-2.226 2.867-3.8064 5.0388-2.1662 2.9767-3.6326 4.8055-3.8532 4.8055-0.05161 0-0.4788-0.25278-0.94931-0.56173z"
|
||||
fill="#fff"
|
||||
fill="transparent"
|
||||
stroke-width=".093311"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span class="icon has-text-primary is-large">
|
||||
<span class="text-primary dark:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976">
|
||||
<title>Mastodon logo</title>
|
||||
<path
|
||||
|
@ -5,13 +5,13 @@
|
||||
</header>
|
||||
|
||||
<section class="flex">
|
||||
<div class="">
|
||||
<div class="w-full">
|
||||
<slot></slot>
|
||||
<o-field :label="inputLabel" label-for="url-text">
|
||||
<o-input id="url-text" ref="URLInput" :modelValue="url" expanded />
|
||||
<p class="control">
|
||||
<o-tooltip
|
||||
:label="$t('URL copied to clipboard')"
|
||||
:label="t('URL copied to clipboard')"
|
||||
:active="showCopiedTooltip"
|
||||
always
|
||||
variant="success"
|
||||
@ -23,7 +23,7 @@
|
||||
native-type="button"
|
||||
@click="copyURL"
|
||||
@keyup.enter="copyURL"
|
||||
:title="$t('Copy URL to clipboard')"
|
||||
:title="t('Copy URL to clipboard')"
|
||||
/>
|
||||
</o-tooltip>
|
||||
</p>
|
||||
@ -34,7 +34,7 @@
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
title="Twitter"
|
||||
><Twitter :size="48"
|
||||
><Twitter :size="48" class="dark:text-white"
|
||||
/></a>
|
||||
<a
|
||||
:href="mastodonShare"
|
||||
@ -50,14 +50,14 @@
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
title="Facebook"
|
||||
><Facebook :size="48"
|
||||
><Facebook :size="48" class="dark:text-white"
|
||||
/></a>
|
||||
<a
|
||||
:href="whatsAppShare"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
title="WhatsApp"
|
||||
><Whatsapp :size="48"
|
||||
><Whatsapp :size="48" class="dark:text-white"
|
||||
/></a>
|
||||
<a
|
||||
:href="telegramShare"
|
||||
@ -73,7 +73,7 @@
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
title="LinkedIn"
|
||||
><LinkedIn :size="48"
|
||||
><LinkedIn :size="48" class="dark:text-white"
|
||||
/></a>
|
||||
<a
|
||||
:href="diasporaShare"
|
||||
@ -90,7 +90,7 @@
|
||||
rel="nofollow noopener"
|
||||
title="Email"
|
||||
>
|
||||
<Email :size="48" />
|
||||
<Email :size="48" class="dark:text-white" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -118,6 +118,7 @@ import {
|
||||
twitterShareUrl,
|
||||
whatsAppShareUrl,
|
||||
} from "@/utils/share";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -129,7 +130,9 @@ const props = withDefaults(
|
||||
{}
|
||||
);
|
||||
|
||||
const URLInput = ref<HTMLElement | null>(null);
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const URLInput = ref<{ $refs: { input: HTMLInputElement } } | null>(null);
|
||||
|
||||
const showCopiedTooltip = ref(false);
|
||||
|
||||
@ -159,7 +162,6 @@ const mastodonShare = computed((): string | undefined =>
|
||||
);
|
||||
|
||||
const copyURL = (): void => {
|
||||
console.log("URLInput", URLInput.value);
|
||||
URLInput.value?.$refs.input.select();
|
||||
document.execCommand("copy");
|
||||
showCopiedTooltip.value = true;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span class="icon has-text-primary is-large">
|
||||
<span class="text-primary dark:text-white dark:fill-white">
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Telegram</title>
|
||||
<path
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<span
|
||||
class="rounded-md my-1 truncate text-sm text-violet-title px-2 py-1"
|
||||
:class="[typeClasses, capitalize]"
|
||||
:class="[
|
||||
typeClasses,
|
||||
capitalize,
|
||||
withHashTag ? `before:content-['#']` : '',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
@ -11,10 +15,11 @@ import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: "info" | "danger" | "warning" | "light";
|
||||
capitalize: boolean;
|
||||
variant?: "info" | "danger" | "warning" | "light" | "primary";
|
||||
capitalize?: boolean;
|
||||
withHashTag?: boolean;
|
||||
}>(),
|
||||
{ variant: "light", capitalize: false }
|
||||
{ variant: "light", capitalize: false, withHashTag: false }
|
||||
);
|
||||
|
||||
const typeClasses = computed(() => {
|
||||
@ -23,7 +28,7 @@ const typeClasses = computed(() => {
|
||||
case "light":
|
||||
return "bg-purple-3 dark:text-violet-3";
|
||||
case "info":
|
||||
return "bg-mbz-info dark:text-white";
|
||||
return "bg-mbz-info dark:text-black";
|
||||
case "warning":
|
||||
return "bg-yellow-1";
|
||||
case "danger":
|
||||
@ -33,9 +38,7 @@ const typeClasses = computed(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
span.tag {
|
||||
&:not(.category)::before {
|
||||
content: "#";
|
||||
}
|
||||
span.withHashTag::before {
|
||||
content: "#";
|
||||
}
|
||||
</style>
|
||||
|
@ -7,7 +7,7 @@
|
||||
:data-actor-id="currentActor && currentActor.id"
|
||||
>
|
||||
<div
|
||||
class="menubar bar-is-hidden"
|
||||
class="mb-2 menubar bar-is-hidden"
|
||||
v-if="isDescriptionMode"
|
||||
:editor="editor"
|
||||
>
|
||||
@ -16,9 +16,9 @@
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
@click="editor?.chain().focus().toggleBold().run()"
|
||||
type="button"
|
||||
:title="$t('Bold')"
|
||||
:title="t('Bold')"
|
||||
>
|
||||
<o-icon icon="format-bold" />
|
||||
<FormatBold :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -26,9 +26,9 @@
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
@click="editor?.chain().focus().toggleItalic().run()"
|
||||
type="button"
|
||||
:title="$t('Italic')"
|
||||
:title="t('Italic')"
|
||||
>
|
||||
<o-icon icon="format-italic" />
|
||||
<FormatItalic :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -36,9 +36,9 @@
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
@click="editor?.chain().focus().toggleUnderline().run()"
|
||||
type="button"
|
||||
:title="$t('Underline')"
|
||||
:title="t('Underline')"
|
||||
>
|
||||
<o-icon icon="format-underline" />
|
||||
<FormatUnderline :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -47,9 +47,9 @@
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||
@click="editor?.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
type="button"
|
||||
:title="$t('Heading Level 1')"
|
||||
:title="t('Heading Level 1')"
|
||||
>
|
||||
<o-icon icon="format-header-1" />
|
||||
<FormatHeader1 :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -58,9 +58,9 @@
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||
@click="editor?.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
type="button"
|
||||
:title="$t('Heading Level 2')"
|
||||
:title="t('Heading Level 2')"
|
||||
>
|
||||
<o-icon icon="format-header-2" />
|
||||
<FormatHeader2 :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -69,9 +69,9 @@
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
||||
@click="editor?.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
type="button"
|
||||
:title="$t('Heading Level 3')"
|
||||
:title="t('Heading Level 3')"
|
||||
>
|
||||
<o-icon icon="format-header-3" />
|
||||
<FormatHeader3 :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -79,9 +79,9 @@
|
||||
@click="showLinkMenu()"
|
||||
:class="{ 'is-active': editor.isActive('link') }"
|
||||
type="button"
|
||||
:title="$t('Add link')"
|
||||
:title="t('Add link')"
|
||||
>
|
||||
<o-icon icon="link" />
|
||||
<LinkIcon :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -89,9 +89,9 @@
|
||||
class="menubar__button"
|
||||
@click="editor?.chain().focus().unsetLink().run()"
|
||||
type="button"
|
||||
:title="$t('Remove link')"
|
||||
:title="t('Remove link')"
|
||||
>
|
||||
<o-icon icon="link-off" />
|
||||
<LinkOff :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -99,9 +99,9 @@
|
||||
v-if="!isBasicMode"
|
||||
@click="showImagePrompt()"
|
||||
type="button"
|
||||
:title="$t('Add picture')"
|
||||
:title="t('Add picture')"
|
||||
>
|
||||
<o-icon icon="image" />
|
||||
<Image :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -110,9 +110,9 @@
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
@click="editor?.chain().focus().toggleBulletList().run()"
|
||||
type="button"
|
||||
:title="$t('Bullet list')"
|
||||
:title="t('Bullet list')"
|
||||
>
|
||||
<o-icon icon="format-list-bulleted" />
|
||||
<FormatListBulleted :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -121,9 +121,9 @@
|
||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||
@click="editor?.chain().focus().toggleOrderedList().run()"
|
||||
type="button"
|
||||
:title="$t('Ordered list')"
|
||||
:title="t('Ordered list')"
|
||||
>
|
||||
<o-icon icon="format-list-numbered" />
|
||||
<FormatListNumbered :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -132,9 +132,9 @@
|
||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||
@click="editor?.chain().focus().toggleBlockquote().run()"
|
||||
type="button"
|
||||
:title="$t('Quote')"
|
||||
:title="t('Quote')"
|
||||
>
|
||||
<o-icon icon="format-quote-close" />
|
||||
<FormatQuoteClose :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -142,9 +142,9 @@
|
||||
class="menubar__button"
|
||||
@click="editor?.chain().focus().undo().run()"
|
||||
type="button"
|
||||
:title="$t('Undo')"
|
||||
:title="t('Undo')"
|
||||
>
|
||||
<o-icon icon="undo" />
|
||||
<Undo :size="24" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -152,9 +152,9 @@
|
||||
class="menubar__button"
|
||||
@click="editor?.chain().focus().redo().run()"
|
||||
type="button"
|
||||
:title="$t('Redo')"
|
||||
:title="t('Redo')"
|
||||
>
|
||||
<o-icon icon="redo" />
|
||||
<Redo :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -169,10 +169,10 @@
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
@click="editor?.chain().focus().toggleBold().run()"
|
||||
type="button"
|
||||
:title="$t('Bold')"
|
||||
:title="t('Bold')"
|
||||
>
|
||||
<o-icon icon="format-bold" />
|
||||
<span class="visually-hidden">{{ $t("Bold") }}</span>
|
||||
<FormatBold :size="24" />
|
||||
<span class="visually-hidden">{{ t("Bold") }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -180,10 +180,10 @@
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
@click="editor?.chain().focus().toggleItalic().run()"
|
||||
type="button"
|
||||
:title="$t('Italic')"
|
||||
:title="t('Italic')"
|
||||
>
|
||||
<o-icon icon="format-italic" />
|
||||
<span class="visually-hidden">{{ $t("Italic") }}</span>
|
||||
<FormatItalic :size="24" />
|
||||
<span class="visually-hidden">{{ t("Italic") }}</span>
|
||||
</button>
|
||||
</bubble-menu>
|
||||
|
||||
@ -223,6 +223,20 @@ import { Dialog } from "@/plugins/dialog";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import FormatBold from "vue-material-design-icons/FormatBold.vue";
|
||||
import FormatItalic from "vue-material-design-icons/FormatItalic.vue";
|
||||
import FormatUnderline from "vue-material-design-icons/FormatUnderline.vue";
|
||||
import FormatHeader1 from "vue-material-design-icons/FormatHeader1.vue";
|
||||
import FormatHeader2 from "vue-material-design-icons/FormatHeader2.vue";
|
||||
import FormatHeader3 from "vue-material-design-icons/FormatHeader3.vue";
|
||||
import LinkIcon from "vue-material-design-icons/Link.vue";
|
||||
import LinkOff from "vue-material-design-icons/LinkOff.vue";
|
||||
import Image from "vue-material-design-icons/Image.vue";
|
||||
import FormatListBulleted from "vue-material-design-icons/FormatListBulleted.vue";
|
||||
import FormatListNumbered from "vue-material-design-icons/FormatListNumbered.vue";
|
||||
import FormatQuoteClose from "vue-material-design-icons/FormatQuoteClose.vue";
|
||||
import Undo from "vue-material-design-icons/Undo.vue";
|
||||
import Redo from "vue-material-design-icons/Redo.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -259,7 +273,7 @@ const isBasicMode = computed((): boolean => {
|
||||
});
|
||||
|
||||
const insertMention = (obj: { range: any; attrs: any }) => {
|
||||
console.log("initialize Mention");
|
||||
console.debug("initialize Mention");
|
||||
};
|
||||
|
||||
const observer = ref<MutationObserver | null>(null);
|
||||
@ -421,7 +435,6 @@ onBeforeUnmount(() => {
|
||||
@import "./Editor/style.scss";
|
||||
|
||||
.menubar {
|
||||
margin-bottom: 1rem;
|
||||
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
|
||||
|
||||
&__button {
|
@ -55,8 +55,8 @@ onDone(() => {
|
||||
onError((e) => {
|
||||
// Snackbar.open({
|
||||
// message: e.message,
|
||||
// type: "is-danger",
|
||||
// position: "is-bottom",
|
||||
// variant: "danger",
|
||||
// position: "bottom",
|
||||
// });
|
||||
});
|
||||
</script>
|
||||
|
@ -85,7 +85,7 @@ updateTodoError((e) => {
|
||||
snackbar?.open({
|
||||
message: e.message,
|
||||
variant: "danger",
|
||||
position: "is-bottom",
|
||||
position: "bottom",
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<nav class="flex mb-3" :aria-label="$t('Breadcrumbs')">
|
||||
<nav class="flex mb-3" :aria-label="t('Breadcrumbs')">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-3 flex-wrap">
|
||||
<li
|
||||
class="inline-flex items-center"
|
||||
@ -57,6 +57,7 @@
|
||||
</nav>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { RouteLocationRaw } from "vue-router";
|
||||
|
||||
type LinkElement = RouteLocationRaw & { text: string };
|
||||
@ -64,4 +65,6 @@ type LinkElement = RouteLocationRaw & { text: string };
|
||||
defineProps<{
|
||||
links: LinkElement[];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
</script>
|
@ -6,7 +6,7 @@
|
||||
<div class="column has-text-centered">
|
||||
<o-button
|
||||
variant="primary"
|
||||
size="is-medium"
|
||||
size="medium"
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.LOGIN,
|
||||
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-text-centered">
|
||||
<o-button tag="a" type="is-text" @click="$router.go(-1)">{{
|
||||
<o-button tag="a" variant="text" @click="$router.go(-1)">{{
|
||||
$t("Back to previous page")
|
||||
}}</o-button>
|
||||
</div>
|
||||
@ -74,9 +74,9 @@ const host = computed((): string => {
|
||||
});
|
||||
|
||||
const redirectToInstance = async (): Promise<void> => {
|
||||
const [, host] = remoteActorAddress.value.split("@", 2);
|
||||
const [, hostname] = remoteActorAddress.value.split("@", 2);
|
||||
const remoteInteractionURI = await webFingerFetch(
|
||||
host,
|
||||
hostname,
|
||||
remoteActorAddress.value
|
||||
);
|
||||
window.open(remoteInteractionURI);
|
||||
|
@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<component
|
||||
:is="computedTag"
|
||||
class="button"
|
||||
v-bind="attrs"
|
||||
:type="computedTag === 'button' ? nativeType : undefined"
|
||||
:class="[
|
||||
size,
|
||||
type,
|
||||
// {
|
||||
// 'is-rounded': rounded,
|
||||
// 'is-loading': loading,
|
||||
// 'is-outlined': outlined,
|
||||
// 'is-fullwidth': expanded,
|
||||
// 'is-inverted': inverted,
|
||||
// 'is-focused': focused,
|
||||
// 'is-active': active,
|
||||
// 'is-hovered': hovered,
|
||||
// 'is-selected': selected,
|
||||
// },
|
||||
]"
|
||||
v-on="attrs"
|
||||
>
|
||||
<!-- <o-icon
|
||||
v-if="iconLeft"
|
||||
:pack="iconPack"
|
||||
:icon="iconLeft"
|
||||
:size="iconSize"
|
||||
/> -->
|
||||
<span v-if="label">{{ label }}</span>
|
||||
<span v-else-if="$slots.default">
|
||||
<slot />
|
||||
</span>
|
||||
<!-- <o-icon
|
||||
v-if="iconRight"
|
||||
:pack="iconPack"
|
||||
:icon="iconRight"
|
||||
:size="iconSize"
|
||||
/> -->
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, useAttrs } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: string;
|
||||
size?: string;
|
||||
label?: string;
|
||||
nativeType?: "button" | "submit" | "reset";
|
||||
tag?: "button" | "a" | "router-link";
|
||||
}>(),
|
||||
{ tag: "button" }
|
||||
);
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const computedTag = computed(() => {
|
||||
if (attrs.disabled !== undefined && attrs.disabled !== false) {
|
||||
return "button";
|
||||
}
|
||||
return props.tag;
|
||||
});
|
||||
|
||||
const iconSize = computed(() => {
|
||||
if (!props.size || props.size === "is-medium") {
|
||||
return "is-small";
|
||||
} else if (props.size === "is-large") {
|
||||
return "is-medium";
|
||||
}
|
||||
return props.size;
|
||||
});
|
||||
</script>
|
@ -63,8 +63,8 @@ const props = withDefaults(
|
||||
canCancel?: boolean;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
onConfirm: (prompt?: string) => {};
|
||||
onCancel?: (source: string) => {};
|
||||
onConfirm: (prompt?: string) => any;
|
||||
onCancel?: (source: string) => any;
|
||||
ariaLabel?: string;
|
||||
ariaModal?: boolean;
|
||||
ariaRole?: string;
|
@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<label :for="labelFor" class="block mb-2">
|
||||
<span class="font-bold mb-2 block">
|
||||
{{ label }}
|
||||
</span>
|
||||
<slot :type="type" />
|
||||
<template v-if="Array.isArray(message) && message.length > 0">
|
||||
<p v-for="msg in message" :key="msg" :class="classNames">
|
||||
{{ msg }}
|
||||
</p>
|
||||
</template>
|
||||
<p v-else-if="typeof message === 'string'" :class="classNames">
|
||||
{{ message }}
|
||||
</p>
|
||||
</label>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
type?: string;
|
||||
message?: string | string[];
|
||||
labelFor?: string;
|
||||
}>();
|
||||
|
||||
const classNames = computed(() => {
|
||||
switch (props.type) {
|
||||
case "is-danger":
|
||||
return "text-red-600";
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,292 +0,0 @@
|
||||
<template>
|
||||
<div class="control" :class="rootClasses">
|
||||
<input
|
||||
v-if="type !== 'textarea'"
|
||||
ref="input"
|
||||
class="input"
|
||||
:class="[inputClasses, customClass]"
|
||||
:type="newType"
|
||||
:autocomplete="autocomplete"
|
||||
:maxlength="maxLength"
|
||||
:value="computedValue"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
v-else
|
||||
ref="textarea"
|
||||
class="textarea"
|
||||
:class="[inputClasses, customClass]"
|
||||
:maxlength="maxLength"
|
||||
:value="computedValue"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
|
||||
<!-- <o-icon
|
||||
v-if="icon"
|
||||
class="is-left"
|
||||
:icon="icon"
|
||||
:size="iconSize"
|
||||
@click.native="emit('icon-click', $event)"
|
||||
/>
|
||||
|
||||
<o-icon
|
||||
v-if="!loading && hasIconRight"
|
||||
class="is-right"
|
||||
:class="{ 'is-clickable': passwordReveal }"
|
||||
:icon="rightIcon"
|
||||
:size="iconSize"
|
||||
:type="rightIconType"
|
||||
both
|
||||
@click.native="rightIconClick"
|
||||
/> -->
|
||||
|
||||
<small
|
||||
v-if="maxLength && hasCounter && type !== 'number'"
|
||||
class="help counter"
|
||||
:class="{ 'is-invisible': !isFocused }"
|
||||
>
|
||||
{{ valueLength }} / {{ maxLength }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
icon?: string;
|
||||
modelValue: number | string;
|
||||
size?: string;
|
||||
type?: string;
|
||||
passwordReveal?: boolean;
|
||||
iconRight?: string;
|
||||
rounded?: boolean;
|
||||
loading?: boolean;
|
||||
customClass?: string;
|
||||
maxLength?: number | string;
|
||||
hasCounter?: boolean;
|
||||
autocomplete?: "on" | "off";
|
||||
statusType?: string;
|
||||
}>(),
|
||||
{
|
||||
type: "text",
|
||||
rounded: false,
|
||||
loading: false,
|
||||
customClass: "",
|
||||
hasCounter: false,
|
||||
autocomplete: "on",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "icon-click", "blur", "focus"]);
|
||||
|
||||
const newValue = ref(props.modelValue);
|
||||
const newType = ref(props.type);
|
||||
const isPasswordVisible = ref(false);
|
||||
const isValid = ref(true);
|
||||
const isFocused = ref(false);
|
||||
|
||||
const computedValue = computed({
|
||||
get() {
|
||||
return newValue.value;
|
||||
},
|
||||
set(value) {
|
||||
newValue.value = value;
|
||||
emit("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
const rootClasses = computed(() => {
|
||||
return [
|
||||
iconPosition,
|
||||
props.size,
|
||||
// {
|
||||
// 'is-expanded': this.expanded,
|
||||
// 'is-loading': this.loading,
|
||||
// 'is-clearfix': !this.hasMessage
|
||||
// }
|
||||
];
|
||||
});
|
||||
const inputClasses = computed(() => {
|
||||
return [props.statusType, props.size, { "is-rounded": props.rounded }];
|
||||
});
|
||||
|
||||
const hasIconRight = computed(() => {
|
||||
return (
|
||||
props.passwordReveal || props.loading || statusTypeIcon || props.iconRight
|
||||
);
|
||||
});
|
||||
const rightIcon = computed(() => {
|
||||
if (props.passwordReveal) {
|
||||
return passwordVisibleIcon;
|
||||
} else if (props.iconRight) {
|
||||
return props.iconRight;
|
||||
}
|
||||
return statusTypeIcon;
|
||||
});
|
||||
const rightIconType = computed(() => {
|
||||
if (props.passwordReveal) {
|
||||
return "is-primary";
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Position of the icon or if it's both sides.
|
||||
*/
|
||||
const iconPosition = computed(() => {
|
||||
let iconClasses = "";
|
||||
if (props.icon) {
|
||||
iconClasses += "has-icons-left ";
|
||||
}
|
||||
if (hasIconRight.value) {
|
||||
iconClasses += "has-icons-right";
|
||||
}
|
||||
return iconClasses;
|
||||
});
|
||||
/**
|
||||
* Icon name (MDI) based on the type.
|
||||
*/
|
||||
const statusTypeIcon = computed(() => {
|
||||
switch (props.statusType) {
|
||||
case "is-success":
|
||||
return "check";
|
||||
case "is-danger":
|
||||
return "alert-circle";
|
||||
case "is-info":
|
||||
return "information";
|
||||
case "is-warning":
|
||||
return "alert";
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Current password-reveal icon name.
|
||||
*/
|
||||
const passwordVisibleIcon = computed(() => {
|
||||
return !isPasswordVisible.value ? "eye" : "eye-off";
|
||||
});
|
||||
/**
|
||||
* Get value length
|
||||
*/
|
||||
const valueLength = computed(() => {
|
||||
if (typeof computedValue.value === "string") {
|
||||
return Array.from(computedValue.value).length;
|
||||
} else if (typeof computedValue.value === "number") {
|
||||
return computedValue.value.toString().length;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* Fix icon size for inputs, large was too big
|
||||
*/
|
||||
const iconSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case "is-small":
|
||||
return props.size;
|
||||
case "is-medium":
|
||||
return;
|
||||
case "is-large":
|
||||
return "is-medium";
|
||||
}
|
||||
});
|
||||
|
||||
watch(props, () => {
|
||||
newValue.value = props.modelValue;
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle the visibility of a password-reveal input
|
||||
* by changing the type and focus the input right away.
|
||||
*/
|
||||
const togglePasswordVisibility = async () => {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
newType.value = isPasswordVisible.value ? "text" : "password";
|
||||
await nextTick();
|
||||
await focus();
|
||||
};
|
||||
const rightIconClick = (event: Event) => {
|
||||
if (props.passwordReveal) {
|
||||
togglePasswordVisibility();
|
||||
}
|
||||
};
|
||||
const onInput = (event: Event) => {
|
||||
const value = event.target?.value;
|
||||
updateValue(value);
|
||||
};
|
||||
const updateValue = (value: string) => {
|
||||
computedValue.value = value;
|
||||
!isValid.value && checkHtml5Validity();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check HTML5 validation, set isValid property.
|
||||
* If validation fail, send 'is-danger' type,
|
||||
* and error message to parent if it's a Field.
|
||||
*/
|
||||
const checkHtml5Validity = () => {
|
||||
const el = getElement();
|
||||
if (el === undefined) return;
|
||||
|
||||
if (!el.value?.checkValidity()) {
|
||||
// setInvalid();
|
||||
isValid.value = false;
|
||||
} else {
|
||||
// setValidity(null, null);
|
||||
isValid.value = true;
|
||||
}
|
||||
|
||||
return isValid.value;
|
||||
};
|
||||
|
||||
// const setInvalid = () => {
|
||||
// let type = "is-danger";
|
||||
// let message = validationMessage || getElement().validationMessage;
|
||||
// setValidity(type, message);
|
||||
// };
|
||||
|
||||
// const setValidity = async (type, message) => {
|
||||
// await nextTick();
|
||||
// if (this.parentField) {
|
||||
// // Set type only if not defined
|
||||
// if (!this.parentField.type) {
|
||||
// this.parentField.newType = type;
|
||||
// }
|
||||
// // Set message only if not defined
|
||||
// if (!this.parentField.message) {
|
||||
// this.parentField.newMessage = message;
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
const textarea = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const getElement = () => {
|
||||
return props.type === "input" ? input : textarea;
|
||||
};
|
||||
|
||||
const focus = async () => {
|
||||
const el = getElement();
|
||||
if (el.value === undefined) return;
|
||||
|
||||
await nextTick();
|
||||
if (el.value) el.value?.focus();
|
||||
};
|
||||
|
||||
const onBlur = ($event: FocusEvent) => {
|
||||
isFocused.value = false;
|
||||
emit("blur", $event);
|
||||
checkHtml5Validity();
|
||||
};
|
||||
|
||||
const onFocus = ($event: FocusEvent) => {
|
||||
isFocused.value = true;
|
||||
emit("focus", $event);
|
||||
};
|
||||
</script>
|
39
js/src/components/core/LinkOrRouterLink.vue
Normal file
39
js/src/components/core/LinkOrRouterLink.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<a
|
||||
v-if="isInternal"
|
||||
:target="newTab ? '_blank' : undefined"
|
||||
:href="href"
|
||||
rel="noopener noreferrer"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
<router-link :to="to" v-bind="$attrs" v-else>
|
||||
<slot />
|
||||
</router-link>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// use normal <script> to declare options
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
to: { name: string; params?: any; query?: any } | string;
|
||||
isInternal?: boolean;
|
||||
newTab?: boolean;
|
||||
}>(),
|
||||
{ isInternal: true, newTab: true }
|
||||
);
|
||||
|
||||
const href = computed(() => {
|
||||
if (typeof props.to === "string" || props.to instanceof String) {
|
||||
return props.to as string;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user