Add global search

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2022-08-26 16:08:58 +02:00
parent bfc936f57c
commit 48935e2168
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
216 changed files with 3646 additions and 2806 deletions

View File

@ -365,6 +365,14 @@ config :mobilizon, Mobilizon.Service.Pictures.Unsplash,
app_name: "Mobilizon", app_name: "Mobilizon",
access_key: nil 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 # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View File

@ -11,7 +11,7 @@ module.exports = {
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
"plugin:vue/vue3-essential", "plugin:vue/vue3-essential",
"@vue/eslint-config-typescript", "@vue/eslint-config-typescript/recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
"@vue/eslint-config-prettier", "@vue/eslint-config-prettier",
], ],
@ -24,12 +24,11 @@ module.exports = {
}, },
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-underscore-dangle": [ "no-underscore-dangle": [
"error", "error",
{ {
allow: ["__typename"], allow: ["__typename", "__schema"],
}, },
], ],
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",

3
js/.gitignore vendored
View File

@ -24,3 +24,6 @@ yarn-error.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/test-results/
/playwright-report/
/playwright/.cache/

View File

@ -1,5 +1,5 @@
const fetch = require("node-fetch"); import fetch from "node-fetch";
const fs = require("fs"); import fs from "fs";
fetch(`http://localhost:4000/api`, { fetch(`http://localhost:4000/api`, {
method: "POST", method: "POST",

View File

@ -51,6 +51,7 @@
"@vue-leaflet/vue-leaflet": "^0.6.1", "@vue-leaflet/vue-leaflet": "^0.6.1",
"@vue/apollo-composable": "^4.0.0-alpha.17", "@vue/apollo-composable": "^4.0.0-alpha.17",
"@vue/compiler-sfc": "^3.2.37", "@vue/compiler-sfc": "^3.2.37",
"@vueuse/core": "^9.1.0",
"@vueuse/head": "^0.7.9", "@vueuse/head": "^0.7.9",
"@vueuse/router": "^9.0.2", "@vueuse/router": "^9.0.2",
"@xiaoshuapp/draggable": "^4.1.0", "@xiaoshuapp/draggable": "^4.1.0",
@ -93,6 +94,7 @@
"devDependencies": { "devDependencies": {
"@histoire/plugin-vue": "^0.10.0", "@histoire/plugin-vue": "^0.10.0",
"@intlify/vite-plugin-vue-i18n": "^6.0.0", "@intlify/vite-plugin-vue-i18n": "^6.0.0",
"@playwright/test": "^1.25.1",
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.1.4",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",

107
js/playwright.config.ts Normal file
View 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;

View File

Before

Width:  |  Height:  |  Size: 920 B

After

Width:  |  Height:  |  Size: 920 B

View File

@ -32,17 +32,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import NavBar from "./components/NavBar.vue"; import NavBar from "@/components/NavBar.vue";
import { import {
AUTH_ACCESS_TOKEN, AUTH_ACCESS_TOKEN,
AUTH_USER_EMAIL, AUTH_USER_EMAIL,
AUTH_USER_ID, AUTH_USER_ID,
AUTH_USER_ROLE, AUTH_USER_ROLE,
} from "./constants"; } from "@/constants";
import { UPDATE_CURRENT_USER_CLIENT } from "./graphql/user"; import { UPDATE_CURRENT_USER_CLIENT } from "@/graphql/user";
import MobilizonFooter from "./components/Footer.vue"; import MobilizonFooter from "@/components/PageFooter.vue";
import jwt_decode, { JwtPayload } from "jwt-decode"; import jwt_decode, { JwtPayload } from "jwt-decode";
import { refreshAccessToken } from "./apollo/utils"; import { refreshAccessToken } from "@/apollo/utils";
import { import {
reactive, reactive,
ref, ref,
@ -52,25 +52,30 @@ import {
onBeforeMount, onBeforeMount,
inject, inject,
defineAsyncComponent, defineAsyncComponent,
computed,
watch,
} from "vue"; } from "vue";
import { LocationType } from "./types/user-location.model"; import { LocationType } from "@/types/user-location.model";
import { useMutation } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
import { initializeCurrentActor } from "./utils/identity"; import { initializeCurrentActor } from "@/utils/identity";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { Snackbar } from "./plugins/snackbar"; import { Snackbar } from "@/plugins/snackbar";
import { Notifier } from "./plugins/notifier"; import { Notifier } from "@/plugins/notifier";
import { import { CONFIG } from "@/graphql/config";
useIsDemoMode, import { IConfig } from "@/types/config.model";
useServerProvidedLocation, import { useRouter } from "vue-router";
} from "./composition/apollo/config";
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
const config = computed(() => configResult.value?.config);
const ErrorComponent = defineAsyncComponent( const ErrorComponent = defineAsyncComponent(
() => import("./components/ErrorComponent.vue") () => import("@/components/ErrorComponent.vue")
); );
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const { location } = useServerProvidedLocation(); const location = computed(() => config.value?.location);
const userLocation = reactive<LocationType>({ const userLocation = reactive<LocationType>({
lon: undefined, lon: undefined,
@ -251,16 +256,19 @@ const showOfflineNetworkWarning = (): void => {
// }, 0); // }, 0);
// }); // });
// watch(config, async (configWatched: IConfig) => { const router = useRouter();
// if (configWatched) {
// const { statistics } = (await import("./services/statistics")) as {
// statistics: (config: IConfig, environment: Record<string, any>) => void;
// };
// statistics(configWatched, { router, version: configWatched.version });
// }
// });
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> </script>
<style lang="scss"> <style lang="scss">

View File

@ -83,7 +83,7 @@ const errorLink = onError(
graphQLErrors.map( graphQLErrors.map(
(graphQLError: GraphQLError & { status_code?: number }) => { (graphQLError: GraphQLError & { status_code?: number }) => {
if (graphQLError?.status_code !== 401) { if (graphQLError?.status_code !== 401) {
console.log( console.debug(
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}` `[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
); );
} }

View File

@ -6,22 +6,35 @@ import { authMiddleware } from "./auth";
import errorLink from "./error-link"; import errorLink from "./error-link";
import { uploadLink } from "./absinthe-upload-socket-link"; import { uploadLink } from "./absinthe-upload-socket-link";
// const link = split( let link;
// // split based on operation type
// ({ query }) => { // The Absinthe socket Apollo link relies on an old library
// const definition = getMainDefinition(query); // (@jumpn/utils-composite) which itself relies on an old
// return ( // Babel version, which is incompatible with Histoire.
// definition.kind === "OperationDefinition" && // We just don't use the absinthe apollo socket link
// definition.operation === "subscription" // in this case.
// ); // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// }, // @ts-ignore
// absintheSocketLink, if (!import.meta.env.VITE_HISTOIRE_ENV) {
// uploadLink // 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(); const retryLink = new RetryLink();
export const fullLink = authMiddleware export const fullLink = authMiddleware
.concat(retryLink) .concat(retryLink)
.concat(errorLink) .concat(errorLink)
.concat(uploadLink); .concat(link ?? uploadLink);

View File

@ -8,7 +8,7 @@ import { Resolvers } from "@apollo/client/core/types";
export default function buildCurrentUserResolver( export default function buildCurrentUserResolver(
cache: ApolloCache<NormalizedCacheObject> cache: ApolloCache<NormalizedCacheObject>
): Resolvers { ): Resolvers {
cache.writeQuery({ cache?.writeQuery({
query: CURRENT_USER_CLIENT, query: CURRENT_USER_CLIENT,
data: { data: {
currentUser: { currentUser: {
@ -21,7 +21,7 @@ export default function buildCurrentUserResolver(
}, },
}); });
cache.writeQuery({ cache?.writeQuery({
query: CURRENT_ACTOR_CLIENT, query: CURRENT_ACTOR_CLIENT,
data: { data: {
currentActor: { currentActor: {
@ -34,7 +34,7 @@ export default function buildCurrentUserResolver(
}, },
}); });
cache.writeQuery({ cache?.writeQuery({
query: CURRENT_USER_LOCATION_CLIENT, query: CURRENT_USER_LOCATION_CLIENT,
data: { data: {
currentUserLocation: { currentUserLocation: {
@ -70,8 +70,6 @@ export default function buildCurrentUserResolver(
}, },
}; };
console.debug("updating current user", data);
localCache.writeQuery({ data, query: CURRENT_USER_CLIENT }); localCache.writeQuery({ data, query: CURRENT_USER_CLIENT });
}, },
updateCurrentActor: ( updateCurrentActor: (

View File

@ -73,6 +73,9 @@ export const typePolicies: TypePolicies = {
Config: { Config: {
merge: true, merge: true,
}, },
Address: {
keyFields: ["id"],
},
RootQueryType: { RootQueryType: {
fields: { fields: {
relayFollowers: paginatedLimitPagination<IFollower>(), relayFollowers: paginatedLimitPagination<IFollower>(),
@ -110,7 +113,7 @@ export async function refreshAccessToken(): Promise<boolean> {
return false; return false;
} }
console.log("Refreshing access token."); console.debug("Refreshing access token.");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const { mutate, onDone, onError } = provideApolloClient(apolloClient)(() => const { mutate, onDone, onError } = provideApolloClient(apolloClient)(() =>
@ -130,7 +133,7 @@ export async function refreshAccessToken(): Promise<boolean> {
}); });
onError((err) => { onError((err) => {
console.debug("Failed to refresh token"); console.debug("Failed to refresh token", err);
reject(false); reject(false);
}); });
}); });

View File

@ -1,11 +1,10 @@
body { 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 */ /* Button */
.btn { .btn {
outline: none !important; @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;
@apply font-bold py-2 px-4 bg-mbz-bluegreen dark:bg-violet-3 text-white rounded h-10;
} }
.btn:hover { .btn:hover {
@apply text-slate-200; @apply text-slate-200;
@ -28,11 +27,14 @@ body {
@apply opacity-50 cursor-not-allowed; @apply opacity-50 cursor-not-allowed;
} }
.btn-danger { .btn-danger {
@apply bg-mbz-danger; @apply bg-mbz-danger hover:bg-mbz-danger/90;
} }
.btn-success { .btn-success {
@apply bg-mbz-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 */
.field { .field {
@ -62,7 +64,7 @@ body {
/* Input */ /* Input */
.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 { .input-danger {
@apply border-red-500; @apply border-red-500;
@ -70,6 +72,10 @@ body {
.input-icon-right { .input-icon-right {
right: 0.5rem; right: 0.5rem;
} }
.input[type="text"]:disabled,
.input[type="email"]:disabled {
@apply bg-zinc-200 dark:bg-zinc-400;
}
.icon-warning { .icon-warning {
@apply text-amber-600; @apply text-amber-600;
@ -78,6 +84,12 @@ body {
.icon-danger { .icon-danger {
@apply text-red-500; @apply text-red-500;
} }
.icon-success {
@apply text-mbz-success;
}
.icon-grey {
@apply text-gray-500;
}
.o-input__icon-left { .o-input__icon-left {
@apply dark:text-black h-10 w-10; @apply dark:text-black h-10 w-10;
@ -111,25 +123,27 @@ body {
} }
.dropdown-menu { .dropdown-menu {
min-width: 12em; 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 { .dropdown-item {
@apply relative inline-flex gap-1 no-underline p-2 cursor-pointer w-full; @apply relative inline-flex gap-1 no-underline p-2 cursor-pointer w-full;
} }
.dropdown-item-active { .dropdown-item-active {
/* @apply bg-violet-2; */ @apply bg-white text-black;
@apply bg-white; }
.dropdown-button {
@apply inline-flex gap-1;
} }
/* Checkbox */ /* Checkbox */
.checkbox { .checkbox {
@apply appearance-none bg-blue-500 border-blue-500; @apply appearance-none bg-primary border-primary;
} }
.checkbox-checked { .checkbox-checked {
@apply bg-blue-500; @apply bg-primary text-primary;
} }
.checkbox-label { .checkbox-label {
@ -139,7 +153,7 @@ body {
/* Modal */ /* Modal */
.modal-content { .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 */ /* Switch */
@ -151,14 +165,18 @@ body {
@apply pl-2; @apply pl-2;
} }
.switch-check-checked {
@apply bg-primary;
}
/* Select */ /* Select */
.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 */ /* Radio */
.form-radio { .form-radio {
@apply bg-none; @apply bg-none text-primary accent-primary;
} }
.radio-label { .radio-label {
@apply pl-2; @apply pl-2;
@ -171,7 +189,7 @@ button.menubar__button {
/* Notification */ /* Notification */
.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 { .notification-primary {
@ -187,18 +205,26 @@ button.menubar__button {
} }
.notification-danger { .notification-danger {
@apply bg-mbz-danger; @apply bg-mbz-danger text-white;
} }
/* Table */ /* Table */
.table tr { .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 { .table-td {
@apply py-4 px-2 whitespace-nowrap; @apply py-4 px-2 whitespace-nowrap;
} }
.table-th {
@apply p-2;
}
.table-root {
@apply mt-4;
}
/* Snackbar */ /* Snackbar */
.notification-dark { .notification-dark {
@apply text-white; @apply text-white;
@ -210,14 +236,14 @@ button.menubar__button {
@apply flex items-center text-center justify-between; @apply flex items-center text-center justify-between;
} }
.pagination-link { .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 { .pagination-list {
@apply flex items-center text-center list-none flex-wrap grow shrink justify-start; @apply flex items-center text-center list-none flex-wrap grow shrink justify-start;
} }
.pagination-next, .pagination-next,
.pagination-previous { .pagination-previous {
@apply px-3; @apply px-3 dark:text-black;
} }
.pagination-link-current { .pagination-link-current {
@apply bg-primary cursor-not-allowed pointer-events-none border-primary text-white; @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 { .tabs-nav-item-active-boxed {
@apply bg-white border-gray-300 text-primary; @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)];
}

View File

@ -22,13 +22,9 @@
} }
} }
a:hover {
color: inherit;
}
@layer components { @layer components {
.mbz-card { .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;
} }
} }

View File

@ -25,7 +25,7 @@
> >
{{ displayName(actor) }} {{ displayName(actor) }}
</h5> </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> <span dir="ltr">@{{ usernameWithDomain(actor) }}</span>
</p> </p>
<div <div

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="activity-item"> <div class="activity-item">
<o-icon :icon="'chat'" :type="iconColor" /> <o-icon :icon="'chat'" :variant="iconColor" custom-size="24" />
<div class="subject"> <div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
<i18n-t :keypath="translation" tag="p"> <i18n-t :keypath="translation" tag="p">
<template #discussion> <template #discussion>
<router-link <router-link
@ -102,12 +102,12 @@ const iconColor = computed((): string | undefined => {
switch (props.activity.subject) { switch (props.activity.subject) {
case ActivityDiscussionSubject.DISCUSSION_CREATED: case ActivityDiscussionSubject.DISCUSSION_CREATED:
case ActivityDiscussionSubject.DISCUSSION_REPLIED: case ActivityDiscussionSubject.DISCUSSION_REPLIED:
return "is-success"; return "success";
case ActivityDiscussionSubject.DISCUSSION_RENAMED: case ActivityDiscussionSubject.DISCUSSION_RENAMED:
case ActivityDiscussionSubject.DISCUSSION_ARCHIVED: case ActivityDiscussionSubject.DISCUSSION_ARCHIVED:
return "is-grey"; return "grey";
case ActivityDiscussionSubject.DISCUSSION_DELETED: case ActivityDiscussionSubject.DISCUSSION_DELETED:
return "is-danger"; return "danger";
default: default:
return undefined; return undefined;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="activity-item"> <div class="activity-item">
<o-icon :icon="'calendar'" :type="iconColor" /> <o-icon :icon="'calendar'" :variant="iconColor" custom-size="24" />
<div class="subject"> <div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
<i18n-t :keypath="translation" tag="p"> <i18n-t :keypath="translation" tag="p">
<template #event> <template #event>
<router-link <router-link
@ -93,11 +93,11 @@ const iconColor = computed((): string | undefined => {
switch (props.activity.subject) { switch (props.activity.subject) {
case ActivityEventSubject.EVENT_CREATED: case ActivityEventSubject.EVENT_CREATED:
case ActivityEventCommentSubject.COMMENT_POSTED: case ActivityEventCommentSubject.COMMENT_POSTED:
return "is-success"; return "success";
case ActivityEventSubject.EVENT_UPDATED: case ActivityEventSubject.EVENT_UPDATED:
return "is-grey"; return "grey";
case ActivityEventSubject.EVENT_DELETED: case ActivityEventSubject.EVENT_DELETED:
return "is-danger"; return "danger";
default: default:
return undefined; return undefined;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="activity-item"> <div class="activity-item">
<o-icon :icon="'cog'" :type="iconColor" /> <o-icon :icon="'cog'" :variant="iconColor" custom-size="24" />
<div class="subject"> <div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
<i18n-t :keypath="translation" tag="p"> <i18n-t :keypath="translation" tag="p">
<template #group> <template #group>
<router-link <router-link
@ -28,13 +28,7 @@
></template ></template
></i18n-t ></i18n-t
> >
<i18n-t <i18n-t :keypath="detail" v-for="detail in details" :key="detail" tag="p">
:keypath="detail"
v-for="detail in details"
:key="detail"
tag="p"
class="has-text-grey-dark"
>
<template #profile> <template #profile>
<popover-actor-card :actor="activity.author" :inline="true"> <popover-actor-card :actor="activity.author" :inline="true">
<b> <b>
@ -63,9 +57,7 @@
}}</b> }}</b>
</template> </template>
</i18n-t> </i18n-t>
<small class="has-text-grey-dark activity-date">{{ <small>{{ formatTimeString(activity.insertedAt) }}</small>
formatTimeString(activity.insertedAt)
}}</small>
</div> </div>
</div> </div>
</template> </template>
@ -110,9 +102,9 @@ const translation = computed((): string | undefined => {
const iconColor = computed((): string | undefined => { const iconColor = computed((): string | undefined => {
switch (props.activity.subject) { switch (props.activity.subject) {
case ActivityGroupSubject.GROUP_CREATED: case ActivityGroupSubject.GROUP_CREATED:
return "is-success"; return "success";
case ActivityGroupSubject.GROUP_UPDATED: case ActivityGroupSubject.GROUP_UPDATED:
return "is-grey"; return "grey";
default: default:
return undefined; return undefined;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="activity-item"> <div class="activity-item">
<o-icon :icon="icon" :type="iconColor" /> <o-icon :icon="icon" :variant="iconColor" custom-size="24" />
<div class="subject"> <div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
<i18n-t :keypath="translation" tag="p"> <i18n-t :keypath="translation" tag="p">
<template #member> <template #member>
<popover-actor-card <popover-actor-card
@ -144,14 +144,14 @@ const iconColor = computed((): string | undefined => {
case ActivityMemberSubject.MEMBER_JOINED: case ActivityMemberSubject.MEMBER_JOINED:
case ActivityMemberSubject.MEMBER_APPROVED: case ActivityMemberSubject.MEMBER_APPROVED:
case ActivityMemberSubject.MEMBER_ACCEPTED_INVITATION: case ActivityMemberSubject.MEMBER_ACCEPTED_INVITATION:
return "is-success"; return "success";
case ActivityMemberSubject.MEMBER_REQUEST: case ActivityMemberSubject.MEMBER_REQUEST:
case ActivityMemberSubject.MEMBER_UPDATED: case ActivityMemberSubject.MEMBER_UPDATED:
return "is-grey"; return "grey";
case ActivityMemberSubject.MEMBER_REMOVED: case ActivityMemberSubject.MEMBER_REMOVED:
case ActivityMemberSubject.MEMBER_REJECTED_INVITATION: case ActivityMemberSubject.MEMBER_REJECTED_INVITATION:
case ActivityMemberSubject.MEMBER_QUIT: case ActivityMemberSubject.MEMBER_QUIT:
return "is-danger"; return "danger";
default: default:
return undefined; return undefined;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="activity-item"> <div class="activity-item">
<o-icon :icon="'bullhorn'" :type="iconColor" /> <o-icon :icon="'bullhorn'" :variant="iconColor" custom-size="24" />
<div class="subject"> <div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
<i18n-t :keypath="translation" tag="p"> <i18n-t :keypath="translation" tag="p">
<template #post> <template #post>
<router-link <router-link
@ -78,11 +78,11 @@ const translation = computed((): string | undefined => {
const iconColor = computed((): string | undefined => { const iconColor = computed((): string | undefined => {
switch (props.activity.subject) { switch (props.activity.subject) {
case ActivityPostSubject.POST_CREATED: case ActivityPostSubject.POST_CREATED:
return "is-success"; return "success";
case ActivityPostSubject.POST_UPDATED: case ActivityPostSubject.POST_UPDATED:
return "is-grey"; return "grey";
case ActivityPostSubject.POST_DELETED: case ActivityPostSubject.POST_DELETED:
return "is-danger"; return "danger";
default: default:
return undefined; return undefined;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="activity-item"> <div class="activity-item">
<o-icon :icon="'link'" :type="iconColor" /> <o-icon :icon="'link'" :variant="iconColor" custom-size="24" />
<div class="subject"> <div class="mt-1 ml-2 prose dark:prose-invert prose-p:m-0">
<i18n-t :keypath="translation" tag="p"> <i18n-t :keypath="translation" tag="p">
<template #resource> <template #resource>
<router-link v-if="activity.object" :to="path">{{ <router-link v-if="activity.object" :to="path">{{
@ -142,12 +142,12 @@ const translation = computed((): string | undefined => {
const iconColor = computed((): string | undefined => { const iconColor = computed((): string | undefined => {
switch (props.activity.subject) { switch (props.activity.subject) {
case ActivityResourceSubject.RESOURCE_CREATED: case ActivityResourceSubject.RESOURCE_CREATED:
return "is-success"; return "success";
case ActivityResourceSubject.RESOURCE_MOVED: case ActivityResourceSubject.RESOURCE_MOVED:
case ActivityResourceSubject.RESOURCE_UPDATED: case ActivityResourceSubject.RESOURCE_UPDATED:
return "is-grey"; return "grey";
case ActivityResourceSubject.RESOURCE_DELETED: case ActivityResourceSubject.RESOURCE_DELETED:
return "is-danger"; return "danger";
default: default:
return undefined; return undefined;
} }

View File

@ -1,6 +1,6 @@
.activity-item { .activity-item {
display: flex; display: flex;
span.icon { span.o-icon {
width: 2em; width: 2em;
height: 2em; height: 2em;
box-sizing: border-box; box-sizing: border-box;
@ -10,8 +10,4 @@
flex-shrink: 0; flex-shrink: 0;
} }
.subject {
padding: 0.25rem 0 0 0.5rem;
}
} }

View File

@ -3,7 +3,7 @@
<o-icon <o-icon
v-if="showIcon" v-if="showIcon"
:icon="poiInfos?.poiIcon.icon" :icon="poiInfos?.poiIcon.icon"
size="is-medium" size="medium"
class="icon" class="icon"
/> />
<p> <p>

View File

@ -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 CategoryPictureLicencingElement = { name: string; url: string };
export type CategoryPictureLicencing = { export type CategoryPictureLicencing = {
author: CategoryPictureLicencingElement; author: CategoryPictureLicencingElement;

View File

@ -85,7 +85,7 @@
</template> </template>
<script lang="ts" setup> <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 IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "@/types/enums"; import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model"; import { CommentModel, IComment } from "../../types/comment.model";
@ -122,7 +122,9 @@ const props = defineProps<{
newComment?: IComment; newComment?: IComment;
}>(); }>();
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue")); const Editor = defineAsyncComponent(
() => import("@/components/TextEditor.vue")
);
const newComment = ref<IComment>(props.newComment ?? new CommentModel()); const newComment = ref<IComment>(props.newComment ?? new CommentModel());
@ -284,7 +286,7 @@ const { mutate: deleteComment, onError: deleteCommentMutationError } =
replies: updatedReplies, replies: updatedReplies,
totalReplies: parentComment.totalReplies - 1, totalReplies: parentComment.totalReplies - 1,
}); });
console.log("updatedComments", updatedComments); console.debug("updatedComments", updatedComments);
} else { } else {
// we have deleted a thread itself // we have deleted a thread itself
updatedComments = updatedComments.map((reply) => { updatedComments = updatedComments.map((reply) => {

View File

@ -23,7 +23,7 @@
</Story> </Story>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { IActor } from "@/types/actor"; import { IPerson } from "@/types/actor";
import { IComment } from "@/types/comment.model"; import { IComment } from "@/types/comment.model";
import { import {
ActorType, ActorType,
@ -34,7 +34,7 @@ import {
} from "@/types/enums"; } from "@/types/enums";
import { IEvent } from "@/types/event.model"; import { IEvent } from "@/types/event.model";
import { reactive } from "vue"; import { reactive } from "vue";
import Comment from "./Comment.vue"; import Comment from "./EventComment.vue";
import FloatingVue from "floating-vue"; import FloatingVue from "floating-vue";
import "floating-vue/dist/style.css"; import "floating-vue/dist/style.css";
import { hstEvent } from "histoire/client"; 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", url: "https://social.tcit.fr/system/accounts/avatars/000/000/001/original/a28c50ce5f2b13fd.jpg",
}; };
const baseActor: IActor = { const baseActor: IPerson = {
name: "Thomas Citharel", name: "Thomas Citharel",
preferredUsername: "tcit", preferredUsername: "tcit",
avatar: baseActorAvatar, avatar: baseActorAvatar,
@ -67,8 +67,8 @@ const baseEvent: IEvent = {
uuid: "", uuid: "",
title: "A very interesting event", title: "A very interesting event",
description: "Things happen", description: "Things happen",
beginsOn: new Date(), beginsOn: new Date().toISOString(),
endsOn: new Date(), endsOn: new Date().toISOString(),
physicalAddress: { physicalAddress: {
description: "Somewhere", description: "Somewhere",
street: "", street: "",
@ -88,7 +88,7 @@ const baseEvent: IEvent = {
url: "", url: "",
local: true, local: true,
slug: "", slug: "",
publishAt: new Date(), publishAt: new Date().toISOString(),
status: EventStatus.CONFIRMED, status: EventStatus.CONFIRMED,
visibility: EventVisibility.PUBLIC, visibility: EventVisibility.PUBLIC,
joinOptions: EventJoinOptions.FREE, joinOptions: EventJoinOptions.FREE,
@ -151,7 +151,7 @@ const comment = reactive<IComment>({
text: "a reply!", text: "a reply!",
id: "90", id: "90",
actor: baseActor, actor: baseActor,
updatedAt: new Date(), updatedAt: new Date().toISOString(),
url: "http://somewhere.tld", url: "http://somewhere.tld",
replies: [], replies: [],
totalReplies: 0, totalReplies: 0,
@ -162,7 +162,7 @@ const comment = reactive<IComment>({
text: "a reply to another reply!", text: "a reply to another reply!",
id: "92", id: "92",
actor: baseActor, actor: baseActor,
updatedAt: new Date(), updatedAt: new Date().toISOString(),
url: "http://somewhere.tld", url: "http://somewhere.tld",
replies: [], replies: [],
totalReplies: 0, totalReplies: 0,
@ -171,7 +171,7 @@ const comment = reactive<IComment>({
}, },
], ],
isAnnouncement: false, isAnnouncement: false,
updatedAt: new Date(), updatedAt: new Date().toISOString(),
url: "http://somewhere.tld", url: "http://somewhere.tld",
}); });
</script> </script>

View File

@ -175,7 +175,7 @@
</li> </li>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import EditorComponent from "@/components/Editor.vue"; import EditorComponent from "@/components/TextEditor.vue";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { CommentModeration } from "@/types/enums"; import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model"; 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 Reply from "vue-material-design-icons/Reply.vue";
import type { Locale } from "date-fns"; import type { Locale } from "date-fns";
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue")); const Editor = defineAsyncComponent(
() => import("@/components/TextEditor.vue")
);
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -257,7 +259,7 @@ const replyToComment = (): void => {
newComment.value.inReplyToComment = props.comment; newComment.value.inReplyToComment = props.comment;
newComment.value.originComment = props.comment.originComment ?? props.comment; newComment.value.originComment = props.comment.originComment ?? props.comment;
newComment.value.actor = props.currentActor; newComment.value.actor = props.currentActor;
console.log(newComment.value); console.debug(newComment.value);
emit("create-comment", newComment.value); emit("create-comment", newComment.value);
newComment.value = new CommentModel(); newComment.value = new CommentModel();
replyTo.value = false; replyTo.value = false;

View File

@ -1,5 +1,5 @@
<template> <template>
<article class="flex gap-2"> <article class="flex gap-2 bg-white dark:bg-transparent">
<div class=""> <div class="">
<figure class="" v-if="comment.actor && comment.actor.avatar"> <figure class="" v-if="comment.actor && comment.actor.avatar">
<img <img
@ -32,7 +32,7 @@
comment.actor.id === currentActor?.id comment.actor.id === currentActor?.id
" "
> >
<o-dropdown aria-role="list"> <o-dropdown aria-role="list" position="bottom-left">
<template #trigger> <template #trigger>
<o-icon role="button" icon="dots-horizontal" /> <o-icon role="button" icon="dots-horizontal" />
</template> </template>
@ -133,7 +133,9 @@ import { formatDateTimeString } from "@/filters/datetime";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue"; import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import type { Locale } from "date-fns"; import type { Locale } from "date-fns";
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue")); const Editor = defineAsyncComponent(
() => import("@/components/TextEditor.vue")
);
const props = defineProps<{ const props = defineProps<{
modelValue: IComment; modelValue: IComment;

View File

@ -68,7 +68,7 @@
</p> </p>
<details> <details>
<summary class="is-size-5">{{ t("Technical details") }}</summary> <summary>{{ t("Technical details") }}</summary>
<p>{{ t("Error message") }}</p> <p>{{ t("Error message") }}</p>
<pre>{{ error }}</pre> <pre>{{ error }}</pre>
<p>{{ t("Error stacktrace") }}</p> <p>{{ t("Error stacktrace") }}</p>

View File

@ -14,6 +14,9 @@
<Variant title="cancelled"> <Variant title="cancelled">
<EventCard :event="cancelledEvent" /> <EventCard :event="cancelledEvent" />
</Variant> </Variant>
<Variant title="Row mode">
<EventCard :event="longEvent" mode="row" />
</Variant>
</Story> </Story>
</template> </template>
@ -53,8 +56,8 @@ const baseEvent: IEvent = {
uuid: "", uuid: "",
title: "A very interesting event", title: "A very interesting event",
description: "Things happen", description: "Things happen",
beginsOn: new Date(), beginsOn: new Date().toISOString(),
endsOn: new Date(), endsOn: new Date().toISOString(),
physicalAddress: { physicalAddress: {
description: "Somewhere", description: "Somewhere",
street: "", street: "",
@ -74,7 +77,7 @@ const baseEvent: IEvent = {
url: "", url: "",
local: true, local: true,
slug: "", slug: "",
publishAt: new Date(), publishAt: new Date().toISOString(),
status: EventStatus.CONFIRMED, status: EventStatus.CONFIRMED,
visibility: EventVisibility.PUBLIC, visibility: EventVisibility.PUBLIC,
joinOptions: EventJoinOptions.FREE, joinOptions: EventJoinOptions.FREE,
@ -130,7 +133,7 @@ const event = reactive<IEvent>(baseEvent);
const longEvent = reactive<IEvent>({ const longEvent = reactive<IEvent>({
...baseEvent, ...baseEvent,
title: 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>({ const tentativeEvent = reactive<IEvent>({

View File

@ -1,16 +1,25 @@
<template> <template>
<router-link <LinkOrRouterLink
class="mbz-card max-w-xs shrink-0 w-[18rem] snap-center dark:bg-mbz-purple" class="mbz-card snap-center dark:bg-mbz-purple"
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }" :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"> <figure class="block relative pt-40">
<lazy-image-wrapper <lazy-image-wrapper
:picture="event.picture" :picture="event.picture"
style="height: 100%; position: absolute; top: 0; left: 0; width: 100%" style="height: 100%; position: absolute; top: 0; left: 0; width: 100%"
/> />
<div <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" v-if="event.tags || event.status !== EventStatus.CONFIRMED"
> >
<mobilizon-tag <mobilizon-tag
@ -30,30 +39,39 @@
v-for="tag in (event.tags || []).slice(0, 3)" v-for="tag in (event.tags || []).slice(0, 3)"
:key="tag.slug" :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> </router-link>
</div> </div>
</figure> </figure>
</div> </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="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 <date-calendar-icon
:small="true" :small="true"
v-if="!mergedOptions.hideDate" v-if="!mergedOptions.hideDate"
:date="event.beginsOn.toString()" :date="event.beginsOn.toString()"
/> />
</div> </div>
<div class="w-full flex flex-col justify-between"> <span
<h3 class="text-gray-700 dark:text-white font-semibold hidden"
class="text-lg leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white" :class="{ 'sm:block': mode === 'row' }"
:title="event.title" >{{ 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" dir="auto"
:lang="event.language" :lang="event.language"
> >
{{ event.title }} {{ event.title }}
</h3> </h2>
<div class="pt-3"> <div class="">
<div <div
class="flex items-center text-violet-3 dark:text-white" class="flex items-center text-violet-3 dark:text-white"
dir="auto" dir="auto"
@ -68,7 +86,7 @@
/> />
</figure> </figure>
<account-circle v-else /> <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) }} {{ organizerDisplayName(event) }}
</span> </span>
</div> </div>
@ -84,11 +102,38 @@
<Video /> <Video />
<span class="ltr:pl-2 rtl:pr-2">{{ $t("Online") }}</span> <span class="ltr:pl-2 rtl:pr-2">{{ $t("Online") }}</span>
</div> </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>
</div> </div>
</div> </div>
</router-link> </LinkOrRouterLink>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -104,17 +149,29 @@ import { EventStatus } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import InlineAddress from "@/components/Address/InlineAddress.vue"; import InlineAddress from "@/components/Address/InlineAddress.vue";
import { computed } from "vue"; import { computed, inject } from "vue";
import MobilizonTag from "../Tag.vue"; import MobilizonTag from "@/components/Tag.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue"; import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import Video from "vue-material-design-icons/Video.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 = { const defaultOptions: IEventCardOptions = {
hideDate: false, hideDate: false,
loggedPerson: false, loggedPerson: false,
hideDetails: false, hideDetails: false,
organizerActor: null, organizerActor: null,
isRemoteEvent: false,
isLoggedIn: true,
}; };
const mergedOptions = computed<IEventCardOptions>(() => ({ const mergedOptions = computed<IEventCardOptions>(() => ({
@ -132,4 +189,31 @@ const mergedOptions = computed<IEventCardOptions>(() => ({
const actorAvatarURL = computed<string | null>(() => const actorAvatarURL = computed<string | null>(() =>
organizerAvatarUrl(props.event) 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> </script>

View File

@ -14,7 +14,7 @@
</p> </p>
<p v-else-if="isSameDay() && showStartTime && showEndTime"> <p v-else-if="isSameDay() && showStartTime && showEndTime">
<span>{{ <span>{{
$t("On {date} from {startTime} to {endTime}", { t("On {date} from {startTime} to {endTime}", {
date: formatDate(beginsOn), date: formatDate(beginsOn),
startTime: formatTime(beginsOn, timezoneToShow), startTime: formatTime(beginsOn, timezoneToShow),
endTime: formatTime(endsOn, timezoneToShow), endTime: formatTime(endsOn, timezoneToShow),
@ -31,27 +31,24 @@
</p> </p>
<p v-else-if="isSameDay() && showStartTime && !showEndTime"> <p v-else-if="isSameDay() && showStartTime && !showEndTime">
{{ {{
$t("On {date} starting at {startTime}", { t("On {date} starting at {startTime}", {
date: formatDate(beginsOn), date: formatDate(beginsOn),
startTime: formatTime(beginsOn), startTime: formatTime(beginsOn),
}) })
}} }}
</p> </p>
<p v-else-if="isSameDay()"> <p v-else-if="isSameDay()">
{{ $t("On {date}", { date: formatDate(beginsOn) }) }} {{ t("On {date}", { date: formatDate(beginsOn) }) }}
</p> </p>
<p v-else-if="endsOn && showStartTime && showEndTime"> <p v-else-if="endsOn && showStartTime && showEndTime">
<span> <span>
{{ {{
$t( t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
"From the {startDate} at {startTime} to the {endDate} at {endTime}", startDate: formatDate(beginsOn),
{ startTime: formatTime(beginsOn, timezoneToShow),
startDate: formatDate(beginsOn), endDate: formatDate(endsOn),
startTime: formatTime(beginsOn, timezoneToShow), endTime: formatTime(endsOn, timezoneToShow),
endDate: formatDate(endsOn), })
endTime: formatTime(endsOn, timezoneToShow),
}
)
}} }}
</span> </span>
<br /> <br />
@ -66,7 +63,7 @@
<p v-else-if="endsOn && showStartTime"> <p v-else-if="endsOn && showStartTime">
<span> <span>
{{ {{
$t("From the {startDate} at {startTime} to the {endDate}", { t("From the {startDate} at {startTime} to the {endDate}", {
startDate: formatDate(beginsOn), startDate: formatDate(beginsOn),
startTime: formatTime(beginsOn, timezoneToShow), startTime: formatTime(beginsOn, timezoneToShow),
endDate: formatDate(endsOn), endDate: formatDate(endsOn),
@ -169,22 +166,22 @@ const differentFromUserTimezone = computed((): boolean => {
const singleTimeZone = computed((): string => { const singleTimeZone = computed((): string => {
if (showLocalTimezone.value) { if (showLocalTimezone.value) {
return t("Local time ({timezone})", { return t("Local time ({timezone})", {
timezone: timezoneToShow, timezone: timezoneToShow.value,
}) as string; });
} }
return t("Time in your timezone ({timezone})", { return t("Time in your timezone ({timezone})", {
timezone: timezoneToShow, timezone: timezoneToShow.value,
}) as string; });
}); });
const multipleTimeZones = computed((): string => { const multipleTimeZones = computed((): string => {
if (showLocalTimezone.value) { if (showLocalTimezone.value) {
return t("Local time ({timezone})", { return t("Local times ({timezone})", {
timezone: timezoneToShow, timezone: timezoneToShow.value,
}) as string; });
} }
return t("Times in your timezone ({timezone})", { return t("Times in your timezone ({timezone})", {
timezone: timezoneToShow, timezone: timezoneToShow.value,
}) as string; });
}); });
</script> </script>

View File

@ -87,7 +87,7 @@ const RoutingParamType = {
}, },
}; };
const MapLeaflet = import("../../components/Map.vue"); const MapLeaflet = import("@/components/LeafletMap.vue");
const props = defineProps<{ const props = defineProps<{
address: IAddress; address: IAddress;

View File

@ -136,10 +136,10 @@ const metadata = computed({
}; };
}) as any[]; }) as any[];
}, },
set(metadata: IEventMetadataDescription[]) { set(newMetadata: IEventMetadataDescription[]) {
emit( emit(
"update:modelValue", "update:modelValue",
metadata.filter((elem) => elem) newMetadata.filter((elem) => elem)
); );
}, },
}); });

View File

@ -10,7 +10,7 @@
<div class="address" v-if="physicalAddress"> <div class="address" v-if="physicalAddress">
<address-info :address="physicalAddress" /> <address-info :address="physicalAddress" />
<o-button <o-button
type="is-text" variant="text"
class="map-show-button" class="map-show-button"
@click="$emit('showMapModal', true)" @click="$emit('showMapModal', true)"
v-if="physicalAddress.geom" v-if="physicalAddress.geom"

View File

@ -22,27 +22,23 @@
:lang="event.language" :lang="event.language"
dir="auto" dir="auto"
> >
<b-tag <tag
variant="info" variant="info"
class="mr-1" class="mr-1"
v-if="event.status === EventStatus.TENTATIVE" v-if="event.status === EventStatus.TENTATIVE"
> >
{{ $t("Tentative") }} {{ $t("Tentative") }}
</b-tag> </tag>
<b-tag <tag
variant="danger" variant="danger"
class="mr-1" class="mr-1"
v-if="event.status === EventStatus.CANCELLED" v-if="event.status === EventStatus.CANCELLED"
> >
{{ $t("Cancelled") }} {{ $t("Cancelled") }}
</b-tag> </tag>
<b-tag <tag class="mr-2" variant="warning" size="medium" v-if="event.draft">{{
class="mr-2" $t("Draft")
variant="warning" }}</tag>
size="is-medium"
v-if="event.draft"
>{{ $t("Draft") }}</b-tag
>
{{ event.title }} {{ event.title }}
</h3> </h3>
<inline-address <inline-address
@ -99,7 +95,7 @@
</span> </span>
<span v-if="event.participantStats.notApproved > 0"> <span v-if="event.participantStats.notApproved > 0">
<o-button <o-button
type="is-text" variant="text"
@click=" @click="
gotToWithCheck(participation, { gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS, name: RouteName.PARTICIPATIONS,
@ -134,6 +130,7 @@ import InlineAddress from "@/components/Address/InlineAddress.vue";
import Video from "vue-material-design-icons/Video.vue"; import Video from "vue-material-design-icons/Video.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue"; import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue"; import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
import Tag from "@/components/Tag.vue";
withDefaults( withDefaults(
defineProps<{ defineProps<{

View File

@ -1,12 +1,17 @@
<template> <template>
<article class="bg-white dark:bg-mbz-purple mb-5 mt-4 pb-2 md:p-0"> <article
<div class="bg-yellow-2 flex p-2 text-violet-title rounded-t-lg" dir="auto"> 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 <figure
class="image is-24x24 ltr:pr-1 rtl:pl-1" class="image is-24x24 ltr:pr-1 rtl:pl-1"
v-if="participation.actor.avatar" v-if="participation.actor.avatar"
> >
<img <img
class="is-rounded" class="rounded"
:src="participation.actor.avatar.url" :src="participation.actor.avatar.url"
alt="" alt=""
height="24" height="24"
@ -157,7 +162,7 @@
</span> </span>
<o-button <o-button
v-if="participation.event.participantStats.notApproved > 0" v-if="participation.event.participantStats.notApproved > 0"
type="is-text" variant="text"
@click=" @click="
gotToWithCheck(participation, { gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS, name: RouteName.PARTICIPATIONS,
@ -330,7 +335,7 @@ const defaultOptions: IEventCardOptions = {
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
participation: IParticipant; participation: IParticipant;
options: IEventCardOptions; options?: IEventCardOptions;
}>(), }>(),
{ {
options: () => ({ options: () => ({

View File

@ -7,14 +7,11 @@
:message="fieldErrors" :message="fieldErrors"
:type="{ 'is-danger': fieldErrors }" :type="{ 'is-danger': fieldErrors }"
class="!-mt-2" class="!-mt-2"
:labelClass="labelClass"
> >
<template #label> <template #label>
{{ actualLabel }} {{ actualLabel }}
<span <span v-if="gettingLocation">{{ t("Getting location") }}</span>
class="is-size-6 has-text-weight-normal"
v-if="gettingLocation"
>{{ t("Getting location") }}</span
>
</template> </template>
<p class="control" v-if="canShowLocateMeButton"> <p class="control" v-if="canShowLocateMeButton">
<o-loading <o-loading
@ -54,7 +51,7 @@
</template> </template>
<template #empty> <template #empty>
<span v-if="isFetching">{{ t("Searching") }}</span> <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>{{ <span>{{
t('No results for "{queryText}"', { queryText }) t('No results for "{queryText}"', { queryText })
}}</span> }}</span>
@ -121,12 +118,16 @@ import { useGeocodingAutocomplete } from "@/composition/apollo/config";
import { ADDRESS } from "@/graphql/address"; import { ADDRESS } from "@/graphql/address";
import { useReverseGeocode } from "@/composition/apollo/address"; import { useReverseGeocode } from "@/composition/apollo/address";
import { useLazyQuery } from "@vue/apollo-composable"; import { useLazyQuery } from "@vue/apollo-composable";
const MapLeaflet = defineAsyncComponent(() => import("../Map.vue")); const MapLeaflet = defineAsyncComponent(
() => import("@/components/LeafletMap.vue")
);
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue: IAddress | null; modelValue: IAddress | null;
defaultText?: string | null;
label?: string; label?: string;
labelClass?: string;
userTimezone?: string; userTimezone?: string;
disabled?: boolean; disabled?: boolean;
hideMap?: boolean; hideMap?: boolean;
@ -134,7 +135,8 @@ const props = withDefaults(
placeholder?: string; placeholder?: string;
}>(), }>(),
{ {
label: "", labelClass: "",
defaultText: "",
disabled: false, disabled: false,
hideMap: false, hideMap: false,
hideSelected: false, hideSelected: false,
@ -204,7 +206,7 @@ const checkCurrentPosition = (e: LatLng): boolean => {
const { t, locale } = useI18n({ useScope: "global" }); const { t, locale } = useI18n({ useScope: "global" });
const actualLabel = computed((): string => { 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 // eslint-disable-next-line class-methods-use-this
@ -253,11 +255,14 @@ const asyncData = async (query: string): Promise<void> => {
const queryText = computed({ const queryText = computed({
get() { get() {
return selected.value ? addressFullName(selected.value) : ""; return (
(selected.value ? addressFullName(selected.value) : props.defaultText) ??
""
);
}, },
set(text) { set(text) {
if (text === "" && selected.value?.id) { if (text === "" && selected.value?.id) {
console.log("doing reset"); console.debug("doing reset");
resetAddress(); resetAddress();
} }
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="events-wrapper"> <div class="events-wrapper">
<div class="flex flex-col gap-4" v-for="key of keys" :key="key"> <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]) }} {{ monthName(groupEvents(key)[0]) }}
</h2> </h2>
<event-minimalist-card <event-minimalist-card

View File

@ -27,10 +27,6 @@ const videoDetails = computed((): { host: string; uuid: string } | null => {
} }
return null; return null;
}); });
const origin = computed((): string => {
return window.location.hostname;
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.peertube { .peertube {

View File

@ -28,10 +28,6 @@ const videoID = computed((): string | null => {
} }
return null; return null;
}); });
const origin = computed((): string => {
return window.location.hostname;
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.youtube { .youtube {

View File

@ -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" 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}`" :for="`availableActor-${availableActor?.id}`"
> >
<figure class="" v-if="availableActor?.avatar"> <figure class="h-12 w-12" v-if="availableActor?.avatar">
<img <img
class="rounded" class="rounded-full h-full w-full object-cover"
:src="availableActor.avatar.url" :src="availableActor.avatar.url"
alt="" alt=""
width="48" width="48"

View File

@ -12,9 +12,9 @@
> >
<div class="flex gap-1 p-4"> <div class="flex gap-1 p-4">
<div class=""> <div class="">
<figure class="" v-if="selectedActor.avatar"> <figure class="h-12 w-12" v-if="selectedActor.avatar">
<img <img
class="rounded" class="rounded-full h-full w-full object-cover"
:src="selectedActor.avatar.url" :src="selectedActor.avatar.url"
:alt="selectedActor.avatar.alt ?? ''" :alt="selectedActor.avatar.alt ?? ''"
height="48" height="48"
@ -207,7 +207,7 @@ const props = withDefaults(
{ inline: true, contacts: () => [] } { inline: true, contacts: () => [] }
); );
const emit = defineEmits(["update:modelValue", "update:Contacts"]); const emit = defineEmits(["update:modelValue", "update:contacts"]);
const selectedActor = computed({ const selectedActor = computed({
get(): IActor | undefined { get(): IActor | undefined {
@ -252,7 +252,7 @@ const actualContacts = computed({
}, },
set(contactsIds: (string | undefined)[]) { set(contactsIds: (string | undefined)[]) {
emit( emit(
"update:Contacts", "update:contacts",
actorMembers.value.filter(({ id }) => contactsIds.includes(id)) actorMembers.value.filter(({ id }) => contactsIds.includes(id))
); );
}, },

View File

@ -68,7 +68,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { IActor, IPerson } from "@/types/actor"; import { IPerson } from "@/types/actor";
import { EventJoinOptions, ParticipantRole } from "@/types/enums"; import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import { IEvent } from "@/types/event.model"; import { IEvent } from "@/types/event.model";
import ParticipationButton from "./ParticipationButton.vue"; import ParticipationButton from "./ParticipationButton.vue";

View File

@ -37,7 +37,7 @@ import { useI18n } from "vue-i18n";
import { IEvent } from "@/types/event.model"; import { IEvent } from "@/types/event.model";
import ShareModal from "@/components/Share/ShareModal.vue"; import ShareModal from "@/components/Share/ShareModal.vue";
const props = withDefaults( withDefaults(
defineProps<{ defineProps<{
event: IEvent; event: IEvent;
eventCapacityOK?: boolean; eventCapacityOK?: boolean;

View File

@ -16,8 +16,8 @@ import TagInput from "./TagInput.vue";
const tags = reactive<ITag[]>([{ title: "Hello", slug: "hello" }]); const tags = reactive<ITag[]>([{ title: "Hello", slug: "hello" }]);
const fetchTags = async (text: string) => const fetchTags = async () =>
new Promise<ITag[]>((resolve, reject) => { new Promise<ITag[]>((resolve) => {
resolve([{ title: "Welcome", slug: "welcome" }]); resolve([{ title: "Welcome", slug: "welcome" }]);
}); });
</script> </script>

View File

@ -3,7 +3,7 @@
<template #label> <template #label>
{{ $t("Add some tags") }} {{ $t("Add some tags") }}
<o-tooltip <o-tooltip
type="dark" variant="dark"
:label=" :label="
$t('You can add tags by hitting the Enter key or by adding a comma') $t('You can add tags by hitting the Enter key or by adding a comma')
" "
@ -77,9 +77,9 @@ const tagsStrings = computed({
get(): string[] { get(): string[] {
return props.modelValue.map((tag: ITag) => tag.title); return props.modelValue.map((tag: ITag) => tag.title);
}, },
set(tagsStrings: string[]) { set(newTagsStrings: string[]) {
console.debug("tagsStrings", tagsStrings); console.debug("tagsStrings", newTagsStrings);
const tagEntities = tagsStrings.map((tag: string | ITag) => { const tagEntities = newTagsStrings.map((tag: string | ITag) => {
if (typeof tag !== "string") { if (typeof tag !== "string") {
return tag; return tag;
} }

View File

@ -15,14 +15,17 @@
<GroupCard :group="groupWithFollowersOrMembers" /> <GroupCard :group="groupWithFollowersOrMembers" />
</div> </div>
</Variant> </Variant>
<Variant title="Row mode">
<GroupCard :group="groupWithFollowersOrMembers" mode="row" />
</Variant>
</Story> </Story>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { IActor } from "@/types/actor"; import { IGroup } from "@/types/actor";
import GroupCard from "./GroupCard.vue"; import GroupCard from "./GroupCard.vue";
const basicGroup: IActor = { const basicGroup: IGroup = {
name: "Framasoft", name: "Framasoft",
preferredUsername: "framasoft", preferredUsername: "framasoft",
avatar: null, avatar: null,
@ -34,7 +37,7 @@ const basicGroup: IActor = {
followers: { total: 0, elements: [] }, followers: { total: 0, elements: [] },
}; };
const groupWithMedia = { const groupWithMedia: IGroup = {
...basicGroup, ...basicGroup,
banner: { banner: {
url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg", url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg",
@ -44,9 +47,14 @@ const groupWithMedia = {
}, },
}; };
const groupWithFollowersOrMembers = { const groupWithFollowersOrMembers: IGroup = {
...groupWithMedia, ...groupWithMedia,
members: { total: 2, elements: [] }, members: { total: 2, elements: [] },
followers: { total: 5, 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> </script>

View File

@ -1,29 +1,31 @@
<template> <template>
<router-link <LinkOrRouterLink
:to="{ :to="to"
name: RouteName.GROUP, :isInternal="isInternal"
params: { preferredUsername: usernameWithDomain(group) }, 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"> <div class="flex-none p-2 md:p-4">
<lazy-image-wrapper :picture="group.banner" :rounded="true" /> <figure class="" v-if="group.avatar">
</figure> <img
<div class="py-2 pl-2"> 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="flex gap-1 mb-2">
<div class=""> <div class="px-1 overflow-hidden flex-auto">
<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">
<h3 <h3
class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white" class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
dir="auto" dir="auto"
@ -46,7 +48,10 @@
v-if="group.physicalAddress && addressFullName(group.physicalAddress)" v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
:physicalAddress="group.physicalAddress" :physicalAddress="group.physicalAddress"
/> />
<p class="flex gap-1"> <p
class="flex gap-1"
v-if="group?.members?.total && group?.followers?.total"
>
<Account /> <Account />
{{ {{
t( t(
@ -58,14 +63,28 @@
) )
}} }}
</p> </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>
</div> </div>
</router-link> </LinkOrRouterLink>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { displayName, IGroup, usernameWithDomain } from "@/types/actor"; import { displayName, IGroup, usernameWithDomain } from "@/types/actor";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import InlineAddress from "@/components/Address/InlineAddress.vue"; import InlineAddress from "@/components/Address/InlineAddress.vue";
import { addressFullName } from "@/types/address.model"; 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 Account from "vue-material-design-icons/Account.vue";
import { htmlToText } from "@/utils/html"; import { htmlToText } from "@/utils/html";
import { computed } from "vue"; import { computed } from "vue";
import LinkOrRouterLink from "../core/LinkOrRouterLink.vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
group: IGroup; 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 { 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> </script>

View File

@ -73,19 +73,19 @@ const adminMember: IMember = {
role: MemberRole.ADMINISTRATOR, role: MemberRole.ADMINISTRATOR,
}; };
const groupWithMedia = { // const groupWithMedia = {
...basicGroup, // ...basicGroup,
banner: { // banner: {
url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg", // url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg",
}, // },
avatar: { // avatar: {
url: "https://mobilizon.fr/media/ff5b2d425fb73e17fcbb56a1a032359ee0b21453c11af59e103e783817a32fdf.png?name=framasoft%27s%20avatar.png", // url: "https://mobilizon.fr/media/ff5b2d425fb73e17fcbb56a1a032359ee0b21453c11af59e103e783817a32fdf.png?name=framasoft%27s%20avatar.png",
}, // },
}; // };
const groupWithFollowersOrMembers = { // const groupWithFollowersOrMembers = {
...groupWithMedia, // ...groupWithMedia,
members: { total: 2, elements: [] }, // members: { total: 2, elements: [] },
followers: { total: 5, elements: [] }, // followers: { total: 5, elements: [] },
}; // };
</script> </script>

View File

@ -41,19 +41,19 @@
}" }"
> >
<h2 class="mt-0">{{ member.parent.name }}</h2> <h2 class="mt-0">{{ member.parent.name }}</h2>
<div class="flex flex-col"> <div class="flex flex-col items-start">
<span class="text-sm">{{ <span class="text-sm">{{
`@${usernameWithDomain(member.parent)}` `@${usernameWithDomain(member.parent)}`
}}</span> }}</span>
<tag <tag
variant="info" variant="info"
v-if="member.role === MemberRole.ADMINISTRATOR" v-if="member.role === MemberRole.ADMINISTRATOR"
>{{ $t("Administrator") }}</tag >{{ t("Administrator") }}</tag
> >
<tag <tag
variant="info" variant="info"
v-else-if="member.role === MemberRole.MODERATOR" v-else-if="member.role === MemberRole.MODERATOR"
>{{ $t("Moderator") }}</tag >{{ t("Moderator") }}</tag
> >
</div> </div>
</router-link> </router-link>
@ -77,7 +77,7 @@
@click="emit('leave')" @click="emit('leave')"
> >
<ExitToApp /> <ExitToApp />
{{ $t("Leave") }} {{ t("Leave") }}
</o-dropdown-item> </o-dropdown-item>
</o-dropdown> </o-dropdown>
</div> </div>
@ -96,10 +96,13 @@ import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue"; import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import Tag from "@/components/Tag.vue"; import Tag from "@/components/Tag.vue";
import { htmlToText } from "@/utils/html"; import { htmlToText } from "@/utils/html";
import { useI18n } from "vue-i18n";
defineProps<{ defineProps<{
member: IMember; member: IMember;
}>(); }>();
const emit = defineEmits(["leave"]); const emit = defineEmits(["leave"]);
const { t } = useI18n({ useScope: "global" });
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="card"> <div class="">
<div class="card-content media"> <div class="">
<div class="media-content"> <div class="">
<div class="prose dark:prose-invert"> <div class="prose dark:prose-invert">
<i18n-t <i18n-t
tag="p" tag="p"
@ -12,12 +12,18 @@
</template> </template>
</i18n-t> </i18n-t>
</div> </div>
<div class="media subfield"> <div class="">
<div class="media-left"> <div class="">
<figure class="image is-48x48" v-if="member.parent.avatar"> <figure v-if="member.parent.avatar">
<img class="is-rounded" :src="member.parent.avatar.url" alt="" /> <img
class="rounded"
:src="member.parent.avatar.url"
alt=""
height="48"
width="48"
/>
</figure> </figure>
<o-icon v-else size="large" icon="account-group" /> <AccountGroup :size="48" v-else />
</div> </div>
<div class="media-content"> <div class="media-content">
<div class="level"> <div class="level">
@ -31,8 +37,8 @@
}, },
}" }"
> >
<h3 class="is-size-5">{{ member.parent.name }}</h3> <h3 class="">{{ member.parent.name }}</h3>
<p class="is-size-7 has-text-grey-dark"> <p class="">
<span v-if="member.parent.domain"> <span v-if="member.parent.domain">
{{ {{
`@${member.parent.preferredUsername}@${member.parent.domain}` `@${member.parent.preferredUsername}@${member.parent.domain}`
@ -45,8 +51,8 @@
</router-link> </router-link>
</div> </div>
</div> </div>
<div class="level-right"> <div class="">
<div class="level-item"> <div class="">
<o-button <o-button
variant="success" variant="success"
@click="$emit('accept', member.id)" @click="$emit('accept', member.id)"
@ -54,7 +60,7 @@
{{ $t("Accept") }} {{ $t("Accept") }}
</o-button> </o-button>
</div> </div>
<div class="level-item"> <div class="">
<o-button <o-button
variant="danger" variant="danger"
@click="$emit('reject', member.id)" @click="$emit('reject', member.id)"
@ -75,6 +81,7 @@
import { usernameWithDomain } from "@/types/actor"; import { usernameWithDomain } from "@/types/actor";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
defineProps<{ defineProps<{
member: IMember; member: IMember;

View 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>

View File

@ -28,8 +28,7 @@ import { CATEGORY_STATISTICS } from "@/graphql/statistics";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import shuffle from "lodash/shuffle"; import shuffle from "lodash/shuffle";
import { categoriesWithPictures } from "../Categories/constants"; import { categoriesWithPictures } from "../Categories/constants";
import { IConfig } from "@/types/config.model"; import { useEventCategories } from "@/composition/apollo/config";
import { CONFIG } from "@/graphql/config";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
@ -40,14 +39,10 @@ const categoryStats = computed(
() => categoryStatsResult.value?.categoryStatistics ?? [] () => categoryStatsResult.value?.categoryStatistics ?? []
); );
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG); const { eventCategories } = useEventCategories();
const config = computed(() => configResult.value?.config);
const eventCategories = computed(() => config.value?.eventCategories ?? []);
const eventCategoryLabel = (categoryId: string): string | undefined => { 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[] => { const promotedCategories = computed((): CategoryStatsModel[] => {

View File

@ -1,14 +1,17 @@
<template> <template>
<form <form
id="search-anchor" 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" 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 <o-input
class="flex-1"
v-model="search" v-model="search"
:placeholder="t('Keyword, event title, group name, etc.')" :placeholder="t('Keyword, event title, group name, etc.')"
id="search_field_input"
autofocus autofocus
autocapitalize="off" autocapitalize="off"
autocomplete="off" autocomplete="off"
@ -21,8 +24,10 @@
v-model="location" v-model="location"
:hide-map="true" :hide-map="true"
:hide-selected="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-if="search">{{ t("Go!") }}</template>
<template v-else>{{ t("Explore!") }}</template> <template v-else>{{ t("Explore!") }}</template>
</o-button> </o-button>
@ -35,23 +40,28 @@ import { AddressSearchType } from "@/types/enums";
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue"; import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
import { useRouter } from "vue-router";
import RouteName from "@/router/name";
const props = defineProps<{ const props = defineProps<{
location: IAddress; location: IAddress | null;
locationDefaultText?: string | null;
search: string; search: string;
}>(); }>();
const router = useRouter();
const emit = defineEmits<{ const emit = defineEmits<{
(event: "update:location", location: IAddress): void; (event: "update:location", location: IAddress | null): void;
(event: "update:search", newSearch: string): void; (event: "update:search", newSearch: string): void;
(event: "submit"): void; (event: "submit"): void;
}>(); }>();
const location = computed({ const location = computed({
get(): IAddress { get(): IAddress | null {
return props.location; return props.location;
}, },
set(newLocation: IAddress) { set(newLocation: IAddress | null) {
emit("update:location", newLocation); 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" }); const { t } = useI18n({ useScope: "global" });
</script> </script>
<style scoped> <style scoped>

View File

@ -26,7 +26,7 @@
>{{ t("Create an account") }}</o-button >{{ t("Create an account") }}</o-button
> >
<!-- We don't invite to find other instances yet --> <!-- 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 <router-link
:to="{ name: RouteName.ABOUT }" :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" 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 RouteName from "@/router/name";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
defineProps<{ config: IConfig }>(); defineProps<{
config: Pick<
IConfig,
"name" | "description" | "slogan" | "registrationsOpen"
>;
}>();
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
</script> </script>

View File

@ -28,7 +28,7 @@
</div> </div>
<div class="overflow-hidden"> <div class="overflow-hidden">
<div <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" ref="scrollContainer"
@scroll="scrollHandler" @scroll="scrollHandler"
> >

View File

@ -29,7 +29,7 @@
<more-content <more-content
v-if="userLocationName && userLocation?.lat && userLocation?.lon" v-if="userLocationName && userLocation?.lat && userLocation?.lon"
:to="{ :to="{
name: 'SEARCH', name: RouteName.SEARCH,
query: { query: {
locationName: userLocationName, locationName: userLocationName,
lat: userLocation.lat?.toString(), lat: userLocation.lat?.toString(),
@ -63,6 +63,8 @@ import { Paginate } from "@/types/paginate";
import SkeletonEventResult from "../Event/SkeletonEventResult.vue"; import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { coordsToGeoHash } from "@/utils/location"; import { coordsToGeoHash } from "@/utils/location";
import { roundToNearestMinute } from "@/utils/datetime";
import RouteName from "@/router/name";
const props = defineProps<{ userLocation: LocationType }>(); const props = defineProps<{ userLocation: LocationType }>();
const emit = defineEmits(["doGeoLoc"]); const emit = defineEmits(["doGeoLoc"]);
@ -77,17 +79,27 @@ const userLocationName = computed(() => {
}); });
const suggestGeoloc = computed(() => props.userLocation?.isIPLocation); const suggestGeoloc = computed(() => props.userLocation?.isIPLocation);
const geoHash = computed(() =>
coordsToGeoHash(props.userLocation.lat, props.userLocation.lon)
);
const { result: eventsResult, loading: loadingEvents } = useQuery<{ const { result: eventsResult, loading: loadingEvents } = useQuery<{
searchEvents: Paginate<IEvent>; searchEvents: Paginate<IEvent>;
}>(SEARCH_EVENTS, () => ({ }>(
location: coordsToGeoHash(props.userLocation.lat, props.userLocation.lon), SEARCH_EVENTS,
beginsOn: new Date(), () => ({
endsOn: undefined, location: geoHash.value,
radius: 25, beginsOn: roundToNearestMinute(new Date()),
eventPage: 1, endsOn: undefined,
limit: EVENT_PAGE_LIMIT, radius: 25,
type: "IN_PERSON", eventPage: 1,
})); limit: EVENT_PAGE_LIMIT,
type: "IN_PERSON",
}),
() => ({
enabled: geoHash.value !== undefined,
})
);
const events = computed( const events = computed(
() => eventsResult.value?.searchEvents ?? { elements: [], total: 0 } () => eventsResult.value?.searchEvents ?? { elements: [], total: 0 }

View File

@ -18,12 +18,12 @@
</template> </template>
</template> </template>
<template #content> <template #content>
<!-- <skeleton-group-result <skeleton-group-result
v-for="i in [...Array(6).keys()]" v-for="i in [...Array(6).keys()]"
class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4" class="scroll-ml-6 snap-center shrink-0 w-[18rem] my-4"
:key="i" :key="i"
v-show="loadingGroups" v-show="loadingGroups"
/> --> />
<group-card <group-card
v-for="group in selectedGroups" v-for="group in selectedGroups"
:key="group.id" :key="group.id"
@ -37,7 +37,7 @@
<more-content <more-content
v-if="userLocationName" v-if="userLocationName"
:to="{ :to="{
name: 'SEARCH', name: RouteName.SEARCH,
query: { query: {
locationName: userLocationName, locationName: userLocationName,
lat: userLocation.lat?.toString(), lat: userLocation.lat?.toString(),
@ -59,9 +59,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// import SkeletonGroupResult from "../../components/result/SkeletonGroupResult.vue"; import SkeletonGroupResult from "@/components/Group/SkeletonGroupResult.vue";
import sampleSize from "lodash/sampleSize"; 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 MoreContent from "./MoreContent.vue";
import CloseContent from "./CloseContent.vue"; import CloseContent from "./CloseContent.vue";
import { IGroup } from "@/types/actor"; import { IGroup } from "@/types/actor";
@ -72,6 +72,7 @@ import { computed } from "vue";
import GroupCard from "@/components/Group/GroupCard.vue"; import GroupCard from "@/components/Group/GroupCard.vue";
import { coordsToGeoHash } from "@/utils/location"; import { coordsToGeoHash } from "@/utils/location";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import RouteName from "@/router/name";
const props = defineProps<{ userLocation: LocationType }>(); const props = defineProps<{ userLocation: LocationType }>();
const emit = defineEmits(["doGeoLoc"]); const emit = defineEmits(["doGeoLoc"]);

View File

@ -33,7 +33,7 @@
/> />
<more-content <more-content
:to="{ :to="{
name: 'SEARCH', name: RouteName.SEARCH,
query: { query: {
contentType: 'EVENTS', contentType: 'EVENTS',
}, },
@ -57,6 +57,7 @@ import SkeletonEventResult from "../Event/SkeletonEventResult.vue";
import { EventSortField, SortDirection } from "@/types/enums"; import { EventSortField, SortDirection } from "@/types/enums";
import { FETCH_EVENTS } from "@/graphql/event"; import { FETCH_EVENTS } from "@/graphql/event";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import RouteName from "@/router/name";
defineProps<{ defineProps<{
instanceName: string; instanceName: string;

View File

@ -1,7 +1,8 @@
<template> <template>
<close-content <close-content
class="container mx-auto px-2"
:suggest-geoloc="false" :suggest-geoloc="false"
v-show="loadingEvents || events.length > 0" v-show="loadingEvents || (events?.elements && events?.elements.length > 0)"
> >
<template #title> <template #title>
{{ $t("Online upcoming events") }} {{ $t("Online upcoming events") }}
@ -15,7 +16,7 @@
/> />
<event-card <event-card
class="scroll-ml-6 snap-center shrink-0 first:pl-8 last:pr-8 w-[18rem]" 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" :key="event.id"
:event="event" :event="event"
view-mode="column" view-mode="column"
@ -24,7 +25,7 @@
/> />
<more-content <more-content
:to="{ :to="{
name: 'SEARCH', name: RouteName.SEARCH,
query: { query: {
contentType: 'EVENTS', contentType: 'EVENTS',
isOnline: 'true', isOnline: 'true',
@ -50,25 +51,27 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import SkeletonEventResult from "../result/SkeletonEventResult.vue"; import SkeletonEventResult from "@/components/Event/SkeletonEventResult.vue";
import MoreContent from "./MoreContent.vue"; import MoreContent from "./MoreContent.vue";
import CloseContent from "./CloseContent.vue"; import CloseContent from "./CloseContent.vue";
import { SEARCH_EVENTS } from "@/graphql/search"; 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 { 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 EVENT_PAGE_LIMIT = 12;
const { result: searchEventResult, loading: loadingEvents } = useQuery( const { result: searchEventResult, loading: loadingEvents } = useQuery<{
SEARCH_EVENTS, searchEvents: Paginate<IEvent>;
() => ({ }>(SEARCH_EVENTS, () => ({
beginsOn: new Date(), beginsOn: new Date(),
endsOn: undefined, endsOn: undefined,
eventPage: 1, eventPage: 1,
limit: EVENT_PAGE_LIMIT, limit: EVENT_PAGE_LIMIT,
type: "ONLINE", type: "ONLINE",
}) }));
);
const events = computed(() => searchEventResult.value.searchEvents); const events = computed(() => searchEventResult.value?.searchEvents);
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<svg <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 }" :class="{ 'bg-gray-900': invert }"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 248.16 46.78" viewBox="0 0 248.16 46.78"

View File

@ -1,11 +1,11 @@
<template> <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"> <div class="container mx-auto flex flex-wrap items-center mx-auto gap-4">
<router-link :to="{ name: RouteName.HOME }" class="flex items-center"> <router-link :to="{ name: RouteName.HOME }" class="flex items-center">
<MobilizonLogo class="w-40" /> <MobilizonLogo class="w-40" />
</router-link> </router-link>
<div class="flex items-center md:order-2 ml-auto" v-if="currentActor?.id"> <div class="flex items-center md:order-2 ml-auto" v-if="currentActor?.id">
<o-dropdown> <o-dropdown position="bottom-left">
<template #trigger> <template #trigger>
<button <button
type="button" type="button"
@ -14,33 +14,80 @@
aria-expanded="false" aria-expanded="false"
> >
<span class="sr-only">{{ t("Open user menu") }}</span> <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 <img
class="rounded-full" class="rounded-full w-full h-full object-cover"
alt="" alt=""
:src="currentActor?.avatar.url" :src="currentActor?.avatar.url"
width="32" width="32"
height="32" height="32"
/> />
</figure> </figure>
<AccountCircle :size="32" /> <AccountCircle v-else :size="32" />
</button> </button>
</template> </template>
<!-- Dropdown menu --> <!-- Dropdown menu -->
<div <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" position="bottom-left"
> >
<o-dropdown-item aria-role="listitem"> <o-dropdown-item aria-role="listitem">
<div class=""> <div class="px-4">
<span class="block text-sm text-gray-900 dark:text-white">{{ <span class="block text-sm text-zinc-900 dark:text-white">{{
displayName(currentActor) displayName(currentActor)
}}</span> }}</span>
<span <span
class="block text-sm font-medium text-gray-500 truncate dark:text-gray-400" class="block text-sm font-medium text-zinc-500 truncate dark:text-zinc-400"
>{{ currentUser?.role }}</span 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> </div>
</o-dropdown-item> </o-dropdown-item>
<o-dropdown-item <o-dropdown-item
@ -49,7 +96,7 @@
:to="{ name: RouteName.SETTINGS }" :to="{ name: RouteName.SETTINGS }"
> >
<span <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 >{{ t("My account") }}</span
> >
</o-dropdown-item> </o-dropdown-item>
@ -60,7 +107,7 @@
:to="{ name: RouteName.ADMIN_DASHBOARD }" :to="{ name: RouteName.ADMIN_DASHBOARD }"
> >
<span <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 >{{ t("Administration") }}</span
> >
</o-dropdown-item> </o-dropdown-item>
@ -70,7 +117,7 @@
@keyup.enter="logout" @keyup.enter="logout"
> >
<span <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 >{{ t("Log out") }}</span
> >
</o-dropdown-item> </o-dropdown-item>
@ -80,7 +127,7 @@
<button <button
@click="showMobileMenu = !showMobileMenu" @click="showMobileMenu = !showMobileMenu"
type="button" 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-controls="mobile-menu-2"
aria-expanded="false" aria-expanded="false"
> >
@ -105,33 +152,33 @@
:class="{ hidden: !showMobileMenu }" :class="{ hidden: !showMobileMenu }"
> >
<ul <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"> <li v-if="currentActor?.id">
<router-link <router-link
:to="{ name: RouteName.MY_EVENTS }" :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 >{{ t("My events") }}</router-link
> >
</li> </li>
<li v-if="currentActor?.id"> <li v-if="currentActor?.id">
<router-link <router-link
:to="{ name: RouteName.MY_GROUPS }" :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 >{{ t("My groups") }}</router-link
> >
</li> </li>
<li v-if="!currentActor?.id"> <li v-if="!currentActor?.id">
<router-link <router-link
:to="{ name: RouteName.LOGIN }" :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 >{{ t("Login") }}</router-link
> >
</li> </li>
<li v-if="!currentActor?.id"> <li v-if="!currentActor?.id">
<router-link <router-link
:to="{ name: RouteName.REGISTER }" :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 >{{ t("Register") }}</router-link
> >
</li> </li>
@ -327,6 +374,9 @@ import {
useCurrentActorClient, useCurrentActorClient,
useCurrentUserIdentities, useCurrentUserIdentities,
} from "@/composition/apollo/actor"; } 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"; // import { useRestrictions } from "@/composition/apollo/config";
const { currentUser } = useCurrentUserClient(); const { currentUser } = useCurrentUserClient();
@ -400,11 +450,17 @@ watch(identities, () => {
// await router.push({ name: RouteName.HOME }); // 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(() => { onDone(({ data }) => {
// changeIdentity(identity); const identity = identities.value?.find(
// }); ({ id }) => id === data?.changeDefaultActor?.defaultActor?.id
);
if (!identity) return;
changeIdentity(identity);
});
// const hideCreateEventsButton = computed((): boolean => { // const hideCreateEventsButton = computed((): boolean => {
// return !!restrictions.value?.onlyGroupsCanCreateEvents; // return !!restrictions.value?.onlyGroupsCanCreateEvents;

View File

@ -1,16 +1,16 @@
<template> <template>
<section class="container mx-auto"> <section class="container mx-auto">
<h1 class="title" v-if="loading"> <h1 class="title" v-if="loading">
{{ $t("Your participation request is being validated") }} {{ t("Your participation request is being validated") }}
</h1> </h1>
<div v-else> <div v-else>
<div v-if="failed && participation === undefined"> <div v-if="failed && participation === undefined">
<o-notification <o-notification
:title="$t('Error while validating participation request')" :title="t('Error while validating participation request')"
variant="danger" variant="danger"
> >
{{ {{
$t( t(
"Either the participation request has already been validated, either the validation token is incorrect." "Either the participation request has already been validated, either the validation token is incorrect."
) )
}} }}
@ -18,27 +18,25 @@
</div> </div>
<div v-else> <div v-else>
<h1 class="title"> <h1 class="title">
{{ $t("Your participation request has been validated") }} {{ t("Your participation request has been validated") }}
</h1> </h1>
<p <p
class="prose dark:prose-invert" class="prose dark:prose-invert"
v-if="participation?.event.joinOptions == EventJoinOptions.RESTRICTED" 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> </p>
<div v-if="failed"> <div v-if="failed">
<o-notification <o-notification
:title=" :title="
$t( t('Error while updating participation status inside this browser')
'Error while updating participation status inside this browser'
)
" "
variant="warning" 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." "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>
<div class="columns has-text-centered"> <div class="columns has-text-centered">
<div class="column"> <div class="column">
<router-link <o-button
native-type="button" tag="router-link"
tag="a" variant="primary"
class="button is-primary is-large" size="large"
:to="{ :to="{
name: RouteName.EVENT, name: RouteName.EVENT,
params: { uuid: participation?.event.uuid }, params: { uuid: participation?.event.uuid },
}" }"
>{{ $t("Go to the event page") }}</router-link >{{ t("Go to the event page") }}</o-button
> >
</div> </div>
</div> </div>

View File

@ -26,10 +26,7 @@
<template #popper> <template #popper>
{{ t("Click for more information") }} {{ t("Click for more information") }}
</template> </template>
<span <span @click="isAnonymousParticipationModalOpen = true">
class="is-clickable"
@click="isAnonymousParticipationModalOpen = true"
>
<InformationOutline :size="16" /> <InformationOutline :size="16" />
</span> </span>
</VTooltip> </VTooltip>
@ -102,7 +99,8 @@
</p> </p>
<div class="buttons" v-if="isSecureContext()"> <div class="buttons" v-if="isSecureContext()">
<o-button <o-button
type="is-danger is-outlined" variant="danger"
outlined
@click="clearEventParticipationData" @click="clearEventParticipationData"
> >
{{ t("Clear participation data for this event") }} {{ t("Clear participation data for this event") }}
@ -197,7 +195,7 @@ const isEventNotAlreadyPassed = computed((): boolean => {
return new Date(endDate.value) > new Date(); return new Date(endDate.value) > new Date();
}); });
const endDate = computed((): Date => { const endDate = computed((): string => {
return props.event.endsOn !== null && return props.event.endsOn !== null &&
props.event.endsOn > props.event.beginsOn props.event.endsOn > props.event.beginsOn
? props.event.endsOn ? props.event.endsOn

View File

@ -33,7 +33,7 @@
}} }}
</small> </small>
<o-tooltip <o-tooltip
type="is-dark" variant="dark"
:label=" :label="
$t( $t(
'Mobilizon is a federated network. You can interact with this event from a different server.' 'Mobilizon is a federated network. You can interact with this event from a different server.'
@ -90,7 +90,7 @@
</div> </div>
</div> </div>
<div class="has-text-centered"> <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") $t("Back to previous page")
}}</o-button> }}</o-button>
</div> </div>

View File

@ -41,7 +41,7 @@
</o-upload> </o-upload>
</o-field> </o-field>
<o-button <o-button
type="is-text" variant="text"
v-if="imageSrc" v-if="imageSrc"
@click="removeOrClearPicture" @click="removeOrClearPicture"
@keyup.enter="removeOrClearPicture" @keyup.enter="removeOrClearPicture"

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="" v-if="report"> <div class="dark:bg-zinc-700 p-2 rounded" v-if="report">
<div class="flex gap-1"> <div class="flex gap-1">
<figure class="" v-if="report.reported.avatar"> <figure class="" v-if="report.reported.avatar">
<img <img

View File

@ -9,7 +9,7 @@
}, },
}" }"
> >
<div class="preview"> <div class="preview text-mbz-purple dark:text-mbz-purple-300">
<Folder :size="48" /> <Folder :size="48" />
</div> </div>
<div class="body"> <div class="body">
@ -39,7 +39,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import Draggable, { ChangeEvent } from "@xiaoshuapp/draggable"; // import Draggable, { ChangeEvent } from "@xiaoshuapp/draggable";
// import { SnackbarProgrammatic as Snackbar } from "buefy"; // import { SnackbarProgrammatic as Snackbar } from "buefy";
import { IResource } from "@/types/resource"; import { IResource } from "@/types/resource";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
@ -110,8 +110,8 @@ onMovedResource(({ data }) => {
onMovedResourceError((e) => { onMovedResourceError((e) => {
// Snackbar.open({ // Snackbar.open({
// message: e.message, // message: e.message,
// type: "is-danger", // variant: "danger",
// position: "is-bottom", // position: "bottom",
// }); // });
return undefined; return undefined;
}); });

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-1 items-center w-full" dir="auto"> <div class="flex flex-1 items-center w-full" dir="auto">
<a :href="resource.resourceUrl" target="_blank"> <a :href="resource.resourceUrl" target="_blank">
<div class="preview"> <div class="preview text-mbz-purple dark:text-mbz-purple-300">
<div <div
v-if=" v-if="
resource.type && resource.type &&
@ -79,7 +79,7 @@ const emit = defineEmits<{
(e: "delete", resourceID: string): void; (e: "delete", resourceID: string): void;
}>(); }>();
const list = ref([]); // const list = ref([]);
const urlHostname = computed((): string | undefined => { const urlHostname = computed((): string | undefined => {
if (props.resource?.resourceUrl) { if (props.resource?.resourceUrl) {

View File

@ -69,7 +69,7 @@
/> />
</article> </article>
<div class="flex gap-2 mt-2"> <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") $t("Cancel")
}}</o-button> }}</o-button>
<o-button <o-button

View File

@ -56,8 +56,8 @@ const updateSetting = async (
} catch (e: any) { } catch (e: any) {
// Snackbar.open({ // Snackbar.open({
// message: e.message, // message: e.message,
// type: "is-danger", // variant: "danger",
// position: "is-bottom", // position: "bottom",
// }); // });
} }
}; };

View File

@ -1,5 +1,12 @@
<template> <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"> <router-link v-if="to" :to="to">
<span>{{ title }}</span> <span>{{ title }}</span>
</router-link> </router-link>
@ -31,7 +38,7 @@ const isActive = computed((): boolean => {
<style lang="scss" scoped> <style lang="scss" scoped>
li.setting-menu-item { li.setting-menu-item {
font-size: 1.05rem; font-size: 1.05rem;
background-color: #fff1de; // background-color: #fff1de;
margin: auto; margin: auto;
span { span {
@ -47,7 +54,7 @@ li.setting-menu-item {
&:hover, &:hover,
&.active { &.active {
cursor: pointer; cursor: pointer;
background-color: lighten(#fea72b, 10%); // background-color: lighten(#fea72b, 10%);
} }
} }
</style> </style>

View File

@ -1,5 +1,7 @@
<template> <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 <router-link
class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline" class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline"
v-if="to" v-if="to"

View File

@ -1,5 +1,5 @@
<template> <template>
<aside> <aside class="mb-6">
<ul> <ul>
<SettingMenuSection <SettingMenuSection
:title="t('Account')" :title="t('Account')"

View File

@ -1,5 +1,5 @@
<template> <template>
<span class="icon has-text-primary is-large"> <span class="text-black dark:text-white dark:fill-white">
<svg <svg
version="1.1" version="1.1"
viewBox="0 0 65.131 65.131" viewBox="0 0 65.131 65.131"
@ -12,7 +12,7 @@
/> />
<path <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" 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" stroke-width=".093311"
/> />
</svg> </svg>

View File

@ -1,5 +1,5 @@
<template> <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"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976">
<title>Mastodon logo</title> <title>Mastodon logo</title>
<path <path

View File

@ -5,13 +5,13 @@
</header> </header>
<section class="flex"> <section class="flex">
<div class=""> <div class="w-full">
<slot></slot> <slot></slot>
<o-field :label="inputLabel" label-for="url-text"> <o-field :label="inputLabel" label-for="url-text">
<o-input id="url-text" ref="URLInput" :modelValue="url" expanded /> <o-input id="url-text" ref="URLInput" :modelValue="url" expanded />
<p class="control"> <p class="control">
<o-tooltip <o-tooltip
:label="$t('URL copied to clipboard')" :label="t('URL copied to clipboard')"
:active="showCopiedTooltip" :active="showCopiedTooltip"
always always
variant="success" variant="success"
@ -23,7 +23,7 @@
native-type="button" native-type="button"
@click="copyURL" @click="copyURL"
@keyup.enter="copyURL" @keyup.enter="copyURL"
:title="$t('Copy URL to clipboard')" :title="t('Copy URL to clipboard')"
/> />
</o-tooltip> </o-tooltip>
</p> </p>
@ -34,7 +34,7 @@
target="_blank" target="_blank"
rel="nofollow noopener" rel="nofollow noopener"
title="Twitter" title="Twitter"
><Twitter :size="48" ><Twitter :size="48" class="dark:text-white"
/></a> /></a>
<a <a
:href="mastodonShare" :href="mastodonShare"
@ -50,14 +50,14 @@
target="_blank" target="_blank"
rel="nofollow noopener" rel="nofollow noopener"
title="Facebook" title="Facebook"
><Facebook :size="48" ><Facebook :size="48" class="dark:text-white"
/></a> /></a>
<a <a
:href="whatsAppShare" :href="whatsAppShare"
target="_blank" target="_blank"
rel="nofollow noopener" rel="nofollow noopener"
title="WhatsApp" title="WhatsApp"
><Whatsapp :size="48" ><Whatsapp :size="48" class="dark:text-white"
/></a> /></a>
<a <a
:href="telegramShare" :href="telegramShare"
@ -73,7 +73,7 @@
target="_blank" target="_blank"
rel="nofollow noopener" rel="nofollow noopener"
title="LinkedIn" title="LinkedIn"
><LinkedIn :size="48" ><LinkedIn :size="48" class="dark:text-white"
/></a> /></a>
<a <a
:href="diasporaShare" :href="diasporaShare"
@ -90,7 +90,7 @@
rel="nofollow noopener" rel="nofollow noopener"
title="Email" title="Email"
> >
<Email :size="48" /> <Email :size="48" class="dark:text-white" />
</a> </a>
</div> </div>
</div> </div>
@ -118,6 +118,7 @@ import {
twitterShareUrl, twitterShareUrl,
whatsAppShareUrl, whatsAppShareUrl,
} from "@/utils/share"; } from "@/utils/share";
import { useI18n } from "vue-i18n";
const props = withDefaults( const props = withDefaults(
defineProps<{ 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); const showCopiedTooltip = ref(false);
@ -159,7 +162,6 @@ const mastodonShare = computed((): string | undefined =>
); );
const copyURL = (): void => { const copyURL = (): void => {
console.log("URLInput", URLInput.value);
URLInput.value?.$refs.input.select(); URLInput.value?.$refs.input.select();
document.execCommand("copy"); document.execCommand("copy");
showCopiedTooltip.value = true; showCopiedTooltip.value = true;

View File

@ -1,5 +1,5 @@
<template> <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"> <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Telegram</title> <title>Telegram</title>
<path <path

View File

@ -1,7 +1,11 @@
<template> <template>
<span <span
class="rounded-md my-1 truncate text-sm text-violet-title px-2 py-1" 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 /> <slot />
</span> </span>
@ -11,10 +15,11 @@ import { computed } from "vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
variant?: "info" | "danger" | "warning" | "light"; variant?: "info" | "danger" | "warning" | "light" | "primary";
capitalize: boolean; capitalize?: boolean;
withHashTag?: boolean;
}>(), }>(),
{ variant: "light", capitalize: false } { variant: "light", capitalize: false, withHashTag: false }
); );
const typeClasses = computed(() => { const typeClasses = computed(() => {
@ -23,7 +28,7 @@ const typeClasses = computed(() => {
case "light": case "light":
return "bg-purple-3 dark:text-violet-3"; return "bg-purple-3 dark:text-violet-3";
case "info": case "info":
return "bg-mbz-info dark:text-white"; return "bg-mbz-info dark:text-black";
case "warning": case "warning":
return "bg-yellow-1"; return "bg-yellow-1";
case "danger": case "danger":
@ -33,9 +38,7 @@ const typeClasses = computed(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
span.tag { span.withHashTag::before {
&:not(.category)::before { content: "#";
content: "#";
}
} }
</style> </style>

View File

@ -7,7 +7,7 @@
:data-actor-id="currentActor && currentActor.id" :data-actor-id="currentActor && currentActor.id"
> >
<div <div
class="menubar bar-is-hidden" class="mb-2 menubar bar-is-hidden"
v-if="isDescriptionMode" v-if="isDescriptionMode"
:editor="editor" :editor="editor"
> >
@ -16,9 +16,9 @@
:class="{ 'is-active': editor.isActive('bold') }" :class="{ 'is-active': editor.isActive('bold') }"
@click="editor?.chain().focus().toggleBold().run()" @click="editor?.chain().focus().toggleBold().run()"
type="button" type="button"
:title="$t('Bold')" :title="t('Bold')"
> >
<o-icon icon="format-bold" /> <FormatBold :size="24" />
</button> </button>
<button <button
@ -26,9 +26,9 @@
:class="{ 'is-active': editor.isActive('italic') }" :class="{ 'is-active': editor.isActive('italic') }"
@click="editor?.chain().focus().toggleItalic().run()" @click="editor?.chain().focus().toggleItalic().run()"
type="button" type="button"
:title="$t('Italic')" :title="t('Italic')"
> >
<o-icon icon="format-italic" /> <FormatItalic :size="24" />
</button> </button>
<button <button
@ -36,9 +36,9 @@
:class="{ 'is-active': editor.isActive('underline') }" :class="{ 'is-active': editor.isActive('underline') }"
@click="editor?.chain().focus().toggleUnderline().run()" @click="editor?.chain().focus().toggleUnderline().run()"
type="button" type="button"
:title="$t('Underline')" :title="t('Underline')"
> >
<o-icon icon="format-underline" /> <FormatUnderline :size="24" />
</button> </button>
<button <button
@ -47,9 +47,9 @@
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor?.chain().focus().toggleHeading({ level: 1 }).run()" @click="editor?.chain().focus().toggleHeading({ level: 1 }).run()"
type="button" type="button"
:title="$t('Heading Level 1')" :title="t('Heading Level 1')"
> >
<o-icon icon="format-header-1" /> <FormatHeader1 :size="24" />
</button> </button>
<button <button
@ -58,9 +58,9 @@
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor?.chain().focus().toggleHeading({ level: 2 }).run()" @click="editor?.chain().focus().toggleHeading({ level: 2 }).run()"
type="button" type="button"
:title="$t('Heading Level 2')" :title="t('Heading Level 2')"
> >
<o-icon icon="format-header-2" /> <FormatHeader2 :size="24" />
</button> </button>
<button <button
@ -69,9 +69,9 @@
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor?.chain().focus().toggleHeading({ level: 3 }).run()" @click="editor?.chain().focus().toggleHeading({ level: 3 }).run()"
type="button" type="button"
:title="$t('Heading Level 3')" :title="t('Heading Level 3')"
> >
<o-icon icon="format-header-3" /> <FormatHeader3 :size="24" />
</button> </button>
<button <button
@ -79,9 +79,9 @@
@click="showLinkMenu()" @click="showLinkMenu()"
:class="{ 'is-active': editor.isActive('link') }" :class="{ 'is-active': editor.isActive('link') }"
type="button" type="button"
:title="$t('Add link')" :title="t('Add link')"
> >
<o-icon icon="link" /> <LinkIcon :size="24" />
</button> </button>
<button <button
@ -89,9 +89,9 @@
class="menubar__button" class="menubar__button"
@click="editor?.chain().focus().unsetLink().run()" @click="editor?.chain().focus().unsetLink().run()"
type="button" type="button"
:title="$t('Remove link')" :title="t('Remove link')"
> >
<o-icon icon="link-off" /> <LinkOff :size="24" />
</button> </button>
<button <button
@ -99,9 +99,9 @@
v-if="!isBasicMode" v-if="!isBasicMode"
@click="showImagePrompt()" @click="showImagePrompt()"
type="button" type="button"
:title="$t('Add picture')" :title="t('Add picture')"
> >
<o-icon icon="image" /> <Image :size="24" />
</button> </button>
<button <button
@ -110,9 +110,9 @@
:class="{ 'is-active': editor.isActive('bulletList') }" :class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor?.chain().focus().toggleBulletList().run()" @click="editor?.chain().focus().toggleBulletList().run()"
type="button" type="button"
:title="$t('Bullet list')" :title="t('Bullet list')"
> >
<o-icon icon="format-list-bulleted" /> <FormatListBulleted :size="24" />
</button> </button>
<button <button
@ -121,9 +121,9 @@
:class="{ 'is-active': editor.isActive('orderedList') }" :class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor?.chain().focus().toggleOrderedList().run()" @click="editor?.chain().focus().toggleOrderedList().run()"
type="button" type="button"
:title="$t('Ordered list')" :title="t('Ordered list')"
> >
<o-icon icon="format-list-numbered" /> <FormatListNumbered :size="24" />
</button> </button>
<button <button
@ -132,9 +132,9 @@
:class="{ 'is-active': editor.isActive('blockquote') }" :class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor?.chain().focus().toggleBlockquote().run()" @click="editor?.chain().focus().toggleBlockquote().run()"
type="button" type="button"
:title="$t('Quote')" :title="t('Quote')"
> >
<o-icon icon="format-quote-close" /> <FormatQuoteClose :size="24" />
</button> </button>
<button <button
@ -142,9 +142,9 @@
class="menubar__button" class="menubar__button"
@click="editor?.chain().focus().undo().run()" @click="editor?.chain().focus().undo().run()"
type="button" type="button"
:title="$t('Undo')" :title="t('Undo')"
> >
<o-icon icon="undo" /> <Undo :size="24" />
</button> </button>
<button <button
@ -152,9 +152,9 @@
class="menubar__button" class="menubar__button"
@click="editor?.chain().focus().redo().run()" @click="editor?.chain().focus().redo().run()"
type="button" type="button"
:title="$t('Redo')" :title="t('Redo')"
> >
<o-icon icon="redo" /> <Redo :size="24" />
</button> </button>
</div> </div>
@ -169,10 +169,10 @@
:class="{ 'is-active': editor.isActive('bold') }" :class="{ 'is-active': editor.isActive('bold') }"
@click="editor?.chain().focus().toggleBold().run()" @click="editor?.chain().focus().toggleBold().run()"
type="button" type="button"
:title="$t('Bold')" :title="t('Bold')"
> >
<o-icon icon="format-bold" /> <FormatBold :size="24" />
<span class="visually-hidden">{{ $t("Bold") }}</span> <span class="visually-hidden">{{ t("Bold") }}</span>
</button> </button>
<button <button
@ -180,10 +180,10 @@
:class="{ 'is-active': editor.isActive('italic') }" :class="{ 'is-active': editor.isActive('italic') }"
@click="editor?.chain().focus().toggleItalic().run()" @click="editor?.chain().focus().toggleItalic().run()"
type="button" type="button"
:title="$t('Italic')" :title="t('Italic')"
> >
<o-icon icon="format-italic" /> <FormatItalic :size="24" />
<span class="visually-hidden">{{ $t("Italic") }}</span> <span class="visually-hidden">{{ t("Italic") }}</span>
</button> </button>
</bubble-menu> </bubble-menu>
@ -223,6 +223,20 @@ import { Dialog } from "@/plugins/dialog";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from "@vue/apollo-composable";
import { Notifier } from "@/plugins/notifier"; 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( const props = withDefaults(
defineProps<{ defineProps<{
@ -259,7 +273,7 @@ const isBasicMode = computed((): boolean => {
}); });
const insertMention = (obj: { range: any; attrs: any }) => { const insertMention = (obj: { range: any; attrs: any }) => {
console.log("initialize Mention"); console.debug("initialize Mention");
}; };
const observer = ref<MutationObserver | null>(null); const observer = ref<MutationObserver | null>(null);
@ -421,7 +435,6 @@ onBeforeUnmount(() => {
@import "./Editor/style.scss"; @import "./Editor/style.scss";
.menubar { .menubar {
margin-bottom: 1rem;
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s; transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
&__button { &__button {

View File

@ -55,8 +55,8 @@ onDone(() => {
onError((e) => { onError((e) => {
// Snackbar.open({ // Snackbar.open({
// message: e.message, // message: e.message,
// type: "is-danger", // variant: "danger",
// position: "is-bottom", // position: "bottom",
// }); // });
}); });
</script> </script>

View File

@ -85,7 +85,7 @@ updateTodoError((e) => {
snackbar?.open({ snackbar?.open({
message: e.message, message: e.message,
variant: "danger", variant: "danger",
position: "is-bottom", position: "bottom",
}); });
}); });

View File

@ -1,5 +1,5 @@
<template> <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"> <ol class="inline-flex items-center space-x-1 md:space-x-3 flex-wrap">
<li <li
class="inline-flex items-center" class="inline-flex items-center"
@ -57,6 +57,7 @@
</nav> </nav>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { RouteLocationRaw } from "vue-router"; import { RouteLocationRaw } from "vue-router";
type LinkElement = RouteLocationRaw & { text: string }; type LinkElement = RouteLocationRaw & { text: string };
@ -64,4 +65,6 @@ type LinkElement = RouteLocationRaw & { text: string };
defineProps<{ defineProps<{
links: LinkElement[]; links: LinkElement[];
}>(); }>();
const { t } = useI18n({ useScope: "global" });
</script> </script>

View File

@ -6,7 +6,7 @@
<div class="column has-text-centered"> <div class="column has-text-centered">
<o-button <o-button
variant="primary" variant="primary"
size="is-medium" size="medium"
tag="router-link" tag="router-link"
:to="{ :to="{
name: RouteName.LOGIN, name: RouteName.LOGIN,
@ -46,7 +46,7 @@
</div> </div>
</div> </div>
<div class="has-text-centered"> <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") $t("Back to previous page")
}}</o-button> }}</o-button>
</div> </div>
@ -74,9 +74,9 @@ const host = computed((): string => {
}); });
const redirectToInstance = async (): Promise<void> => { const redirectToInstance = async (): Promise<void> => {
const [, host] = remoteActorAddress.value.split("@", 2); const [, hostname] = remoteActorAddress.value.split("@", 2);
const remoteInteractionURI = await webFingerFetch( const remoteInteractionURI = await webFingerFetch(
host, hostname,
remoteActorAddress.value remoteActorAddress.value
); );
window.open(remoteInteractionURI); window.open(remoteInteractionURI);

View File

@ -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>

View File

@ -63,8 +63,8 @@ const props = withDefaults(
canCancel?: boolean; canCancel?: boolean;
confirmText?: string; confirmText?: string;
cancelText?: string; cancelText?: string;
onConfirm: (prompt?: string) => {}; onConfirm: (prompt?: string) => any;
onCancel?: (source: string) => {}; onCancel?: (source: string) => any;
ariaLabel?: string; ariaLabel?: string;
ariaModal?: boolean; ariaModal?: boolean;
ariaRole?: string; ariaRole?: string;

View File

@ -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>

View File

@ -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>

View 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