mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
feat: ✨ Create new user profile view, refine components, add dropdown to notes
This commit is contained in:
parent
a0d0737683
commit
a17df9fff8
21 changed files with 470 additions and 133 deletions
|
|
@ -9,7 +9,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { InputHTMLAttributes } from 'vue';
|
||||
import type { InputHTMLAttributes } from "vue";
|
||||
|
||||
interface Props extends /* @vue-ignore */ InputHTMLAttributes {
|
||||
isInvalid?: boolean;
|
||||
|
|
|
|||
19
components/buttons/DropdownElement.vue
Normal file
19
components/buttons/DropdownElement.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<ButtonsBase
|
||||
class="bg-white/10 hover:bg-white/20 !text-left flex flex-row gap-x-3 !rounded-none !ring-0 !p-4 sm:!p-3">
|
||||
<Icon :name="icon" class="h-5 w-5 text-gray-200" aria-hidden="true" />
|
||||
<slot />
|
||||
</ButtonsBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ButtonHTMLAttributes } from "vue";
|
||||
|
||||
interface Props extends /* @vue-ignore */ ButtonHTMLAttributes { }
|
||||
|
||||
defineProps<Props & {
|
||||
icon: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ButtonHTMLAttributes } from "vue";
|
||||
|
||||
interface Props extends /* @vue-ignore */ ButtonHTMLAttributes { }
|
||||
interface Props extends /* @vue-ignore */ ButtonHTMLAttributes {}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
|
|
|||
29
components/dropdowns/AdaptiveDropdown.vue
Normal file
29
components/dropdowns/AdaptiveDropdown.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<HeadlessMenu v-slot="{ close }">
|
||||
<slot name="button"></slot>
|
||||
|
||||
<HeadlessMenuItems @click="close" class="fixed inset-0 z-5 bg-black/50">
|
||||
|
||||
</HeadlessMenuItems>
|
||||
|
||||
<transition enter-active-class="transition ease-in duration-100"
|
||||
enter-from-class="transform opacity-0 translate-y-full sm:translate-y-0 scale-95"
|
||||
enter-to-class="transform translate-y-0 opacity-100 scale-100"
|
||||
leave-active-class="transition ease-out duration-75" leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95">
|
||||
<HeadlessMenuItems
|
||||
:class="['z-10 mt-2 rounded overflow-hidden bg-dark-900 shadow-lg ring-1 ring-white/10 focus:outline-none',
|
||||
isSmallScreen ? 'bottom-0 fixed inset-x-0 w-full origin-bottom' : 'absolute right-0 origin-top-right top-full min-w-56']">
|
||||
<div v-if="isSmallScreen" class="w-full bg-white/10 py-2">
|
||||
<div class="rounded-full h-1 bg-gray-400 w-12 mx-auto"></div>
|
||||
</div>
|
||||
<slot name="items"></slot>
|
||||
</HeadlessMenuItems>
|
||||
</transition>
|
||||
</HeadlessMenu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { width } = useWindowSize()
|
||||
const isSmallScreen = computed(() => width.value < 640)
|
||||
</script>
|
||||
|
|
@ -12,29 +12,38 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{
|
||||
enabled: boolean;
|
||||
shape?: "circle" | "rect";
|
||||
type?: "text" | "content";
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
widthUnit?: "px" | "%";
|
||||
class?: string;
|
||||
}>(), {
|
||||
shape: "rect",
|
||||
type: "text",
|
||||
widthUnit: "px",
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
enabled: boolean;
|
||||
shape?: "circle" | "rect";
|
||||
type?: "text" | "content";
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
widthUnit?: "px" | "%";
|
||||
class?: string;
|
||||
}>(),
|
||||
{
|
||||
shape: "rect",
|
||||
type: "text",
|
||||
widthUnit: "px",
|
||||
},
|
||||
);
|
||||
|
||||
const isContent = computed(() => props.type === "content");
|
||||
const isText = computed(() => props.type === "text");
|
||||
const isWidthSpecified = computed(() => props.minWidth && props.maxWidth);
|
||||
const calculatedWidth = computed(() => Math.random() * ((props.maxWidth ?? 0) - (props.minWidth ?? 0)) + (props.minWidth ?? 0));
|
||||
const calculatedWidth = computed(
|
||||
() =>
|
||||
Math.random() * ((props.maxWidth ?? 0) - (props.minWidth ?? 0)) +
|
||||
(props.minWidth ?? 0),
|
||||
);
|
||||
|
||||
const getWidth = (index: number, lines: number) => {
|
||||
if (isWidthSpecified.value) {
|
||||
if (isContent.value)
|
||||
return index === lines ? `${calculatedWidth.value}${props.widthUnit}` : '100%';
|
||||
return index === lines
|
||||
? `${calculatedWidth.value}${props.widthUnit}`
|
||||
: "100%";
|
||||
return `${calculatedWidth.value}${props.widthUnit}`;
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Attachment } from '~/types/mastodon/attachment';
|
||||
import type { Attachment } from "~/types/mastodon/attachment";
|
||||
|
||||
const lightbox = ref(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<span
|
||||
class="shrink break-all rounded bg-pink-700/30 text-pink-200 px-2 py-1 not-prose font-semibold cursor-pointer [&:not(:last-child)]:mr-1 duration-200 hover:bg-pink-600/30">
|
||||
<a :href="`/@${account.acct}`"
|
||||
class="shrink break-all rounded bg-pink-700/80 text-pink-200 px-2 py-1 not-prose font-semibold cursor-pointer [&:not(:last-child)]:mr-1 duration-200 hover:bg-pink-600/30">
|
||||
<img class="h-[1em] w-[1em] rounded ring-1 ring-white/5 inline align-middle mb-1 mr-1" :src="account.avatar"
|
||||
alt="" />
|
||||
{{ account.display_name }}
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Account } from '~/types/mastodon/account';
|
||||
import type { Account } from "~/types/mastodon/account";
|
||||
|
||||
const props = defineProps<{
|
||||
account: Account;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
<Skeleton :enabled="true" v-if="isLoading" :min-width="50" :max-width="100" width-unit="%" shape="rect"
|
||||
type="content">
|
||||
</Skeleton>
|
||||
<div v-else-if="content" class="prose prose-invert prose-a:no-underline" v-html="content">
|
||||
<div v-else-if="content" class="prose prose-invert prose-a:no-underline content" v-html="content">
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div v-if="attachments.length > 0" class="[&:not(:first-child)]:mt-6">
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
</div>
|
||||
<Skeleton class="!h-10 w-full mt-6" :enabled="true" v-if="isLoading"></Skeleton>
|
||||
<div v-else
|
||||
class="mt-6 flex flex-row items-stretch justify-between text-sm h-10 hover:[&>button]:bg-dark-800 [&>button]:duration-200 [&>button]:rounded [&>button]:flex [&>button]:flex-1 [&>button]:flex-row [&>button]:items-center [&>button]:justify-center">
|
||||
class="mt-6 flex flex-row items-stretch relative justify-between text-sm h-10 hover:[&>button]:bg-dark-800 [&>button]:duration-200 [&>button]:rounded [&>button]:flex [&>button]:flex-1 [&>button]:flex-row [&>button]:items-center [&>button]:justify-center">
|
||||
<button>
|
||||
<Icon name="tabler:arrow-back-up" class="h-5 w-5 text-gray-200" aria-hidden="true" />
|
||||
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(note?.replies_count) }}</span>
|
||||
|
|
@ -60,9 +60,28 @@
|
|||
<Icon name="tabler:quote" class="h-5 w-5 text-gray-200" aria-hidden="true" />
|
||||
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(0) }}</span>
|
||||
</button>
|
||||
<button>
|
||||
<Icon name="tabler:dots" class="h-5 w-5 text-gray-200" aria-hidden="true" />
|
||||
</button>
|
||||
<DropdownsAdaptiveDropdown>
|
||||
<template #button>
|
||||
<HeadlessMenuButton>
|
||||
<Icon name="tabler:dots" class="h-5 w-5 text-gray-200" aria-hidden="true" />
|
||||
</HeadlessMenuButton>
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<HeadlessMenuItem>
|
||||
<ButtonsDropdownElement @click="copy(JSON.stringify(note, null, 4))" icon="tabler:code"
|
||||
class="w-full">
|
||||
Copy API
|
||||
Response
|
||||
</ButtonsDropdownElement>
|
||||
</HeadlessMenuItem>
|
||||
<HeadlessMenuItem>
|
||||
<ButtonsDropdownElement @click="note && copy(note.uri)" icon="tabler:code" class="w-full">
|
||||
Copy Link
|
||||
</ButtonsDropdownElement>
|
||||
</HeadlessMenuItem>
|
||||
</template>
|
||||
</DropdownsAdaptiveDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -79,15 +98,53 @@ const props = defineProps<{
|
|||
const isLoading = props.skeleton;
|
||||
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 numberFormat = (number = 0) => new Intl.NumberFormat(undefined, {
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(number);
|
||||
const content = props.note
|
||||
? await useParsedContent(
|
||||
props.note.content,
|
||||
props.note.emojis,
|
||||
mentions.value,
|
||||
)
|
||||
: "";
|
||||
const numberFormat = (number = 0) =>
|
||||
new Intl.NumberFormat(undefined, {
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(number);
|
||||
const attachments = props.note?.media_attachments ?? [];
|
||||
const noteUrl = props.note && `/@${props.note.account.acct}/${props.note.id}`;
|
||||
const accountUrl = props.note && `/@${props.note.account.acct}`;
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content pre:has(code) {
|
||||
word-wrap: normal;
|
||||
background: transparent;
|
||||
background-color: #ffffff0d;
|
||||
border-radius: .25rem;
|
||||
-webkit-hyphens: none;
|
||||
hyphens: none;
|
||||
margin-top: 1rem;
|
||||
overflow-x: auto;
|
||||
padding: .75rem 1rem;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
white-space: pre;
|
||||
word-break: normal;
|
||||
word-spacing: normal;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 0 #0000;
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
--tw-ring-color: hsla(0, 0%, 100%, .1)
|
||||
}
|
||||
|
||||
.content pre code {
|
||||
display: block;
|
||||
padding: 0
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue