mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 11:39:16 +01:00
feat: 🎨 Design refactor of all pages
This commit is contained in:
parent
9467cef34b
commit
a45c04258e
20 changed files with 407 additions and 267 deletions
|
|
@ -21,6 +21,7 @@ const props = withDefaults(
|
|||
maxWidth?: number;
|
||||
widthUnit?: "px" | "%";
|
||||
class?: string;
|
||||
lines?: number;
|
||||
}>(),
|
||||
{
|
||||
shape: "rect",
|
||||
|
|
@ -49,5 +50,5 @@ const getWidth = (index: number, lines: number) => {
|
|||
return undefined;
|
||||
};
|
||||
|
||||
const lines = isContent.value ? Math.ceil(Math.random() * 5) : 1;
|
||||
const lines = isContent.value ? props.lines ?? Math.ceil(Math.random() * 5) : 1;
|
||||
</script>
|
||||
25
components/social-elements/instance/Presentation.vue
Normal file
25
components/social-elements/instance/Presentation.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="flex flex-col p-10 gap-4">
|
||||
<div
|
||||
class="aspect-video shrink-0 w-full rounded ring-white/5 bg-dark-800 shadow overflow-hidden ring-1 hover:ring-2 duration-100">
|
||||
<img class="object-cover w-full h-full duration-150 hover:scale-[102%] ease-in-out" v-if="instance?.banner"
|
||||
:src="instance.banner" />
|
||||
</div>
|
||||
|
||||
<div class="prose prose-invert prose-sm">
|
||||
<h2 class="text-center mb-10">{{ instance?.title }}</h2>
|
||||
<div v-html="description?.content"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="instance?.contact_account" class="flex flex-col gap-2 mt-auto">
|
||||
<h2 class="text-gray-200 font-semibold uppercase text-xs">Administrator</h2>
|
||||
<SocialElementsUsersSmallCard :account="instance.contact_account" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const client = await useMegalodon();
|
||||
const instance = await useInstance(client);
|
||||
const description = await useExtendedDescription(client);
|
||||
</script>
|
||||
|
|
@ -101,13 +101,14 @@ const timeAgo = useTimeAgo(props.note?.created_at ?? 0);
|
|||
const { copy } = useClipboard();
|
||||
const client = await useMegalodon();
|
||||
const mentions = await useResolveMentions(props.note?.mentions ?? [], client);
|
||||
const content = props.note
|
||||
? await useParsedContent(
|
||||
props.note.content,
|
||||
props.note.emojis,
|
||||
mentions.value,
|
||||
)
|
||||
: "";
|
||||
const content =
|
||||
props.note && process.client
|
||||
? await useParsedContent(
|
||||
props.note.content,
|
||||
props.note.emojis,
|
||||
mentions.value,
|
||||
)
|
||||
: "";
|
||||
const numberFormat = (number = 0) =>
|
||||
new Intl.NumberFormat(undefined, {
|
||||
notation: "compact",
|
||||
|
|
|
|||
132
components/social-elements/users/Account.vue
Normal file
132
components/social-elements/users/Account.vue
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<ClientOnly>
|
||||
<div class="w-full rounded ring-1 ring-white/10 pb-10">
|
||||
<Skeleton :enabled="skeleton" class="!w-full !h-full !aspect-[8/3]">
|
||||
<div class="w-full aspect-[8/3] border-b border-white/10 bg-dark-700">
|
||||
<img v-if="account?.header" :src="account.header" class="object-cover w-full h-full" />
|
||||
</div>
|
||||
</Skeleton>
|
||||
|
||||
<div class="flex items-start justify-between px-4 py-3">
|
||||
<div class="h-32 w-32 -mt-[4.5rem] z-10 bg-dark-700 rounded overflow-hidden">
|
||||
<Skeleton :enabled="skeleton" class="!h-full !w-full">
|
||||
<img class="cursor-pointer bg-dark-700 ring-1 ring-white/10" :src="account?.avatar" />
|
||||
</Skeleton>
|
||||
</div>
|
||||
<ButtonsSecondary v-if="account">Edit Profile</ButtonsSecondary>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 px-4">
|
||||
<h2
|
||||
class="text-xl font-bold text-gray-100 tracking-tight bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 text-transparent bg-clip-text">
|
||||
<Skeleton :enabled="skeleton" :min-width="200" :max-width="350" class="h-6">
|
||||
{{ account?.display_name }}
|
||||
<Icon v-if="account?.locked" name="tabler:lock"
|
||||
class="w-5 h-5 mb-0.5 text-gray-400 cursor-pointer"
|
||||
title="This account manually approves its followers" />
|
||||
</Skeleton>
|
||||
</h2>
|
||||
<span class="text-gray-400 block mt-2">
|
||||
<Skeleton :enabled="skeleton" :min-width="130" :max-width="250">@{{ account?.acct }}</Skeleton>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 px-4">
|
||||
<Skeleton :enabled="true" v-if="skeleton" class="!h-6" :min-width="50" :max-width="100" width-unit="%"
|
||||
shape="rect" type="content">
|
||||
</Skeleton>
|
||||
<div class="prose prose-invert" v-html="parsedNote" v-else></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center space-x-4 px-4">
|
||||
<div class="flex items-center space-x-1">
|
||||
<Skeleton :enabled="skeleton" :min-width="150" :max-width="150" shape="rect">
|
||||
<Icon name="tabler:calendar" class="w-5 h-5 text-gray-400" />
|
||||
<span class="text-gray-400">Created {{ formattedJoin }}</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<div v-if="account?.bot" class="flex items-center space-x-1">
|
||||
<Icon name="tabler:robot" class="w-5 h-5 text-gray-400" />
|
||||
<span class="text-gray-400">Bot</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center space-x-4 px-4">
|
||||
<div class="cursor-pointer hover:underline space-x-1">
|
||||
<Skeleton :enabled="skeleton" :min-width="100" :max-width="150" shape="rect">
|
||||
<span class="font-bold text-gray-200">{{ account?.statuses_count }}</span>
|
||||
<span class="text-gray-400">Posts</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<div class="cursor-pointer hover:underline space-x-1">
|
||||
<Skeleton :enabled="skeleton" :min-width="100" :max-width="150" shape="rect">
|
||||
<span class="font-bold text-gray-200">{{ account?.following_count }}</span>
|
||||
<span class="text-gray-400">Following</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!skeleton && parsedFields.length > 0" class="mt-4 px-4 flex-col flex space-y-3">
|
||||
<div v-for="field of parsedFields" :key="field.name" class="flex flex-col gap-1">
|
||||
<span class="text-pink-500 font-semibold" v-html="field.name"></span>
|
||||
<span class="text-gray-200 prose prose-invert break-all" v-html="field.value"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="skeleton" class="mt-4 px-4 flex-col space-y-3">
|
||||
<div v-for="_ of 3" class="flex flex-col gap-1">
|
||||
<Skeleton :enabled="skeleton" :min-width="10" :max-width="100" width-unit="%" shape="rect">
|
||||
</Skeleton>
|
||||
<Skeleton :enabled="skeleton" :min-width="10" :max-width="100" width-unit="%" shape="rect">
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from "~/types/mastodon/account";
|
||||
|
||||
const props = defineProps<{
|
||||
account?: Account;
|
||||
}>();
|
||||
|
||||
const skeleton = computed(() => !props.account);
|
||||
|
||||
const formattedJoin = computed(() => Intl.DateTimeFormat("en-US", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(new Date(props.account?.created_at ?? 0)));
|
||||
|
||||
const parsedNote = ref("");
|
||||
const parsedFields: Ref<{
|
||||
name: string;
|
||||
value: string;
|
||||
}[]> = ref([]);
|
||||
|
||||
watch(skeleton, async () => {
|
||||
if (skeleton.value) return;
|
||||
parsedNote.value = (await useParsedContent(
|
||||
props.account?.note ?? "",
|
||||
props.account?.emojis ?? [],
|
||||
[],
|
||||
)).value;
|
||||
parsedFields.value = await Promise.all(
|
||||
props.account?.fields.map(async (field) => ({
|
||||
name: await (await useParsedContent(
|
||||
field.name,
|
||||
props.account?.emojis ?? [],
|
||||
[]
|
||||
)).value,
|
||||
value: await (await useParsedContent(
|
||||
field.value,
|
||||
props.account?.emojis ?? [],
|
||||
[]
|
||||
)).value,
|
||||
})) ?? [],
|
||||
);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
</script>
|
||||
42
components/social-elements/users/SmallCard.vue
Normal file
42
components/social-elements/users/SmallCard.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<NuxtLink :href="accountUrl" class="flex flex-row">
|
||||
<Skeleton :enabled="skeleton" shape="rect" class="!h-12 w-12">
|
||||
<div>
|
||||
<img class="h-12 w-12 rounded ring-1 ring-white/5" :src="account?.avatar" alt="" />
|
||||
</div>
|
||||
</Skeleton>
|
||||
<div class="flex flex-col items-start justify-around ml-4 grow overflow-hidden">
|
||||
<div class="flex flex-row items-center justify-between w-full">
|
||||
<div class="font-semibold text-gray-200 line-clamp-1 break-all">
|
||||
<Skeleton :enabled="skeleton" :min-width="90" :max-width="170" shape="rect">
|
||||
{{
|
||||
account?.display_name }}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-gray-400 text-sm line-clamp-1 break-all w-full">
|
||||
<Skeleton :enabled="skeleton" :min-width="130" :max-width="250" shape="rect">
|
||||
@{{
|
||||
account?.acct
|
||||
}}
|
||||
</Skeleton>
|
||||
</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from "~/types/mastodon/account";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
account?: Account;
|
||||
skeleton?: boolean;
|
||||
}>(),
|
||||
{
|
||||
skeleton: false,
|
||||
},
|
||||
);
|
||||
|
||||
const accountUrl = props.account && `/@${props.account.acct}`;
|
||||
</script>
|
||||
49
components/timelines/Public.vue
Normal file
49
components/timelines/Public.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<ClientOnly>
|
||||
|
||||
<SocialElementsNotesNote v-for="note of timeline" :key="note.id" :note="note" />
|
||||
<span ref="skeleton"></span>
|
||||
<SocialElementsNotesNote v-for="index of 5" v-if="!hasReachedEnd" :skeleton="true" />
|
||||
|
||||
<div v-if="hasReachedEnd"
|
||||
class="text-center flex flex-row justify-center items-center py-10 text-gray-400 gap-3">
|
||||
<Icon name="tabler:message-off" class="h-6 w-6" />
|
||||
<span>No more posts, you've seen them all</span>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const client = await useMegalodon();
|
||||
|
||||
const isLoading = ref(true);
|
||||
|
||||
const timelineParameters = ref({});
|
||||
const hasReachedEnd = ref(false);
|
||||
const { timeline, loadNext, loadPrev } = usePublicTimeline(
|
||||
client,
|
||||
timelineParameters,
|
||||
);
|
||||
const skeleton = ref<HTMLSpanElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
useIntersectionObserver(skeleton, async (entries) => {
|
||||
if (
|
||||
entries[0].isIntersecting &&
|
||||
!hasReachedEnd.value &&
|
||||
!isLoading.value
|
||||
) {
|
||||
isLoading.value = true;
|
||||
await loadNext();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watch(timeline, (newTimeline, oldTimeline) => {
|
||||
isLoading.value = false;
|
||||
// If less than NOTES_PER_PAGE statuses are returned, we have reached the end
|
||||
if (newTimeline.length - oldTimeline.length < useConfig().NOTES_PER_PAGE) {
|
||||
hasReachedEnd.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue