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:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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