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>
|
||||
<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="() => {
|
||||
if (!preferences.popup_avatar_hover) {
|
||||
popupOpen = false;
|
||||
|
|
@ -16,24 +16,18 @@
|
|||
<SmallCard :account="author" />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
<div
|
||||
:class="cn('flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight', smallLayout && 'text-sm')">
|
||||
<span class="truncate font-semibold" v-render-emojis="author.emojis">{{
|
||||
<Col
|
||||
:class="smallLayout && 'text-sm'">
|
||||
<Text class="font-semibold" v-render-emojis="author.emojis">{{
|
||||
author.display_name
|
||||
}}</span>
|
||||
<span class="truncate text-sm tracking-tight">
|
||||
<span>
|
||||
<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>
|
||||
}}</Text>
|
||||
<div class="-mt-1">
|
||||
<Address as="span" :username="username" :domain="instance" />
|
||||
·
|
||||
<span class="text-muted-foreground ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 h-full justify-center items-end" v-if="!smallLayout">
|
||||
<Text as="span" muted class="ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<div v-if="!smallLayout">
|
||||
<NuxtLink :href="noteUrlAsPath" class="text-xs text-muted-foreground"
|
||||
:title="visibilities[visibility].text">
|
||||
<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 { cn } from "@/lib/utils";
|
||||
import { getLocale } from "~/paraglide/runtime";
|
||||
import Address from "../profiles/address.vue";
|
||||
import Avatar from "../profiles/avatar.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 {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
|
|
|
|||
|
|
@ -8,13 +8,7 @@
|
|||
follower.display_name
|
||||
}}</span>
|
||||
<span class="truncate tracking-tight">
|
||||
<CopyableText :text="follower.acct">
|
||||
<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>
|
||||
<Address :username="username" :domain="domain" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -39,9 +33,9 @@ import type { Account } from "@versia/client/schemas";
|
|||
import { Check, Loader, X } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import type { z } from "zod";
|
||||
import CopyableText from "~/components/notes/copyable-text.vue";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import Address from "../profiles/address.vue";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
|
||||
const { follower } = defineProps<{
|
||||
|
|
@ -50,7 +44,7 @@ const { follower } = defineProps<{
|
|||
|
||||
const loading = ref(true);
|
||||
const followerUrl = `/@${follower.acct}`;
|
||||
const [username, instance] = follower.acct.split("@");
|
||||
const [username, domain] = follower.acct.split("@");
|
||||
const { relationship } = useRelationship(client, follower.id);
|
||||
|
||||
// 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>
|
||||
<Tooltip>
|
||||
<TooltipTrigger :as-child="true">
|
||||
<Badge variant="outline" class="gap-1">
|
||||
<svg viewBox="0 0 22 22" v-if="verified" aria-hidden="true" class="size-4 fill-secondary-foreground">
|
||||
<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>
|
||||
<Badge variant="default" class="gap-1">
|
||||
<BadgeCheck v-if="verified" />
|
||||
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded" />
|
||||
{{ name }}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent v-if="description">
|
||||
<p>{{ description }}</p>
|
||||
<Text>{{ description }}</Text>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BadgeCheck } from "lucide-vue-next";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
import Text from "../typography/text.vue";
|
||||
|
||||
defineProps<{
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
class="flex flex-row flex-wrap gap-2 -mx-2"
|
||||
<Row class="gap-2" wrap
|
||||
v-if="isDeveloper || account.bot || roles.length > 0"
|
||||
>
|
||||
<ProfileBadge
|
||||
|
|
@ -21,13 +20,14 @@
|
|||
:description="role.description"
|
||||
:icon="role.icon"
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import Row from "../typography/layout/row.vue";
|
||||
import ProfileBadge from "./profile-badge.vue";
|
||||
|
||||
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>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-1 break-words">
|
||||
<h3 class="font-semibold text-sm" v-render-emojis="emojis">{{ field.name }}</h3>
|
||||
<div v-html="field.value" class="prose prose-sm prose-zinc dark:prose-invert" v-render-emojis="emojis"></div>
|
||||
</div>
|
||||
</div>
|
||||
<Col class="gap-y-4">
|
||||
<Col v-for="field in fields" :key="field.name" class="gap-1 break-words">
|
||||
<HeadingSmall v-render-emojis="emojis">{{ field.name }}</HeadingSmall>
|
||||
<Html v-html="field.value" v-render-emojis="emojis" />
|
||||
</Col>
|
||||
</Col>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { CustomEmoji, Field } from "@versia/client/schemas";
|
||||
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<{
|
||||
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>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
|
||||
<div>
|
||||
<CalendarDays class="size-4" />
|
||||
{{ m.gross_fancy_platypus_seek() }} <span class="text-primary font-semibold">{{ formattedCreationDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
|
||||
<div>
|
||||
<span class="text-primary font-semibold">{{ noteCount }}</span> {{ m.real_gray_stork_seek() }}
|
||||
</div>
|
||||
·
|
||||
<div>
|
||||
<span class="text-primary font-semibold">{{ followerCount }}</span> {{ m.teal_helpful_parakeet_hike() }}
|
||||
</div>
|
||||
·
|
||||
<div>
|
||||
<span class="text-primary font-semibold">{{ followingCount }}</span> {{ m.aloof_royal_samuel_startle() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Row class="gap-2 w-full justify-around">
|
||||
<Col centered>
|
||||
<Bold>{{ noteCount }}</Bold>
|
||||
<Small muted>{{ m.real_gray_stork_seek() }}</Small>
|
||||
</Col>
|
||||
<Col centered>
|
||||
<Bold>{{ followerCount }}</Bold>
|
||||
<Small muted>{{ m.teal_helpful_parakeet_hike() }}</Small>
|
||||
</Col>
|
||||
<Col centered>
|
||||
<Bold>{{ followingCount }}</Bold>
|
||||
<Small muted>{{ m.aloof_royal_samuel_startle() }}</Small>
|
||||
</Col>
|
||||
</Row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CalendarDays } from "lucide-vue-next";
|
||||
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<{
|
||||
creationDate: Date;
|
||||
const { noteCount, followerCount, followingCount } = defineProps<{
|
||||
noteCount: number;
|
||||
followerCount: number;
|
||||
followingCount: number;
|
||||
}>();
|
||||
|
||||
const formattedCreationDate = new Intl.DateTimeFormat(getLocale(), {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(creationDate);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,141 +1,65 @@
|
|||
<template>
|
||||
<Card class="*:w-full">
|
||||
<ProfileHeader
|
||||
:header="account.header"
|
||||
:avatar="account.avatar"
|
||||
:display-name="account.display_name"
|
||||
/>
|
||||
<CardContent>
|
||||
<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>
|
||||
<Card class="gap-4">
|
||||
<ProfileHeader :header="account.header" :avatar="account.avatar" :display-name="account.display_name" />
|
||||
<Row class="justify-end gap-2">
|
||||
<ProfileRelationshipActions :account="account" />
|
||||
<ProfileActions :account="account">
|
||||
<Button variant="secondary" size="icon">
|
||||
<Ellipsis />
|
||||
</Button>
|
||||
<ProfileActions :account="account">
|
||||
<Button variant="secondary" size="icon">
|
||||
<Ellipsis />
|
||||
</Button>
|
||||
</ProfileActions>
|
||||
</div>
|
||||
<div class="flex flex-col -mt-1 gap-1 justify-center">
|
||||
<CardTitle class="" v-render-emojis="account.emojis">
|
||||
{{ account.display_name }}
|
||||
</CardTitle>
|
||||
<CopyableText :text="account.acct">
|
||||
<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>
|
||||
<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>
|
||||
</ProfileActions>
|
||||
</Row>
|
||||
<Col class="justify-center">
|
||||
<Text class="font-bold" v-render-emojis="account.emojis">
|
||||
{{ account.display_name }}
|
||||
</Text>
|
||||
<Address :username="username" :domain="domain" />
|
||||
</Col>
|
||||
<ProfileBadges :account="account" />
|
||||
<Html v-html="account.note" v-render-emojis="account.emojis" />
|
||||
<Separator />
|
||||
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" />
|
||||
<Separator v-if="account.fields.length > 0" />
|
||||
<Row>
|
||||
<HeadingSmall class="flex items-center gap-1">
|
||||
<CalendarDays class="size-4" /> {{ formattedCreationDate }}
|
||||
</HeadingSmall>
|
||||
</Row>
|
||||
<Separator />
|
||||
<ProfileStats :follower-count="account.followers_count" :following-count="account.following_count"
|
||||
:note-count="account.statuses_count" />
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from "@versia/client/schemas";
|
||||
import { Ellipsis, Loader } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import { CalendarDays, Ellipsis } from "lucide-vue-next";
|
||||
import type { z } from "zod";
|
||||
import CopyableText from "~/components/notes/copyable-text.vue";
|
||||
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 * as m from "~/paraglide/messages.js";
|
||||
import { confirmModalService } from "../modals/composable";
|
||||
import { getLocale } from "~/paraglide/runtime";
|
||||
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 ProfileBadges from "./profile-badges.vue";
|
||||
import ProfileContent from "./profile-content.vue";
|
||||
import ProfileFields from "./profile-fields.vue";
|
||||
import ProfileHeader from "./profile-header.vue";
|
||||
import ProfileRelationshipActions from "./profile-relationship-actions.vue";
|
||||
import ProfileStats from "./profile-stats.vue";
|
||||
|
||||
const { account } = defineProps<{
|
||||
account: z.infer<typeof Account>;
|
||||
}>();
|
||||
|
||||
const { relationship, isLoading } = useRelationship(client, account.id);
|
||||
const isMe = identity.value?.account.id === account.id;
|
||||
const [username, instance] = account.acct.split("@");
|
||||
const [username, domain] = account.acct.split("@");
|
||||
|
||||
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());
|
||||
};
|
||||
const formattedCreationDate = new Intl.DateTimeFormat(getLocale(), {
|
||||
dateStyle: "long",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(account.created_at || 0));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -23,23 +23,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<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 }}
|
||||
</span>
|
||||
<CopyableText :text="account.acct" class="text-sm">
|
||||
<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>
|
||||
</Text>
|
||||
<Address :username="username" :domain="domain" />
|
||||
</div>
|
||||
<ProfileContent
|
||||
:content="account.note"
|
||||
:emojis="account.emojis"
|
||||
<Html
|
||||
v-html="account.note"
|
||||
v-render-emojis="account.emojis"
|
||||
class="mt-4 max-h-72 overflow-y-auto"
|
||||
/>
|
||||
<Separator v-if="account.fields.length > 0" class="mt-4" />
|
||||
|
|
@ -55,14 +46,15 @@
|
|||
import type { Account } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
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 ProfileContent from "./profile-content.vue";
|
||||
import ProfileFields from "./profile-fields.vue";
|
||||
|
||||
const { account } = defineProps<{
|
||||
account: z.infer<typeof Account>;
|
||||
}>();
|
||||
|
||||
const [username, instance] = account.acct.split("@");
|
||||
const [username, domain] = account.acct.split("@");
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,14 +5,10 @@
|
|||
>
|
||||
<Avatar :src="account.avatar" :name="account.display_name" class="size-10" />
|
||||
<CardContent class="leading-tight">
|
||||
<span
|
||||
class="font-semibold"
|
||||
v-render-emojis="account.emojis"
|
||||
>{{ account.display_name }}</span
|
||||
>
|
||||
<span class="text-xs">
|
||||
@{{ account.username }}@{{ domain }}
|
||||
</span>
|
||||
<Text class="font-semibold" v-render-emojis="account.emojis">
|
||||
{{ account.display_name }}
|
||||
</Text>
|
||||
<Address :username="account.username" :domain="domain" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
@ -21,6 +17,8 @@
|
|||
import type { Account } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import Text from "../typography/text.vue";
|
||||
import Address from "./address.vue";
|
||||
import Avatar from "./avatar.vue";
|
||||
|
||||
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