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",
access_key: nil
config :mobilizon, :search, global: [is_default_search: false, is_enabled: true]
config :mobilizon, Mobilizon.Service.GlobalSearch,
service: Mobilizon.Service.GlobalSearch.SearchMobilizon
config :mobilizon, Mobilizon.Service.GlobalSearch.SearchMobilizon,
endpoint: "https://search.joinmobilizon.org"
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"

View File

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

3
js/.gitignore vendored
View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,13 +22,9 @@
}
}
a:hover {
color: inherit;
}
@layer components {
.mbz-card {
@apply block bg-mbz-yellow hover:bg-mbz-yellow/90 text-violet-title dark:text-white dark:hover:text-white/90 rounded-lg dark:border-violet-title shadow-md dark:bg-gray-700 dark:hover:bg-gray-700/90 dark:text-white dark:hover:text-white;
@apply block bg-mbz-yellow-alt-300 hover:bg-mbz-yellow-alt-200 text-violet-title dark:text-white dark:hover:text-white rounded-lg dark:border-violet-title shadow-md dark:bg-mbz-purple dark:hover:dark:bg-mbz-purple-400 dark:text-white dark:hover:text-white;
}
}

View File

@ -25,7 +25,7 @@
>
{{ displayName(actor) }}
</h5>
<p class="text-gray-500 truncate" v-if="actor.name">
<p class="text-gray-500 dark:text-gray-200 truncate" v-if="actor.name">
<span dir="ltr">@{{ usernameWithDomain(actor) }}</span>
</p>
<div

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
<o-icon
v-if="showIcon"
:icon="poiInfos?.poiIcon.icon"
size="is-medium"
size="medium"
class="icon"
/>
<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 CategoryPictureLicencing = {
author: CategoryPictureLicencingElement;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,9 @@
<Variant title="cancelled">
<EventCard :event="cancelledEvent" />
</Variant>
<Variant title="Row mode">
<EventCard :event="longEvent" mode="row" />
</Variant>
</Story>
</template>
@ -53,8 +56,8 @@ const baseEvent: IEvent = {
uuid: "",
title: "A very interesting event",
description: "Things happen",
beginsOn: new Date(),
endsOn: new Date(),
beginsOn: new Date().toISOString(),
endsOn: new Date().toISOString(),
physicalAddress: {
description: "Somewhere",
street: "",
@ -74,7 +77,7 @@ const baseEvent: IEvent = {
url: "",
local: true,
slug: "",
publishAt: new Date(),
publishAt: new Date().toISOString(),
status: EventStatus.CONFIRMED,
visibility: EventVisibility.PUBLIC,
joinOptions: EventJoinOptions.FREE,
@ -130,7 +133,7 @@ const event = reactive<IEvent>(baseEvent);
const longEvent = reactive<IEvent>({
...baseEvent,
title:
"A very long title that will have trouble to display because it will take multiple lines but where will it stop ?! Maybe after 3 lines is enough. Let's say so.",
"A very long title that will have trouble to display because it will take multiple lines but where will it stop ?! Maybe after 3 lines is enough. Let's say so. But if it doesn't work, we really need to truncate it at some point. Definitively.",
});
const tentativeEvent = reactive<IEvent>({

View File

@ -1,16 +1,25 @@
<template>
<router-link
class="mbz-card max-w-xs shrink-0 w-[18rem] snap-center dark:bg-mbz-purple"
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
<LinkOrRouterLink
class="mbz-card snap-center dark:bg-mbz-purple"
:class="{
'sm:flex sm:items-start': mode === 'row',
'max-w-xs w-[18rem] shrink-0 flex flex-col': mode === 'column',
}"
:to="to"
:isInternal="isInternal"
>
<div class="bg-secondary rounded-lg">
<div
class="bg-secondary rounded-lg"
:class="{ 'sm:w-full sm:max-w-[20rem]': mode === 'row' }"
>
<figure class="block relative pt-40">
<lazy-image-wrapper
:picture="event.picture"
style="height: 100%; position: absolute; top: 0; left: 0; width: 100%"
/>
<div
class="absolute top-3 right-0 ltr:-mr-1 rtl:-ml-1 z-10 max-w-xs no-underline flex flex-col gap-1"
class="absolute top-3 right-0 ltr:-mr-1 rtl:-ml-1 z-10 max-w-xs no-underline flex flex-col gap-1 items-end"
v-show="mode === 'column'"
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
>
<mobilizon-tag
@ -30,30 +39,39 @@
v-for="tag in (event.tags || []).slice(0, 3)"
:key="tag.slug"
>
<mobilizon-tag dir="auto">{{ tag.title }}</mobilizon-tag>
<mobilizon-tag dir="auto" :with-hash-tag="true">{{
tag.title
}}</mobilizon-tag>
</router-link>
</div>
</figure>
</div>
<div class="p-2">
<div class="p-2 flex-auto" :class="{ 'sm:flex-1': mode === 'row' }">
<div class="relative flex flex-col h-full">
<div class="-mt-3 h-0 flex mb-3 ltr:ml-0 rtl:mr-0 items-end self-start">
<div
class="-mt-3 h-0 flex mb-3 ltr:ml-0 rtl:mr-0 items-end self-start"
:class="{ 'sm:hidden': mode === 'row' }"
>
<date-calendar-icon
:small="true"
v-if="!mergedOptions.hideDate"
:date="event.beginsOn.toString()"
/>
</div>
<div class="w-full flex flex-col justify-between">
<h3
class="text-lg leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
:title="event.title"
<span
class="text-gray-700 dark:text-white font-semibold hidden"
:class="{ 'sm:block': mode === 'row' }"
>{{ formatDateTimeWithCurrentLocale }}</span
>
<div class="w-full flex flex-col justify-between h-full">
<h2
class="mt-0 mb-2 text-2xl line-clamp-3 font-bold text-violet-3 dark:text-white"
dir="auto"
:lang="event.language"
>
{{ event.title }}
</h3>
<div class="pt-3">
</h2>
<div class="">
<div
class="flex items-center text-violet-3 dark:text-white"
dir="auto"
@ -68,7 +86,7 @@
/>
</figure>
<account-circle v-else />
<span class="text-sm font-semibold ltr:pl-2 rtl:pr-2">
<span class="font-semibold ltr:pl-2 rtl:pr-2">
{{ organizerDisplayName(event) }}
</span>
</div>
@ -84,11 +102,38 @@
<Video />
<span class="ltr:pl-2 rtl:pr-2">{{ $t("Online") }}</span>
</div>
<div
class="mt-1 no-underline gap-1 items-center hidden"
:class="{ 'sm:flex': mode === 'row' }"
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
>
<mobilizon-tag
variant="info"
v-if="event.status === EventStatus.TENTATIVE"
>
{{ $t("Tentative") }}
</mobilizon-tag>
<mobilizon-tag
variant="danger"
v-if="event.status === EventStatus.CANCELLED"
>
{{ $t("Cancelled") }}
</mobilizon-tag>
<router-link
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
v-for="tag in (event.tags || []).slice(0, 3)"
:key="tag.slug"
>
<mobilizon-tag :with-hash-tag="true" dir="auto">{{
tag.title
}}</mobilizon-tag>
</router-link>
</div>
</div>
</div>
</div>
</div>
</router-link>
</LinkOrRouterLink>
</template>
<script lang="ts" setup>
@ -104,17 +149,29 @@ import { EventStatus } from "@/types/enums";
import RouteName from "../../router/name";
import InlineAddress from "@/components/Address/InlineAddress.vue";
import { computed } from "vue";
import MobilizonTag from "../Tag.vue";
import { computed, inject } from "vue";
import MobilizonTag from "@/components/Tag.vue";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import Video from "vue-material-design-icons/Video.vue";
import { formatDateTimeForEvent } from "@/utils/datetime";
import type { Locale } from "date-fns";
import LinkOrRouterLink from "../core/LinkOrRouterLink.vue";
const props = defineProps<{ event: IEvent; options?: IEventCardOptions }>();
const props = withDefaults(
defineProps<{
event: IEvent;
options?: IEventCardOptions;
mode?: "row" | "column";
}>(),
{ mode: "column" }
);
const defaultOptions: IEventCardOptions = {
hideDate: false,
loggedPerson: false,
hideDetails: false,
organizerActor: null,
isRemoteEvent: false,
isLoggedIn: true,
};
const mergedOptions = computed<IEventCardOptions>(() => ({
@ -132,4 +189,31 @@ const mergedOptions = computed<IEventCardOptions>(() => ({
const actorAvatarURL = computed<string | null>(() =>
organizerAvatarUrl(props.event)
);
const dateFnsLocale = inject<Locale>("dateFnsLocale");
const formatDateTimeWithCurrentLocale = computed(() => {
if (!dateFnsLocale) return;
return formatDateTimeForEvent(new Date(props.event.beginsOn), dateFnsLocale);
});
const isInternal = computed(() => {
return (
mergedOptions.value.isRemoteEvent &&
mergedOptions.value.isLoggedIn === false
);
});
const to = computed(() => {
if (mergedOptions.value.isRemoteEvent) {
if (mergedOptions.value.isLoggedIn === false) {
return props.event.url;
}
return {
name: RouteName.INTERACT,
query: { uri: encodeURI(props.event.url) },
};
}
return { name: RouteName.EVENT, params: { uuid: props.event.uuid } };
});
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,14 +15,17 @@
<GroupCard :group="groupWithFollowersOrMembers" />
</div>
</Variant>
<Variant title="Row mode">
<GroupCard :group="groupWithFollowersOrMembers" mode="row" />
</Variant>
</Story>
</template>
<script lang="ts" setup>
import { IActor } from "@/types/actor";
import { IGroup } from "@/types/actor";
import GroupCard from "./GroupCard.vue";
const basicGroup: IActor = {
const basicGroup: IGroup = {
name: "Framasoft",
preferredUsername: "framasoft",
avatar: null,
@ -34,7 +37,7 @@ const basicGroup: IActor = {
followers: { total: 0, elements: [] },
};
const groupWithMedia = {
const groupWithMedia: IGroup = {
...basicGroup,
banner: {
url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg",
@ -44,9 +47,14 @@ const groupWithMedia = {
},
};
const groupWithFollowersOrMembers = {
const groupWithFollowersOrMembers: IGroup = {
...groupWithMedia,
members: { total: 2, elements: [] },
followers: { total: 5, elements: [] },
summary:
"You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:h-full to apply the h-full utility at only medium screen sizes and above.",
physicalAddress: {
description: "Nantes",
},
};
</script>

View File

@ -1,29 +1,31 @@
<template>
<router-link
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(group) },
<LinkOrRouterLink
:to="to"
:isInternal="isInternal"
class="mbz-card shrink-0 dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg my-4 flex items-center flex-col"
:class="{
'sm:flex-row': mode === 'row',
'max-w-xs w-[18rem] shrink-0 flex flex-col': mode === 'column',
}"
class="card flex flex-col shrink-0 w-[18rem] bg-white dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg"
>
<figure class="rounded-t-lg flex justify-center h-40">
<lazy-image-wrapper :picture="group.banner" :rounded="true" />
</figure>
<div class="py-2 pl-2">
<div class="flex-none p-2 md:p-4">
<figure class="" v-if="group.avatar">
<img
class="rounded-full"
:src="group.avatar.url"
alt=""
height="128"
width="128"
/>
</figure>
<AccountGroup v-else :size="128" />
</div>
<div
class="py-2 px-2 md:px-4 flex flex-col h-full justify-between w-full"
:class="{ 'sm:flex-1': mode === 'row' }"
>
<div class="flex gap-1 mb-2">
<div class="">
<figure class="" v-if="group.avatar">
<img
class="rounded-xl"
:src="group.avatar.url"
alt=""
height="64"
width="64"
/>
</figure>
<AccountGroup v-else :size="64" />
</div>
<div class="px-1 overflow-hidden">
<div class="px-1 overflow-hidden flex-auto">
<h3
class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
dir="auto"
@ -46,7 +48,10 @@
v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
:physicalAddress="group.physicalAddress"
/>
<p class="flex gap-1">
<p
class="flex gap-1"
v-if="group?.members?.total && group?.followers?.total"
>
<Account />
{{
t(
@ -58,14 +63,28 @@
)
}}
</p>
<p
class="flex gap-1"
v-else-if="group?.membersCount || group?.followersCount"
>
<Account />
{{
t(
"{count} members or followers",
{
count: (group.membersCount ?? 0) + (group.followersCount ?? 0),
},
(group.membersCount ?? 0) + (group.followersCount ?? 0)
)
}}
</p>
</div>
</div>
</router-link>
</LinkOrRouterLink>
</template>
<script lang="ts" setup>
import { displayName, IGroup, usernameWithDomain } from "@/types/actor";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
import RouteName from "../../router/name";
import InlineAddress from "@/components/Address/InlineAddress.vue";
import { addressFullName } from "@/types/address.model";
@ -74,16 +93,40 @@ import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
import Account from "vue-material-design-icons/Account.vue";
import { htmlToText } from "@/utils/html";
import { computed } from "vue";
import LinkOrRouterLink from "../core/LinkOrRouterLink.vue";
const props = withDefaults(
defineProps<{
group: IGroup;
showSummary: boolean;
showSummary?: boolean;
isRemoteGroup?: boolean;
isLoggedIn?: boolean;
mode?: "row" | "column";
}>(),
{ showSummary: true }
{ showSummary: true, isRemoteGroup: false, isLoggedIn: true, mode: "column" }
);
const { t } = useI18n({ useScope: "global" });
const saneSummary = computed(() => htmlToText(props.group.summary));
const saneSummary = computed(() => htmlToText(props.group.summary ?? ""));
const isInternal = computed(() => {
return props.isRemoteGroup && props.isLoggedIn === false;
});
const to = computed(() => {
if (props.isRemoteGroup) {
if (props.isLoggedIn === false) {
return props.group.url;
}
return {
name: RouteName.INTERACT,
query: { uri: encodeURI(props.group.url) },
};
}
return {
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(props.group) },
};
});
</script>

View File

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

View File

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

View File

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

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

View File

@ -1,14 +1,17 @@
<template>
<form
id="search-anchor"
class="container mx-auto my-3 px-2 flex flex-wrap flex-col sm:flex-row items-stretch gap-2 text-center items-center justify-center dark:text-slate-100"
class="container mx-auto my-3 flex flex-wrap flex-col sm:flex-row items-stretch gap-2 text-center items-center justify-center dark:text-slate-100"
role="search"
@submit.prevent="emit('submit')"
@submit.prevent="submit"
>
<label class="sr-only" for="search_field_input">{{
t("Keyword, event title, group name, etc.")
}}</label>
<o-input
class="flex-1"
v-model="search"
:placeholder="t('Keyword, event title, group name, etc.')"
id="search_field_input"
autofocus
autocapitalize="off"
autocomplete="off"
@ -21,8 +24,10 @@
v-model="location"
:hide-map="true"
:hide-selected="true"
:default-text="locationDefaultText"
labelClass="sr-only"
/>
<o-button type="submit" icon-left="magnify">
<o-button native-type="submit" icon-left="magnify">
<template v-if="search">{{ t("Go!") }}</template>
<template v-else>{{ t("Explore!") }}</template>
</o-button>
@ -35,23 +40,28 @@ import { AddressSearchType } from "@/types/enums";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
import { useRouter } from "vue-router";
import RouteName from "@/router/name";
const props = defineProps<{
location: IAddress;
location: IAddress | null;
locationDefaultText?: string | null;
search: string;
}>();
const router = useRouter();
const emit = defineEmits<{
(event: "update:location", location: IAddress): void;
(event: "update:location", location: IAddress | null): void;
(event: "update:search", newSearch: string): void;
(event: "submit"): void;
}>();
const location = computed({
get(): IAddress {
get(): IAddress | null {
return props.location;
},
set(newLocation: IAddress) {
set(newLocation: IAddress | null) {
emit("update:location", newLocation);
},
});
@ -65,6 +75,25 @@ const search = computed({
},
});
const submit = () => {
emit("submit");
const lat = location.value?.geom
? parseFloat(location.value?.geom?.split(";")?.[1])
: undefined;
const lon = location.value?.geom
? parseFloat(location.value?.geom?.split(";")?.[0])
: undefined;
router.push({
name: RouteName.SEARCH,
query: {
locationName: location.value?.locality ?? location.value?.region,
lat,
lon,
search: search.value,
},
});
};
const { t } = useI18n({ useScope: "global" });
</script>
<style scoped>

View File

@ -26,7 +26,7 @@
>{{ t("Create an account") }}</o-button
>
<!-- We don't invite to find other instances yet -->
<!-- <o-button v-else type="is-link" tag="a" href="https://joinmastodon.org">{{ t('Find an instance') }}</o-button> -->
<!-- <o-button v-else variant="link" tag="a" href="https://joinmastodon.org">{{ t('Find an instance') }}</o-button> -->
<router-link
:to="{ name: RouteName.ABOUT }"
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-violet-title focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
@ -41,7 +41,12 @@ import { IConfig } from "@/types/config.model";
import RouteName from "@/router/name";
import { useI18n } from "vue-i18n";
defineProps<{ config: IConfig }>();
defineProps<{
config: Pick<
IConfig,
"name" | "description" | "slogan" | "registrationsOpen"
>;
}>();
const { t } = useI18n({ useScope: "global" });
</script>

View File

@ -28,7 +28,7 @@
</div>
<div class="overflow-hidden">
<div
class="relative w-full snap-x snap-always snap-mandatory overflow-x-auto flex pb-6 gap-x-5 gap-y-8"
class="relative w-full snap-x snap-always snap-mandatory overflow-x-auto flex pb-6 gap-x-5 gap-y-8 p-1"
ref="scrollContainer"
@scroll="scrollHandler"
>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<svg
class="bg-white dark:bg-gray-900 dark:fill-white"
class="bg-white dark:bg-zinc-900 dark:fill-white"
:class="{ 'bg-gray-900': invert }"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 248.16 46.78"

View File

@ -1,11 +1,11 @@
<template>
<nav class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-gray-900">
<nav class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-zinc-900">
<div class="container mx-auto flex flex-wrap items-center mx-auto gap-4">
<router-link :to="{ name: RouteName.HOME }" class="flex items-center">
<MobilizonLogo class="w-40" />
</router-link>
<div class="flex items-center md:order-2 ml-auto" v-if="currentActor?.id">
<o-dropdown>
<o-dropdown position="bottom-left">
<template #trigger>
<button
type="button"
@ -14,33 +14,80 @@
aria-expanded="false"
>
<span class="sr-only">{{ t("Open user menu") }}</span>
<figure class="" v-if="currentActor?.avatar">
<figure class="h-8 w-8" v-if="currentActor?.avatar">
<img
class="rounded-full"
class="rounded-full w-full h-full object-cover"
alt=""
:src="currentActor?.avatar.url"
width="32"
height="32"
/>
</figure>
<AccountCircle :size="32" />
<AccountCircle v-else :size="32" />
</button>
</template>
<!-- Dropdown menu -->
<div
class="z-50 mt-4 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
class="z-50 text-base list-none bg-white rounded divide-y divide-gray-100 dark:bg-zinc-700 dark:divide-gray-600 max-w-xs"
position="bottom-left"
>
<o-dropdown-item aria-role="listitem">
<div class="">
<span class="block text-sm text-gray-900 dark:text-white">{{
<div class="px-4">
<span class="block text-sm text-zinc-900 dark:text-white">{{
displayName(currentActor)
}}</span>
<span
class="block text-sm font-medium text-gray-500 truncate dark:text-gray-400"
>{{ currentUser?.role }}</span
class="block text-sm font-medium text-zinc-500 truncate dark:text-zinc-400"
v-if="currentUser?.role === ICurrentUserRole.ADMINISTRATOR"
>{{ t("Administrator") }}</span
>
<span
class="block text-sm font-medium text-zinc-500 truncate dark:text-zinc-400"
v-if="currentUser?.role === ICurrentUserRole.MODERATOR"
>{{ t("Moderator") }}</span
>
</div>
</o-dropdown-item>
<o-dropdown-item
v-for="identity in identities"
:active="identity.id === currentActor.id"
:key="identity.id"
tabindex="0"
@click="
setIdentity({
preferredUsername: identity.preferredUsername,
})
"
@keyup.enter="
setIdentity({
preferredUsername: identity.preferredUsername,
})
"
>
<div class="flex gap-1 items-center">
<div class="flex-none">
<figure class="" v-if="identity.avatar">
<img
class="rounded-full h-8 w-8"
loading="lazy"
:src="identity.avatar.url"
alt=""
height="32"
width="32"
/>
</figure>
<AccountCircle v-else :size="32" />
</div>
<div
class="text-base text-zinc-700 dark:text-zinc-100 flex flex-col flex-auto overflow-hidden items-start"
>
<p class="truncate">{{ displayName(identity) }}</p>
<p class="truncate text-sm" v-if="identity.name">
@{{ identity.preferredUsername }}
</p>
</div>
</div>
</o-dropdown-item>
<o-dropdown-item
@ -49,7 +96,7 @@
:to="{ name: RouteName.SETTINGS }"
>
<span
class="block py-2 px-4 text-sm text-gray-700 dark:text-gray-200 dark:hover:text-white"
class="block py-2 px-4 text-sm text-zinc-700 dark:text-zinc-200 dark:hover:text-white"
>{{ t("My account") }}</span
>
</o-dropdown-item>
@ -60,7 +107,7 @@
:to="{ name: RouteName.ADMIN_DASHBOARD }"
>
<span
class="block py-2 px-4 text-sm text-gray-700 dark:text-gray-200 dark:hover:text-white"
class="block py-2 px-4 text-sm text-zinc-700 dark:text-zinc-200 dark:hover:text-white"
>{{ t("Administration") }}</span
>
</o-dropdown-item>
@ -70,7 +117,7 @@
@keyup.enter="logout"
>
<span
class="block py-2 px-4 text-sm text-gray-700 dark:text-gray-200 dark:hover:text-white"
class="block py-2 px-4 text-sm text-zinc-700 dark:text-zinc-200 dark:hover:text-white"
>{{ t("Log out") }}</span
>
</o-dropdown-item>
@ -80,7 +127,7 @@
<button
@click="showMobileMenu = !showMobileMenu"
type="button"
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
class="inline-flex items-center p-2 ml-1 text-sm text-zinc-500 rounded-lg md:hidden hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:focus:ring-gray-600"
aria-controls="mobile-menu-2"
aria-expanded="false"
>
@ -105,33 +152,33 @@
:class="{ hidden: !showMobileMenu }"
>
<ul
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:text-sm md:font-medium"
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold"
>
<li v-if="currentActor?.id">
<router-link
:to="{ name: RouteName.MY_EVENTS }"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
>{{ t("My events") }}</router-link
>
</li>
<li v-if="currentActor?.id">
<router-link
:to="{ name: RouteName.MY_GROUPS }"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
>{{ t("My groups") }}</router-link
>
</li>
<li v-if="!currentActor?.id">
<router-link
:to="{ name: RouteName.LOGIN }"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
>{{ t("Login") }}</router-link
>
</li>
<li v-if="!currentActor?.id">
<router-link
:to="{ name: RouteName.REGISTER }"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
>{{ t("Register") }}</router-link
>
</li>
@ -327,6 +374,9 @@ import {
useCurrentActorClient,
useCurrentUserIdentities,
} from "@/composition/apollo/actor";
import { useMutation } from "@vue/apollo-composable";
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
import { changeIdentity } from "@/utils/identity";
// import { useRestrictions } from "@/composition/apollo/config";
const { currentUser } = useCurrentUserClient();
@ -400,11 +450,17 @@ watch(identities, () => {
// await router.push({ name: RouteName.HOME });
// };
// const { onDone, mutate: setIdentity } = useMutation(UPDATE_DEFAULT_ACTOR);
const { onDone, mutate: setIdentity } = useMutation<{
changeDefaultActor: { id: string; defaultActor: { id: string } };
}>(UPDATE_DEFAULT_ACTOR);
// onDone(() => {
// changeIdentity(identity);
// });
onDone(({ data }) => {
const identity = identities.value?.find(
({ id }) => id === data?.changeDefaultActor?.defaultActor?.id
);
if (!identity) return;
changeIdentity(identity);
});
// const hideCreateEventsButton = computed((): boolean => {
// return !!restrictions.value?.onlyGroupsCanCreateEvents;

View File

@ -1,16 +1,16 @@
<template>
<section class="container mx-auto">
<h1 class="title" v-if="loading">
{{ $t("Your participation request is being validated") }}
{{ t("Your participation request is being validated") }}
</h1>
<div v-else>
<div v-if="failed && participation === undefined">
<o-notification
:title="$t('Error while validating participation request')"
:title="t('Error while validating participation request')"
variant="danger"
>
{{
$t(
t(
"Either the participation request has already been validated, either the validation token is incorrect."
)
}}
@ -18,27 +18,25 @@
</div>
<div v-else>
<h1 class="title">
{{ $t("Your participation request has been validated") }}
{{ t("Your participation request has been validated") }}
</h1>
<p
class="prose dark:prose-invert"
v-if="participation?.event.joinOptions == EventJoinOptions.RESTRICTED"
>
{{
$t("Your participation still has to be approved by the organisers.")
t("Your participation still has to be approved by the organisers.")
}}
</p>
<div v-if="failed">
<o-notification
:title="
$t(
'Error while updating participation status inside this browser'
)
t('Error while updating participation status inside this browser')
"
variant="warning"
>
{{
$t(
t(
"We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue."
)
}}
@ -46,15 +44,15 @@
</div>
<div class="columns has-text-centered">
<div class="column">
<router-link
native-type="button"
tag="a"
class="button is-primary is-large"
<o-button
tag="router-link"
variant="primary"
size="large"
:to="{
name: RouteName.EVENT,
params: { uuid: participation?.event.uuid },
}"
>{{ $t("Go to the event page") }}</router-link
>{{ t("Go to the event page") }}</o-button
>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -69,7 +69,7 @@
/>
</article>
<div class="flex gap-2 mt-2">
<o-button type="is-text" @click="emit('close-move-modal')">{{
<o-button variant="text" @click="emit('close-move-modal')">{{
$t("Cancel")
}}</o-button>
<o-button

View File

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

View File

@ -1,5 +1,12 @@
<template>
<li class="setting-menu-item" :class="{ active: isActive }">
<li
class="setting-menu-item"
:class="{
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-500': isActive,
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-300 dark:hover:bg-mbz-purple-400 dark:text-white':
!isActive,
}"
>
<router-link v-if="to" :to="to">
<span>{{ title }}</span>
</router-link>
@ -31,7 +38,7 @@ const isActive = computed((): boolean => {
<style lang="scss" scoped>
li.setting-menu-item {
font-size: 1.05rem;
background-color: #fff1de;
// background-color: #fff1de;
margin: auto;
span {
@ -47,7 +54,7 @@ li.setting-menu-item {
&:hover,
&.active {
cursor: pointer;
background-color: lighten(#fea72b, 10%);
// background-color: lighten(#fea72b, 10%);
}
}
</style>

View File

@ -1,5 +1,7 @@
<template>
<li class="bg-yellow-1 text-violet-2 text-xl">
<li
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-500 dark:text-zinc-100 text-xl"
>
<router-link
class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline"
v-if="to"

View File

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

View File

@ -1,5 +1,5 @@
<template>
<span class="icon has-text-primary is-large">
<span class="text-black dark:text-white dark:fill-white">
<svg
version="1.1"
viewBox="0 0 65.131 65.131"
@ -12,7 +12,7 @@
/>
<path
d="m23.631 51.953c-2.348-1.5418-6.9154-5.1737-7.0535-5.6088-0.06717-0.21164 0.45125-0.99318 3.3654-5.0734 2.269-3.177 3.7767-5.3581 3.7767-5.4637 0-0.03748-1.6061-0.60338-3.5691-1.2576-6.1342-2.0442-8.3916-2.9087-8.5288-3.2663-0.03264-0.08506 0.09511-0.68598 0.28388-1.3354 0.643-2.212 2.7038-8.4123 2.7959-8.4123 0.05052 0 2.6821 0.85982 5.848 1.9107 3.1659 1.0509 5.897 1.9222 6.0692 1.9362 0.3089 0.02514 0.31402 0.01925 0.38295-0.44107 0.09851-0.65784 0.26289-5.0029 0.2633-6.9599 1.87e-4 -0.90267 0.02801-2.5298 0.06184-3.6158l0.0615-1.9746h10.392l0.06492 4.4556c0.06287 4.3148 0.18835 7.8236 0.29865 8.3513 0.0295 0.14113 0.11236 0.2566 0.18412 0.2566 0.07176 0 1.6955-0.50861 3.6084-1.1303 4.5213-1.4693 6.2537-2.0038 7.3969-2.2822 0.87349-0.21269 0.94061-0.21704 1.0505-0.06806 0.45169 0.61222 3.3677 9.2365 3.1792 9.4025-0.33681 0.29628-2.492 1.1048-6.9823 2.6194-5.3005 1.7879-5.1321 1.7279-5.1321 1.8283 0 0.13754 0.95042 1.522 3.5468 5.1666 1.3162 1.8475 2.6802 3.7905 3.0311 4.3176l0.63804 0.95842-0.27216 0.28519c-1.1112 1.1644-7.3886 5.8693-7.8309 5.8693-0.22379 0-1.2647-1.2321-2.9284-3.4663-0.90374-1.2137-2.264-3.0402-3.0228-4.059-0.75878-1.0188-1.529-2.0203-1.7116-2.2256l-0.33201-0.37324-0.32674 0.37324c-0.43918 0.50169-2.226 2.867-3.8064 5.0388-2.1662 2.9767-3.6326 4.8055-3.8532 4.8055-0.05161 0-0.4788-0.25278-0.94931-0.56173z"
fill="#fff"
fill="transparent"
stroke-width=".093311"
/>
</svg>

View File

@ -1,5 +1,5 @@
<template>
<span class="icon has-text-primary is-large">
<span class="text-primary dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976">
<title>Mastodon logo</title>
<path

View File

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

View File

@ -1,5 +1,5 @@
<template>
<span class="icon has-text-primary is-large">
<span class="text-primary dark:text-white dark:fill-white">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Telegram</title>
<path

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
confirmText?: string;
cancelText?: string;
onConfirm: (prompt?: string) => {};
onCancel?: (source: string) => {};
onConfirm: (prompt?: string) => any;
onCancel?: (source: string) => any;
ariaLabel?: string;
ariaModal?: boolean;
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