build: switch from yarn to npm to manage js dependencies and move js contents to root
yarn v1 is being deprecated and starts to have some issues Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
60
src/components/Group/GroupCard.story.vue
Normal file
60
src/components/Group/GroupCard.story.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<Story>
|
||||
<Variant title="Empty">
|
||||
<div class="p-5">
|
||||
<GroupCard :group="basicGroup" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="With media">
|
||||
<div class="p-5">
|
||||
<GroupCard :group="groupWithMedia" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="with followers or members">
|
||||
<div class="p-5">
|
||||
<GroupCard :group="groupWithFollowersOrMembers" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="Row mode">
|
||||
<GroupCard :group="groupWithFollowersOrMembers" mode="row" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { IGroup } from "@/types/actor";
|
||||
import GroupCard from "./GroupCard.vue";
|
||||
|
||||
const basicGroup: IGroup = {
|
||||
name: "Framasoft",
|
||||
preferredUsername: "framasoft",
|
||||
avatar: null,
|
||||
domain: "mobilizon.fr",
|
||||
url: "",
|
||||
summary: "",
|
||||
suspended: false,
|
||||
members: { total: 0, elements: [] },
|
||||
followers: { total: 0, elements: [] },
|
||||
};
|
||||
|
||||
const groupWithMedia: IGroup = {
|
||||
...basicGroup,
|
||||
banner: {
|
||||
url: "https://mobilizon.fr/media/a8227a16cc80b3d20ff5ee549a29c1b20a0ca1547f8861129aae9f00c3c69d12.jpg?name=framasoft%27s%20banner.jpg",
|
||||
},
|
||||
avatar: {
|
||||
url: "https://mobilizon.fr/media/890f5396ef80081a6b1b18a5db969746cf8bb340e8a4e657d665e41f6646c539.jpg?name=framasoft%27s%20avatar.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
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>
|
||||
132
src/components/Group/GroupCard.vue
Normal file
132
src/components/Group/GroupCard.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<LinkOrRouterLink
|
||||
:to="to"
|
||||
:isInternal="isInternal"
|
||||
class="mbz-card snap-center shrink-0 dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg flex items-center flex-col"
|
||||
:class="{
|
||||
'sm:flex-row': mode === 'row',
|
||||
'sm:max-w-xs w-[18rem] shrink-0 flex flex-col': mode === 'column',
|
||||
}"
|
||||
>
|
||||
<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="overflow-hidden flex-auto">
|
||||
<h3
|
||||
class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
|
||||
dir="auto"
|
||||
>
|
||||
{{ displayName(group) }}
|
||||
</h3>
|
||||
<span class="block truncate">
|
||||
{{ `@${usernameWithDomain(group)}` }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mb-2 line-clamp-3"
|
||||
dir="auto"
|
||||
v-html="saneSummary"
|
||||
v-if="showSummary"
|
||||
/>
|
||||
<div>
|
||||
<inline-address
|
||||
v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
|
||||
:physicalAddress="group.physicalAddress"
|
||||
/>
|
||||
<p
|
||||
class="flex gap-1"
|
||||
v-if="group?.members?.total && group?.followers?.total"
|
||||
>
|
||||
<Account />
|
||||
{{
|
||||
t(
|
||||
"{count} members or followers",
|
||||
{
|
||||
count: group.members.total + group.followers.total,
|
||||
},
|
||||
group.members.total + group.followers.total
|
||||
)
|
||||
}}
|
||||
</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>
|
||||
</LinkOrRouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { displayName, IGroup, usernameWithDomain } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||
import { addressFullName } from "@/types/address.model";
|
||||
import { useI18n } from "vue-i18n";
|
||||
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;
|
||||
isRemoteGroup?: boolean;
|
||||
isLoggedIn?: boolean;
|
||||
mode?: "row" | "column";
|
||||
}>(),
|
||||
{ showSummary: true, isRemoteGroup: false, isLoggedIn: true, mode: "column" }
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
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>
|
||||
91
src/components/Group/GroupMemberCard.story.vue
Normal file
91
src/components/Group/GroupMemberCard.story.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<Story>
|
||||
<Variant title="simple member">
|
||||
<div class="p-5">
|
||||
<GroupMemberCard :member="basicMember" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="moderator">
|
||||
<div class="p-5">
|
||||
<GroupMemberCard :member="moderatorMember" />
|
||||
</div>
|
||||
</Variant>
|
||||
<Variant title="administrator">
|
||||
<div class="p-5">
|
||||
<GroupMemberCard :member="adminMember" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { IActor } from "@/types/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import GroupMemberCard from "./GroupMemberCard.vue";
|
||||
const baseActorAvatar = {
|
||||
id: "",
|
||||
name: "",
|
||||
alt: "",
|
||||
metadata: {},
|
||||
url: "https://social.tcit.fr/system/accounts/avatars/000/000/001/original/a28c50ce5f2b13fd.jpg",
|
||||
};
|
||||
|
||||
const basePerson: IActor = {
|
||||
name: "Thomas Citharel",
|
||||
preferredUsername: "tcit",
|
||||
avatar: baseActorAvatar,
|
||||
domain: null,
|
||||
url: "",
|
||||
summary: "",
|
||||
suspended: false,
|
||||
};
|
||||
|
||||
const basicGroup: IActor = {
|
||||
name: "Framasoft",
|
||||
preferredUsername: "framasoft",
|
||||
avatar: {
|
||||
url: "https://mobilizon.fr/media/ff5b2d425fb73e17fcbb56a1a032359ee0b21453c11af59e103e783817a32fdf.png?name=framasoft%27s%20avatar.png",
|
||||
},
|
||||
domain: "mobilizon.fr",
|
||||
url: "",
|
||||
summary: `<p><strong>La Fediverse</strong>, <strong>c'est la <em><u>Féd</u>ération qui englobe l'Un<u>ivers</u> des réseaux sociaux libres et décentralisés,</em> </strong>dont Mobilizon (évènements), Mastodon (microblog), Peertube (vidéos), Pixelfed (photos), Funkwhale (musique), Matrix (messagerie instantanée)... et tant d'autres font partie.</p><p><strong>Et "La Fediverse <em>Nantaise</em>" est un collectif cherchant à faire connaître localement tout le potentiel de ces réseaux ! :-)</strong></p>`,
|
||||
suspended: false,
|
||||
members: { total: 0, elements: [] },
|
||||
followers: { total: 0, elements: [] },
|
||||
};
|
||||
|
||||
const basicMember: IMember = {
|
||||
parent: basicGroup as IActor,
|
||||
actor: basePerson,
|
||||
role: MemberRole.MEMBER,
|
||||
};
|
||||
|
||||
const moderatorMember: IMember = {
|
||||
parent: basicGroup,
|
||||
actor: basePerson,
|
||||
role: MemberRole.MODERATOR,
|
||||
};
|
||||
|
||||
const adminMember: IMember = {
|
||||
parent: basicGroup,
|
||||
actor: basePerson,
|
||||
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 groupWithFollowersOrMembers = {
|
||||
// ...groupWithMedia,
|
||||
// members: { total: 2, elements: [] },
|
||||
// followers: { total: 5, elements: [] },
|
||||
// };
|
||||
</script>
|
||||
108
src/components/Group/GroupMemberCard.vue
Normal file
108
src/components/Group/GroupMemberCard.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="rounded shadow-lg bg-white dark:bg-mbz-purple dark:text-white">
|
||||
<div
|
||||
class="bg-mbz-yellow-alt-50 text-black flex items-center gap-1 p-2 rounded-t-lg"
|
||||
dir="auto"
|
||||
>
|
||||
<figure class="" v-if="member.actor.avatar">
|
||||
<img
|
||||
class="rounded-xl"
|
||||
:src="member.actor.avatar.url"
|
||||
alt=""
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</figure>
|
||||
<AccountCircle v-else :size="24" />
|
||||
{{ displayNameAndUsername(member.actor) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 p-2 pr-4" dir="auto">
|
||||
<div class="flex-1">
|
||||
<div class="p-2 flex gap-2">
|
||||
<div class="">
|
||||
<figure class="h-12 w-12" v-if="member.parent.avatar">
|
||||
<img
|
||||
class="rounded-full w-full h-full object-cover"
|
||||
:src="member.parent.avatar.url"
|
||||
alt=""
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<AccountGroup v-else :size="48" />
|
||||
</div>
|
||||
<div class="" dir="auto">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(member.parent),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<h2 class="mt-0">{{ member.parent.name }}</h2>
|
||||
<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
|
||||
>
|
||||
<tag
|
||||
variant="info"
|
||||
v-else-if="member.role === MemberRole.MODERATOR"
|
||||
>{{ t("Moderator") }}</tag
|
||||
>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 prose dark:prose-invert lg:prose-xl line-clamp-2"
|
||||
v-if="member.parent.summary"
|
||||
v-html="htmlToText(member.parent.summary)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<o-dropdown aria-role="list" position="bottom-left">
|
||||
<template #trigger>
|
||||
<DotsHorizontal class="cursor-pointer" />
|
||||
</template>
|
||||
|
||||
<o-dropdown-item
|
||||
class="inline-flex gap-1"
|
||||
aria-role="listitem"
|
||||
@click="emit('leave')"
|
||||
>
|
||||
<ExitToApp />
|
||||
{{ t("Leave") }}
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { displayNameAndUsername, usernameWithDomain } from "@/types/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
import ExitToApp from "vue-material-design-icons/ExitToApp.vue";
|
||||
import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
|
||||
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Tag from "@/components/TagElement.vue";
|
||||
import { htmlToText } from "@/utils/html";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
defineProps<{
|
||||
member: IMember;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["leave"]);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
</script>
|
||||
40
src/components/Group/GroupSection.vue
Normal file
40
src/components/Group/GroupSection.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col mb-3 border-2"
|
||||
:class="{
|
||||
'border-mbz-purple': privateSection,
|
||||
'border-yellow-1': !privateSection,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-stretch py-3 px-1 bg-yellow-1 text-violet-title">
|
||||
<div class="flex flex-1 gap-1">
|
||||
<o-icon :icon="icon" custom-size="36" />
|
||||
<h2 class="text-2xl font-medium mt-0">{{ title }}</h2>
|
||||
</div>
|
||||
<router-link class="self-center" :to="route">{{
|
||||
t("View all")
|
||||
}}</router-link>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="flex justify-end p-2">
|
||||
<slot name="create"></slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
icon: string;
|
||||
privateSection?: boolean;
|
||||
route: { name: string; params: { preferredUsername: string } };
|
||||
}>(),
|
||||
{ privateSection: true }
|
||||
);
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
</script>
|
||||
71
src/components/Group/InvitationCard.vue
Normal file
71
src/components/Group/InvitationCard.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="border rounded p-2 bg-mbz-yellow-alt-50 dark:bg-zinc-700">
|
||||
<div class="prose dark:prose-invert">
|
||||
<i18n-t
|
||||
tag="p"
|
||||
keypath="You have been invited by {invitedBy} to the following group:"
|
||||
>
|
||||
<template #invitedBy>
|
||||
<b>{{ member?.invitedBy?.name }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="">
|
||||
<figure v-if="member.parent.avatar">
|
||||
<img
|
||||
class="rounded-full"
|
||||
:src="member.parent.avatar.url"
|
||||
alt=""
|
||||
height="48"
|
||||
width="48"
|
||||
/>
|
||||
</figure>
|
||||
<AccountGroup :size="48" v-else />
|
||||
</div>
|
||||
<div class="mr-3">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(member.parent),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<p class="">{{ member.parent.name }}</p>
|
||||
<p class="text-sm">@{{ usernameWithDomain(member.parent) }}</p>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex gap-2">
|
||||
<div class="">
|
||||
<o-button variant="success" @click="$emit('accept', member.id)">
|
||||
{{ t("Accept") }}
|
||||
</o-button>
|
||||
</div>
|
||||
<div class="">
|
||||
<o-button variant="danger" @click="$emit('reject', member.id)">
|
||||
{{ t("Decline") }}
|
||||
</o-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
defineProps<{
|
||||
member: IMember;
|
||||
}>();
|
||||
</script>
|
||||
79
src/components/Group/InvitationsList.vue
Normal file
79
src/components/Group/InvitationsList.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<section class="my-3" v-if="invitations && invitations.length > 0">
|
||||
<InvitationCard
|
||||
v-for="member in invitations"
|
||||
:key="member.id"
|
||||
:member="member"
|
||||
@accept="acceptInvitation({ id: member.id })"
|
||||
@reject="rejectInvitation({ id: member.id })"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||
import { PERSON_STATUS_GROUP } from "@/graphql/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { IGroup, IPerson, usernameWithDomain } from "@/types/actor";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { ErrorResponse } from "@/types/errors.model";
|
||||
import { inject } from "vue";
|
||||
import type { Notifier } from "@/plugins/notifier";
|
||||
|
||||
defineProps<{
|
||||
invitations: IMember[];
|
||||
}>();
|
||||
|
||||
const { mutate: acceptInvitation, onError: onAcceptInvitationError } =
|
||||
useMutation<{ acceptInvitation: IMember }, { id: string }>(
|
||||
ACCEPT_INVITATION,
|
||||
{
|
||||
refetchQueries({ data }) {
|
||||
const profile = data?.acceptInvitation?.actor as IPerson;
|
||||
const group = data?.acceptInvitation?.parent as IGroup;
|
||||
if (profile && group) {
|
||||
return [
|
||||
{
|
||||
query: PERSON_STATUS_GROUP,
|
||||
variables: { id: profile.id, group: usernameWithDomain(group) },
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const onError = (error: ErrorResponse) => {
|
||||
console.error(error);
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
notifier?.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
};
|
||||
|
||||
onAcceptInvitationError((err) => onError(err as unknown as ErrorResponse));
|
||||
|
||||
const { mutate: rejectInvitation, onError: onRejectInvitationError } =
|
||||
useMutation<{ rejectInvitation: IMember }, { id: string }>(
|
||||
REJECT_INVITATION,
|
||||
{
|
||||
refetchQueries({ data }) {
|
||||
const profile = data?.rejectInvitation?.actor as IPerson;
|
||||
const group = data?.rejectInvitation?.parent as IGroup;
|
||||
if (profile && group) {
|
||||
return [
|
||||
{
|
||||
query: PERSON_STATUS_GROUP,
|
||||
variables: { id: profile.id, group: usernameWithDomain(group) },
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
onRejectInvitationError((err) => onError(err as unknown as ErrorResponse));
|
||||
</script>
|
||||
44
src/components/Group/JoinGroupWithAccount.vue
Normal file
44
src/components/Group/JoinGroupWithAccount.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<redirect-with-account
|
||||
v-if="uri"
|
||||
:uri="uri"
|
||||
:pathAfterLogin="`/@${preferredUsername}`"
|
||||
:sentence="
|
||||
t(
|
||||
`We will redirect you to your instance in order to interact with this group`
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
|
||||
import { useGroup } from "@/composition/apollo/group";
|
||||
import { displayName } from "@/types/actor";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const props = defineProps<{
|
||||
preferredUsername: string;
|
||||
}>();
|
||||
|
||||
const { group } = useGroup(computed(() => props.preferredUsername));
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const groupTitle = computed((): undefined | string => {
|
||||
return group && displayName(group.value);
|
||||
});
|
||||
|
||||
const uri = computed((): string | undefined => {
|
||||
return group.value?.url;
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: computed(() =>
|
||||
t("Join group {group}", {
|
||||
group: groupTitle.value,
|
||||
})
|
||||
),
|
||||
});
|
||||
</script>
|
||||
32
src/components/Group/MultiGroupCard.vue
Normal file
32
src/components/Group/MultiGroupCard.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="multi-card-group">
|
||||
<group-card
|
||||
class="group-card"
|
||||
v-for="group in groups"
|
||||
:group="group"
|
||||
:key="group.id"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { IGroup } from "@/types/actor";
|
||||
import GroupCard from "./GroupCard.vue";
|
||||
|
||||
defineProps<{
|
||||
groups: IGroup[];
|
||||
}>();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.multi-card-group {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr;
|
||||
grid-column-gap: 30px;
|
||||
grid-row-gap: 30px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
.group-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
src/components/Group/Sections/DiscussionsSection.vue
Normal file
50
src/components/Group/Sections/DiscussionsSection.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<group-section
|
||||
:title="t('Discussions')"
|
||||
icon="chat"
|
||||
:route="{
|
||||
name: RouteName.DISCUSSION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>
|
||||
<template #default>
|
||||
<div v-if="group?.discussions?.total ?? 0 > 0">
|
||||
<discussion-list-item
|
||||
v-for="discussion in group?.discussions?.elements ?? []"
|
||||
:key="discussion.id"
|
||||
:discussion="discussion"
|
||||
/>
|
||||
</div>
|
||||
<empty-content v-else icon="chat" :inline="true">
|
||||
{{ t("No discussions yet") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
<template #create>
|
||||
<o-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.CREATE_DISCUSSION,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ t("+ Start a discussion") }}</o-button
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RouteName from "@/router/name";
|
||||
import { IGroup } from "@/types/actor/group.model";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
|
||||
import GroupSection from "@/components/Group/GroupSection.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
defineProps<{
|
||||
group: Pick<IGroup, "preferredUsername" | "domain" | "discussions">;
|
||||
}>();
|
||||
</script>
|
||||
54
src/components/Group/Sections/EventsSection.vue
Normal file
54
src/components/Group/Sections/EventsSection.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<group-section
|
||||
:title="t('Events')"
|
||||
icon="calendar"
|
||||
:privateSection="false"
|
||||
:route="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>
|
||||
<template #default>
|
||||
<div
|
||||
class="flex flex-wrap gap-2 py-1"
|
||||
v-if="group && group.organizedEvents.total > 0"
|
||||
>
|
||||
<event-minimalist-card
|
||||
v-for="event in group.organizedEvents.elements.slice(0, 3)"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
/>
|
||||
</div>
|
||||
<empty-content v-else-if="group" icon="calendar" :inline="true">
|
||||
{{ t("No public upcoming events") }}
|
||||
</empty-content>
|
||||
<!-- <o-skeleton animated v-else></o-skeleton> -->
|
||||
</template>
|
||||
<template #create>
|
||||
<o-button
|
||||
tag="router-link"
|
||||
v-if="isModerator"
|
||||
:to="{
|
||||
name: RouteName.CREATE_EVENT,
|
||||
query: { actorId: group?.id },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ t("+ Create an event") }}</o-button
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RouteName from "@/router/name";
|
||||
import { IGroup } from "@/types/actor/group.model";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
|
||||
import GroupSection from "@/components/Group/GroupSection.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
defineProps<{ group: IGroup; isModerator: boolean }>();
|
||||
</script>
|
||||
50
src/components/Group/Sections/PostsSection.vue
Normal file
50
src/components/Group/Sections/PostsSection.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<group-section
|
||||
:title="t('Announcements')"
|
||||
icon="bullhorn"
|
||||
:privateSection="false"
|
||||
:route="{
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>
|
||||
<template #default>
|
||||
<div class="p-1">
|
||||
<multi-post-list-item
|
||||
v-if="group?.posts?.total ?? 0 > 0"
|
||||
:posts="(group?.posts?.elements ?? []).slice(0, 3)"
|
||||
:isCurrentActorMember="isMember"
|
||||
/>
|
||||
<empty-content v-else-if="group" icon="bullhorn" :inline="true">
|
||||
{{ t("No posts yet") }}
|
||||
</empty-content>
|
||||
</div>
|
||||
</template>
|
||||
<template #create>
|
||||
<o-button
|
||||
tag="router-link"
|
||||
v-if="isModerator"
|
||||
:to="{
|
||||
name: RouteName.POST_CREATE,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ t("+ Create a post") }}</o-button
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RouteName from "@/router/name";
|
||||
import { IGroup } from "@/types/actor/group.model";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import MultiPostListItem from "@/components/Post/MultiPostListItem.vue";
|
||||
import GroupSection from "@/components/Group/GroupSection.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
defineProps<{ group: IGroup; isModerator: boolean; isMember: boolean }>();
|
||||
</script>
|
||||
65
src/components/Group/Sections/ResourcesSection.vue
Normal file
65
src/components/Group/Sections/ResourcesSection.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<group-section
|
||||
:title="t('Resources')"
|
||||
icon="link"
|
||||
:route="{
|
||||
name: RouteName.RESOURCE_FOLDER_ROOT,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>
|
||||
<template #default>
|
||||
<div
|
||||
v-if="group?.resources?.elements?.length ?? 0 > 0"
|
||||
class="p-1 bg-white dark:bg-transparent"
|
||||
>
|
||||
<div
|
||||
v-for="resource in group?.resources?.elements ?? []"
|
||||
:key="resource.id"
|
||||
>
|
||||
<resource-item
|
||||
:resource="resource"
|
||||
v-if="resource.type !== 'folder'"
|
||||
:inline="true"
|
||||
/>
|
||||
<folder-item
|
||||
:resource="resource"
|
||||
:group="group"
|
||||
v-else-if="group"
|
||||
:inline="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<empty-content v-else icon="link" :inline="true">
|
||||
{{ t("No resources yet") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
<template #create>
|
||||
<o-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.RESOURCE_FOLDER_ROOT,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ t("+ Add a resource") }}</o-button
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RouteName from "@/router/name";
|
||||
import { IGroup } from "@/types/actor/group.model";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import ResourceItem from "@/components/Resource/ResourceItem.vue";
|
||||
import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import GroupSection from "@/components/Group/GroupSection.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
defineProps<{
|
||||
group: Pick<IGroup, "preferredUsername" | "domain" | "resources">;
|
||||
}>();
|
||||
</script>
|
||||
42
src/components/Group/ShareGroupModal.vue
Normal file
42
src/components/Group/ShareGroupModal.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<share-modal
|
||||
:title="t('Share this group')"
|
||||
:text="displayName(group)"
|
||||
:url="group.url"
|
||||
:input-label="t('Group URL')"
|
||||
>
|
||||
<o-notification
|
||||
variant="warning"
|
||||
v-if="group.visibility !== GroupVisibility.PUBLIC"
|
||||
:closable="false"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"This group is accessible only through it's link. Be careful where you post this link."
|
||||
)
|
||||
}}
|
||||
</o-notification>
|
||||
</share-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { GroupVisibility } from "@/types/enums";
|
||||
import { displayName, IGroup } from "@/types/actor";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import ShareModal from "@/components/Share/ShareModal.vue";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
defineProps<{
|
||||
group: IGroup;
|
||||
}>();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.diaspora,
|
||||
.mastodon,
|
||||
.telegram {
|
||||
:deep(span svg) {
|
||||
width: 2.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
18
src/components/Group/SkeletonGroupResult.vue
Normal file
18
src/components/Group/SkeletonGroupResult.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 shadow rounded-md max-w-sm w-full mx-auto"
|
||||
>
|
||||
<div class="animate-pulse flex flex-col space-3-4 items-center">
|
||||
<div
|
||||
class="object-cover h-40 w-40 rounded-full bg-slate-700 p-2 md:p-4"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex gap-3 flex self-start flex-col justify-between p-2 md:p-4 w-full"
|
||||
>
|
||||
<div class="h-5 bg-slate-700"></div>
|
||||
<div class="h-3 bg-slate-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
16
src/components/Group/SkeletonGroupResultList.vue
Normal file
16
src/components/Group/SkeletonGroupResultList.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="bg-white dark:bg-slate-800 shadow rounded-md w-full mx-auto">
|
||||
<div class="animate-pulse flex flex-col sm:flex-row space-3-4 items-center">
|
||||
<div
|
||||
class="object-cover h-40 w-40 rounded-full bg-slate-700 m-2 md:m-4 shrink-0"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex gap-3 flex self-start flex-col justify-between m-2 md:m-4 self-center w-full px-2 md:px-4"
|
||||
>
|
||||
<div class="h-5 bg-slate-700 w-64"></div>
|
||||
<div class="h-3 bg-slate-700 w-52"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user