mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
refactor: ♻️ Refactor typography code in notes and profiles
Some checks failed
Some checks failed
This commit is contained in:
parent
97733c18ee
commit
53b71afdd5
|
|
@ -1,33 +0,0 @@
|
||||||
<template>
|
|
||||||
<span :class="cn('text-primary group', $props.class)">
|
|
||||||
<span class="group-hover:hidden">
|
|
||||||
<slot />
|
|
||||||
</span>
|
|
||||||
<span class="hidden group-hover:inline">
|
|
||||||
<span @click="copyText"
|
|
||||||
class="select-none cursor-pointer space-x-1">
|
|
||||||
<Clipboard class="size-4 -translate-y-0.5 inline" />
|
|
||||||
{{ m.clean_yummy_owl_reside() }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="tsx" setup>
|
|
||||||
import { Check, Clipboard } from "lucide-vue-next";
|
|
||||||
import type { HTMLAttributes } from "vue";
|
|
||||||
import { toast } from "vue-sonner";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import * as m from "~/paraglide/messages.js";
|
|
||||||
|
|
||||||
const { text } = defineProps<{
|
|
||||||
text: string;
|
|
||||||
class?: HTMLAttributes["class"];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { copy } = useClipboard();
|
|
||||||
const copyText = () => {
|
|
||||||
copy(text);
|
|
||||||
toast.success("Copied to clipboard");
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="rounded flex flex-row items-center gap-3">
|
<div class="rounded grid grid-cols-[auto_1fr_auto] items-center gap-3">
|
||||||
<HoverCard v-model:open="popupOpen" @update:open="() => {
|
<HoverCard v-model:open="popupOpen" @update:open="() => {
|
||||||
if (!preferences.popup_avatar_hover) {
|
if (!preferences.popup_avatar_hover) {
|
||||||
popupOpen = false;
|
popupOpen = false;
|
||||||
|
|
@ -16,24 +16,18 @@
|
||||||
<SmallCard :account="author" />
|
<SmallCard :account="author" />
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
<div
|
<Col
|
||||||
:class="cn('flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight', smallLayout && 'text-sm')">
|
:class="smallLayout && 'text-sm'">
|
||||||
<span class="truncate font-semibold" v-render-emojis="author.emojis">{{
|
<Text class="font-semibold" v-render-emojis="author.emojis">{{
|
||||||
author.display_name
|
author.display_name
|
||||||
}}</span>
|
}}</Text>
|
||||||
<span class="truncate text-sm tracking-tight">
|
<div class="-mt-1">
|
||||||
<span>
|
<Address as="span" :username="username" :domain="instance" />
|
||||||
<span
|
|
||||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
|
|
||||||
@{{ username }}
|
|
||||||
</span>
|
|
||||||
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
|
|
||||||
</span>
|
|
||||||
·
|
·
|
||||||
<span class="text-muted-foreground ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</span>
|
<Text as="span" muted class="ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</Text>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</Col>
|
||||||
<div class="flex flex-col gap-1 h-full justify-center items-end" v-if="!smallLayout">
|
<div v-if="!smallLayout">
|
||||||
<NuxtLink :href="noteUrlAsPath" class="text-xs text-muted-foreground"
|
<NuxtLink :href="noteUrlAsPath" class="text-xs text-muted-foreground"
|
||||||
:title="visibilities[visibility].text">
|
:title="visibilities[visibility].text">
|
||||||
<component :is="visibilities[visibility].icon" class="size-4" />
|
<component :is="visibilities[visibility].icon" class="size-4" />
|
||||||
|
|
@ -52,8 +46,12 @@ import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getLocale } from "~/paraglide/runtime";
|
import { getLocale } from "~/paraglide/runtime";
|
||||||
|
import Address from "../profiles/address.vue";
|
||||||
import Avatar from "../profiles/avatar.vue";
|
import Avatar from "../profiles/avatar.vue";
|
||||||
import SmallCard from "../profiles/small-card.vue";
|
import SmallCard from "../profiles/small-card.vue";
|
||||||
|
import Col from "../typography/layout/col.vue";
|
||||||
|
import Row from "../typography/layout/row.vue";
|
||||||
|
import Text from "../typography/text.vue";
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,7 @@
|
||||||
follower.display_name
|
follower.display_name
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="truncate tracking-tight">
|
<span class="truncate tracking-tight">
|
||||||
<CopyableText :text="follower.acct">
|
<Address :username="username" :domain="domain" />
|
||||||
<span
|
|
||||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
|
|
||||||
@{{ username }}
|
|
||||||
</span>
|
|
||||||
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
|
|
||||||
</CopyableText>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -39,9 +33,9 @@ import type { Account } from "@versia/client/schemas";
|
||||||
import { Check, Loader, X } from "lucide-vue-next";
|
import { Check, Loader, X } from "lucide-vue-next";
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from "vue-sonner";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import CopyableText from "~/components/notes/copyable-text.vue";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import * as m from "~/paraglide/messages.js";
|
import * as m from "~/paraglide/messages.js";
|
||||||
|
import Address from "../profiles/address.vue";
|
||||||
import Avatar from "../profiles/avatar.vue";
|
import Avatar from "../profiles/avatar.vue";
|
||||||
|
|
||||||
const { follower } = defineProps<{
|
const { follower } = defineProps<{
|
||||||
|
|
@ -50,7 +44,7 @@ const { follower } = defineProps<{
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const followerUrl = `/@${follower.acct}`;
|
const followerUrl = `/@${follower.acct}`;
|
||||||
const [username, instance] = follower.acct.split("@");
|
const [username, domain] = follower.acct.split("@");
|
||||||
const { relationship } = useRelationship(client, follower.id);
|
const { relationship } = useRelationship(client, follower.id);
|
||||||
|
|
||||||
// TODO: Add "followed" notification
|
// TODO: Add "followed" notification
|
||||||
|
|
|
||||||
15
components/profiles/address.vue
Normal file
15
components/profiles/address.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<Text class="font-semibold text-sm tracking-tight">
|
||||||
|
<span class="text-accent-foreground">@{{ username }}</span>
|
||||||
|
<span v-if="domain" class="text-muted-foreground">@{{ domain }}</span>
|
||||||
|
</Text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Text from "../typography/text.vue";
|
||||||
|
|
||||||
|
const { username, domain } = defineProps<{
|
||||||
|
username: string;
|
||||||
|
domain?: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -1,31 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger :as-child="true">
|
<TooltipTrigger :as-child="true">
|
||||||
<Badge variant="outline" class="gap-1">
|
<Badge variant="default" class="gap-1">
|
||||||
<svg viewBox="0 0 22 22" v-if="verified" aria-hidden="true" class="size-4 fill-secondary-foreground">
|
<BadgeCheck v-if="verified" />
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z">
|
|
||||||
</path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded" />
|
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded" />
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent v-if="description">
|
<TooltipContent v-if="description">
|
||||||
<p>{{ description }}</p>
|
<Text>{{ description }}</Text>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { BadgeCheck } from "lucide-vue-next";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "~/components/ui/tooltip";
|
} from "~/components/ui/tooltip";
|
||||||
|
import Text from "../typography/text.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<Row class="gap-2" wrap
|
||||||
class="flex flex-row flex-wrap gap-2 -mx-2"
|
|
||||||
v-if="isDeveloper || account.bot || roles.length > 0"
|
v-if="isDeveloper || account.bot || roles.length > 0"
|
||||||
>
|
>
|
||||||
<ProfileBadge
|
<ProfileBadge
|
||||||
|
|
@ -21,13 +20,14 @@
|
||||||
:description="role.description"
|
:description="role.description"
|
||||||
:icon="role.icon"
|
:icon="role.icon"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Account } from "@versia/client/schemas";
|
import type { Account } from "@versia/client/schemas";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import * as m from "~/paraglide/messages.js";
|
import * as m from "~/paraglide/messages.js";
|
||||||
|
import Row from "../typography/layout/row.vue";
|
||||||
import ProfileBadge from "./profile-badge.vue";
|
import ProfileBadge from "./profile-badge.vue";
|
||||||
|
|
||||||
const { account } = defineProps<{
|
const { account } = defineProps<{
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :class="['prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content]" v-html="content" v-render-emojis="emojis">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { CustomEmoji } from "@versia/client/schemas";
|
|
||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
const { content } = defineProps<{
|
|
||||||
content: string;
|
|
||||||
emojis: z.infer<typeof CustomEmoji>[];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style module>
|
|
||||||
@import url("~/styles/content.css");
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-y-4">
|
<Col class="gap-y-4">
|
||||||
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-1 break-words">
|
<Col v-for="field in fields" :key="field.name" class="gap-1 break-words">
|
||||||
<h3 class="font-semibold text-sm" v-render-emojis="emojis">{{ field.name }}</h3>
|
<HeadingSmall v-render-emojis="emojis">{{ field.name }}</HeadingSmall>
|
||||||
<div v-html="field.value" class="prose prose-sm prose-zinc dark:prose-invert" v-render-emojis="emojis"></div>
|
<Html v-html="field.value" v-render-emojis="emojis" />
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</Col>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CustomEmoji, Field } from "@versia/client/schemas";
|
import type { CustomEmoji, Field } from "@versia/client/schemas";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
import HeadingSmall from "~/components/typography/headings/small.vue";
|
||||||
|
import Html from "../typography/html.vue";
|
||||||
|
import Col from "../typography/layout/col.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
fields: z.infer<typeof Field>[];
|
fields: z.infer<typeof Field>[];
|
||||||
|
|
|
||||||
80
components/profiles/profile-relationship-actions.vue
Normal file
80
components/profiles/profile-relationship-actions.vue
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && identity"
|
||||||
|
@click="relationship?.following ? unfollow() : follow()">
|
||||||
|
<Loader v-if="isLoading" class="animate-spin" />
|
||||||
|
<span v-else>
|
||||||
|
{{
|
||||||
|
relationship?.following
|
||||||
|
? m.brief_upper_otter_cuddle()
|
||||||
|
: relationship?.requested
|
||||||
|
? m.weak_bright_larva_grasp()
|
||||||
|
: m.lazy_major_loris_grasp()
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Account } from "@versia/client/schemas";
|
||||||
|
import { Loader } from "lucide-vue-next";
|
||||||
|
import { toast } from "vue-sonner";
|
||||||
|
import type { z } from "zod";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import * as m from "~/paraglide/messages.js";
|
||||||
|
import { confirmModalService } from "../modals/composable";
|
||||||
|
|
||||||
|
const { account } = defineProps<{
|
||||||
|
account: z.infer<typeof Account>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { relationship, isLoading } = useRelationship(client, account.id);
|
||||||
|
const isMe = identity.value?.account.id === account.id;
|
||||||
|
|
||||||
|
const follow = async () => {
|
||||||
|
if (preferences.confirm_actions.value.includes("follow")) {
|
||||||
|
const confirmation = await confirmModalService.confirm({
|
||||||
|
title: m.many_fair_capybara_imagine(),
|
||||||
|
message: m.mellow_yummy_jannes_cuddle({
|
||||||
|
acct: `@${account.acct}`,
|
||||||
|
}),
|
||||||
|
confirmText: m.cuddly_even_tern_loop(),
|
||||||
|
cancelText: m.soft_bold_ant_attend(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmation.confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = toast.loading(m.quick_basic_peacock_bubble());
|
||||||
|
const { data } = await client.value.followAccount(account.id);
|
||||||
|
toast.dismiss(id);
|
||||||
|
|
||||||
|
relationship.value = data;
|
||||||
|
toast.success(m.awake_quick_cuckoo_smile());
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfollow = async () => {
|
||||||
|
if (preferences.confirm_actions.value.includes("follow")) {
|
||||||
|
const confirmation = await confirmModalService.confirm({
|
||||||
|
title: m.funny_aloof_swan_loop(),
|
||||||
|
message: m.white_best_dolphin_catch({
|
||||||
|
acct: `@${account.acct}`,
|
||||||
|
}),
|
||||||
|
confirmText: m.cute_polite_oryx_blend(),
|
||||||
|
cancelText: m.soft_bold_ant_attend(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmation.confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = toast.loading(m.big_safe_guppy_mix());
|
||||||
|
const { data } = await client.value.unfollowAccount(account.id);
|
||||||
|
toast.dismiss(id);
|
||||||
|
|
||||||
|
relationship.value = data;
|
||||||
|
toast.success(m.misty_level_stingray_expand());
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,41 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<Row class="gap-2 w-full justify-around">
|
||||||
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
|
<Col centered>
|
||||||
<div>
|
<Bold>{{ noteCount }}</Bold>
|
||||||
<CalendarDays class="size-4" />
|
<Small muted>{{ m.real_gray_stork_seek() }}</Small>
|
||||||
{{ m.gross_fancy_platypus_seek() }} <span class="text-primary font-semibold">{{ formattedCreationDate }}</span>
|
</Col>
|
||||||
</div>
|
<Col centered>
|
||||||
</div>
|
<Bold>{{ followerCount }}</Bold>
|
||||||
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
|
<Small muted>{{ m.teal_helpful_parakeet_hike() }}</Small>
|
||||||
<div>
|
</Col>
|
||||||
<span class="text-primary font-semibold">{{ noteCount }}</span> {{ m.real_gray_stork_seek() }}
|
<Col centered>
|
||||||
</div>
|
<Bold>{{ followingCount }}</Bold>
|
||||||
·
|
<Small muted>{{ m.aloof_royal_samuel_startle() }}</Small>
|
||||||
<div>
|
</Col>
|
||||||
<span class="text-primary font-semibold">{{ followerCount }}</span> {{ m.teal_helpful_parakeet_hike() }}
|
</Row>
|
||||||
</div>
|
|
||||||
·
|
|
||||||
<div>
|
|
||||||
<span class="text-primary font-semibold">{{ followingCount }}</span> {{ m.aloof_royal_samuel_startle() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { CalendarDays } from "lucide-vue-next";
|
|
||||||
import * as m from "~/paraglide/messages.js";
|
import * as m from "~/paraglide/messages.js";
|
||||||
import { getLocale } from "~/paraglide/runtime";
|
import Bold from "../typography/bold.vue";
|
||||||
|
import Col from "../typography/layout/col.vue";
|
||||||
|
import Row from "../typography/layout/row.vue";
|
||||||
|
import Small from "../typography/small.vue";
|
||||||
|
|
||||||
const { creationDate } = defineProps<{
|
const { noteCount, followerCount, followingCount } = defineProps<{
|
||||||
creationDate: Date;
|
|
||||||
noteCount: number;
|
noteCount: number;
|
||||||
followerCount: number;
|
followerCount: number;
|
||||||
followingCount: number;
|
followingCount: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const formattedCreationDate = new Intl.DateTimeFormat(getLocale(), {
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
}).format(creationDate);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,141 +1,65 @@
|
||||||
<template>
|
<template>
|
||||||
<Card class="*:w-full">
|
<Card class="gap-4">
|
||||||
<ProfileHeader
|
<ProfileHeader :header="account.header" :avatar="account.avatar" :display-name="account.display_name" />
|
||||||
:header="account.header"
|
<Row class="justify-end gap-2">
|
||||||
:avatar="account.avatar"
|
<ProfileRelationshipActions :account="account" />
|
||||||
:display-name="account.display_name"
|
<ProfileActions :account="account">
|
||||||
/>
|
<Button variant="secondary" size="icon">
|
||||||
<CardContent>
|
<Ellipsis />
|
||||||
<div class="flex flex-row justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
:disabled="isLoading || relationship?.requested"
|
|
||||||
v-if="!isMe && identity"
|
|
||||||
@click="relationship?.following ? unfollow() : follow()"
|
|
||||||
>
|
|
||||||
<Loader v-if="isLoading" class="animate-spin" />
|
|
||||||
<span v-else>
|
|
||||||
{{
|
|
||||||
relationship?.following
|
|
||||||
? m.brief_upper_otter_cuddle()
|
|
||||||
: relationship?.requested
|
|
||||||
? m.weak_bright_larva_grasp()
|
|
||||||
: m.lazy_major_loris_grasp()
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
<ProfileActions :account="account">
|
</ProfileActions>
|
||||||
<Button variant="secondary" size="icon">
|
</Row>
|
||||||
<Ellipsis />
|
<Col class="justify-center">
|
||||||
</Button>
|
<Text class="font-bold" v-render-emojis="account.emojis">
|
||||||
</ProfileActions>
|
{{ account.display_name }}
|
||||||
</div>
|
</Text>
|
||||||
<div class="flex flex-col -mt-1 gap-1 justify-center">
|
<Address :username="username" :domain="domain" />
|
||||||
<CardTitle class="" v-render-emojis="account.emojis">
|
</Col>
|
||||||
{{ account.display_name }}
|
<ProfileBadges :account="account" />
|
||||||
</CardTitle>
|
<Html v-html="account.note" v-render-emojis="account.emojis" />
|
||||||
<CopyableText :text="account.acct">
|
<Separator />
|
||||||
<span
|
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" />
|
||||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
|
<Separator v-if="account.fields.length > 0" />
|
||||||
>
|
<Row>
|
||||||
@{{ username }}
|
<HeadingSmall class="flex items-center gap-1">
|
||||||
</span>
|
<CalendarDays class="size-4" /> {{ formattedCreationDate }}
|
||||||
<span class="text-muted-foreground"
|
</HeadingSmall>
|
||||||
>{{ instance && "@" }}{{ instance }}</span
|
</Row>
|
||||||
>
|
<Separator />
|
||||||
</CopyableText>
|
<ProfileStats :follower-count="account.followers_count" :following-count="account.following_count"
|
||||||
</div>
|
:note-count="account.statuses_count" />
|
||||||
<ProfileBadges :account="account" class="my-2" />
|
|
||||||
<ProfileContent :content="account.note" :emojis="account.emojis" />
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter class="flex-col *:w-full gap-4">
|
|
||||||
<ProfileStats
|
|
||||||
:creation-date="new Date(account.created_at || 0)"
|
|
||||||
:follower-count="account.followers_count"
|
|
||||||
:following-count="account.following_count"
|
|
||||||
:note-count="account.statuses_count"
|
|
||||||
/>
|
|
||||||
<Separator v-if="account.fields.length > 0" />
|
|
||||||
<ProfileFields
|
|
||||||
v-if="account.fields.length > 0"
|
|
||||||
:fields="account.fields"
|
|
||||||
:emojis="account.emojis"
|
|
||||||
/>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Account } from "@versia/client/schemas";
|
import type { Account } from "@versia/client/schemas";
|
||||||
import { Ellipsis, Loader } from "lucide-vue-next";
|
import { CalendarDays, Ellipsis } from "lucide-vue-next";
|
||||||
import { toast } from "vue-sonner";
|
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import CopyableText from "~/components/notes/copyable-text.vue";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card";
|
import { Card } from "~/components/ui/card";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import * as m from "~/paraglide/messages.js";
|
import { getLocale } from "~/paraglide/runtime";
|
||||||
import { confirmModalService } from "../modals/composable";
|
import HeadingSmall from "../typography/headings/small.vue";
|
||||||
|
import Html from "../typography/html.vue";
|
||||||
|
import Col from "../typography/layout/col.vue";
|
||||||
|
import Row from "../typography/layout/row.vue";
|
||||||
|
import Text from "../typography/text.vue";
|
||||||
|
import Address from "./address.vue";
|
||||||
import ProfileActions from "./profile-actions.vue";
|
import ProfileActions from "./profile-actions.vue";
|
||||||
import ProfileBadges from "./profile-badges.vue";
|
import ProfileBadges from "./profile-badges.vue";
|
||||||
import ProfileContent from "./profile-content.vue";
|
|
||||||
import ProfileFields from "./profile-fields.vue";
|
import ProfileFields from "./profile-fields.vue";
|
||||||
import ProfileHeader from "./profile-header.vue";
|
import ProfileHeader from "./profile-header.vue";
|
||||||
|
import ProfileRelationshipActions from "./profile-relationship-actions.vue";
|
||||||
import ProfileStats from "./profile-stats.vue";
|
import ProfileStats from "./profile-stats.vue";
|
||||||
|
|
||||||
const { account } = defineProps<{
|
const { account } = defineProps<{
|
||||||
account: z.infer<typeof Account>;
|
account: z.infer<typeof Account>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { relationship, isLoading } = useRelationship(client, account.id);
|
const [username, domain] = account.acct.split("@");
|
||||||
const isMe = identity.value?.account.id === account.id;
|
|
||||||
const [username, instance] = account.acct.split("@");
|
|
||||||
|
|
||||||
const follow = async () => {
|
const formattedCreationDate = new Intl.DateTimeFormat(getLocale(), {
|
||||||
if (preferences.confirm_actions.value.includes("follow")) {
|
dateStyle: "long",
|
||||||
const confirmation = await confirmModalService.confirm({
|
timeStyle: "short",
|
||||||
title: m.many_fair_capybara_imagine(),
|
}).format(new Date(account.created_at || 0));
|
||||||
message: m.mellow_yummy_jannes_cuddle({
|
|
||||||
acct: `@${account.acct}`,
|
|
||||||
}),
|
|
||||||
confirmText: m.cuddly_even_tern_loop(),
|
|
||||||
cancelText: m.soft_bold_ant_attend(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmation.confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = toast.loading(m.quick_basic_peacock_bubble());
|
|
||||||
const { data } = await client.value.followAccount(account.id);
|
|
||||||
toast.dismiss(id);
|
|
||||||
|
|
||||||
relationship.value = data;
|
|
||||||
toast.success(m.awake_quick_cuckoo_smile());
|
|
||||||
};
|
|
||||||
|
|
||||||
const unfollow = async () => {
|
|
||||||
if (preferences.confirm_actions.value.includes("follow")) {
|
|
||||||
const confirmation = await confirmModalService.confirm({
|
|
||||||
title: m.funny_aloof_swan_loop(),
|
|
||||||
message: m.white_best_dolphin_catch({
|
|
||||||
acct: `@${account.acct}`,
|
|
||||||
}),
|
|
||||||
confirmText: m.cute_polite_oryx_blend(),
|
|
||||||
cancelText: m.soft_bold_ant_attend(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmation.confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = toast.loading(m.big_safe_guppy_mix());
|
|
||||||
const { data } = await client.value.unfollowAccount(account.id);
|
|
||||||
toast.dismiss(id);
|
|
||||||
|
|
||||||
relationship.value = data;
|
|
||||||
toast.success(m.misty_level_stingray_expand());
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -23,23 +23,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center items-center mt-8">
|
<div class="flex flex-col justify-center items-center mt-8">
|
||||||
<span class="font-semibold" v-render-emojis="account.emojis">
|
<Text class="font-bold" v-render-emojis="account.emojis">
|
||||||
{{ account.display_name }}
|
{{ account.display_name }}
|
||||||
</span>
|
</Text>
|
||||||
<CopyableText :text="account.acct" class="text-sm">
|
<Address :username="username" :domain="domain" />
|
||||||
<span
|
|
||||||
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
|
|
||||||
>
|
|
||||||
@{{ username }}
|
|
||||||
</span>
|
|
||||||
<span class="text-muted-foreground"
|
|
||||||
>{{ instance && "@" }}{{ instance }}</span
|
|
||||||
>
|
|
||||||
</CopyableText>
|
|
||||||
</div>
|
</div>
|
||||||
<ProfileContent
|
<Html
|
||||||
:content="account.note"
|
v-html="account.note"
|
||||||
:emojis="account.emojis"
|
v-render-emojis="account.emojis"
|
||||||
class="mt-4 max-h-72 overflow-y-auto"
|
class="mt-4 max-h-72 overflow-y-auto"
|
||||||
/>
|
/>
|
||||||
<Separator v-if="account.fields.length > 0" class="mt-4" />
|
<Separator v-if="account.fields.length > 0" class="mt-4" />
|
||||||
|
|
@ -55,14 +46,15 @@
|
||||||
import type { Account } from "@versia/client/schemas";
|
import type { Account } from "@versia/client/schemas";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import CopyableText from "../notes/copyable-text.vue";
|
import Html from "../typography/html.vue";
|
||||||
|
import Text from "../typography/text.vue";
|
||||||
|
import Address from "./address.vue";
|
||||||
import Avatar from "./avatar.vue";
|
import Avatar from "./avatar.vue";
|
||||||
import ProfileContent from "./profile-content.vue";
|
|
||||||
import ProfileFields from "./profile-fields.vue";
|
import ProfileFields from "./profile-fields.vue";
|
||||||
|
|
||||||
const { account } = defineProps<{
|
const { account } = defineProps<{
|
||||||
account: z.infer<typeof Account>;
|
account: z.infer<typeof Account>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const [username, instance] = account.acct.split("@");
|
const [username, domain] = account.acct.split("@");
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,10 @@
|
||||||
>
|
>
|
||||||
<Avatar :src="account.avatar" :name="account.display_name" class="size-10" />
|
<Avatar :src="account.avatar" :name="account.display_name" class="size-10" />
|
||||||
<CardContent class="leading-tight">
|
<CardContent class="leading-tight">
|
||||||
<span
|
<Text class="font-semibold" v-render-emojis="account.emojis">
|
||||||
class="font-semibold"
|
{{ account.display_name }}
|
||||||
v-render-emojis="account.emojis"
|
</Text>
|
||||||
>{{ account.display_name }}</span
|
<Address :username="account.username" :domain="domain" />
|
||||||
>
|
|
||||||
<span class="text-xs">
|
|
||||||
@{{ account.username }}@{{ domain }}
|
|
||||||
</span>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -21,6 +17,8 @@
|
||||||
import type { Account } from "@versia/client/schemas";
|
import type { Account } from "@versia/client/schemas";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
import Text from "../typography/text.vue";
|
||||||
|
import Address from "./address.vue";
|
||||||
import Avatar from "./avatar.vue";
|
import Avatar from "./avatar.vue";
|
||||||
|
|
||||||
const { account, domain, naked } = defineProps<{
|
const { account, domain, naked } = defineProps<{
|
||||||
|
|
|
||||||
23
components/typography/bold.vue
Normal file
23
components/typography/bold.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "strong",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('font-semibold', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
23
components/typography/headings/large.vue
Normal file
23
components/typography/headings/large.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "h1",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('text-4xl font-extrabold tracking-tight text-balance', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
23
components/typography/headings/medium.vue
Normal file
23
components/typography/headings/medium.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "h2",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('text-3xl font-semibold tracking-tight', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
23
components/typography/headings/small.vue
Normal file
23
components/typography/headings/small.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "h3",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('text-sm font-semibold tracking-tight', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
27
components/typography/html.vue
Normal file
27
components/typography/html.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "div",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content, props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
@import url("~/styles/content.css");
|
||||||
|
</style>
|
||||||
25
components/typography/layout/col.vue
Normal file
25
components/typography/layout/col.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
wrap?: boolean;
|
||||||
|
centered?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "div",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('flex flex-col', props.wrap && 'flex-wrap', props.class, props.centered && 'items-center')"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
25
components/typography/layout/row.vue
Normal file
25
components/typography/layout/row.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
wrap?: boolean;
|
||||||
|
centered?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "div",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('flex flex-row', props.wrap && 'flex-wrap', props.class, props.centered && 'items-center')"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
24
components/typography/small.vue
Normal file
24
components/typography/small.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
muted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "span",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('text-xs', props.class, props.muted && 'text-muted-foreground')"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
24
components/typography/text.vue
Normal file
24
components/typography/text.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Primitive, type PrimitiveProps } from "reka-ui";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
muted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "p",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('leading-7', props.class, props.muted && 'text-muted-foreground')"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
Loading…
Reference in a new issue