mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 11:39: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
|
|
@ -11,7 +11,7 @@ import { useRoute } from "vue-router";
|
|||
|
||||
const route = useRoute();
|
||||
const client = await useMegalodon();
|
||||
const uuid = (route.params.uuid as string);
|
||||
const uuid = route.params.uuid as string;
|
||||
|
||||
const note = await useNote(client, uuid);
|
||||
</script>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="flex min-h-screen flex-col justify-center py-12 lg:px-8 relative">
|
||||
<div v-if="account" class="mx-auto max-w-lg w-full rounded ring-1 ring-white/10 pb-10">
|
||||
<div
|
||||
class="flex mx-auto max-w-7xl min-h-screen flex-col gap-x-6 md:flex-row justify-center items-start md:py-12 lg:px-8 relative">
|
||||
<div v-if="account" class="w-full rounded ring-1 md:max-w-lg ring-white/10 pb-10">
|
||||
<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>
|
||||
|
|
@ -22,7 +23,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-4 px-4">
|
||||
<div class="prose prose-invert" v-html="account.note"></div>
|
||||
<div class="prose prose-invert" v-html="parsedNote"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center space-x-4 px-4">
|
||||
|
|
@ -46,7 +47,27 @@
|
|||
<span class="text-gray-400">Followers</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="parsedFields.length > 0" class="mt-4 px-4 flex-col flex gap-y-3">
|
||||
<div v-for="field of parsedFields" :key="field.name.value" class="flex flex-col gap-1">
|
||||
<span class="text-pink-500 font-semibold" v-html="field.name.value"></span>
|
||||
<span class="text-gray-200 prose prose-invert break-all" v-html="field.value.value"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<div class="w-full">
|
||||
<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>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -56,11 +77,55 @@ import { useRoute } from "vue-router";
|
|||
const route = useRoute();
|
||||
const client = await useMegalodon();
|
||||
const username = (route.params.username as string).replace("@", "");
|
||||
const id = await useAccountSearch(client, username);
|
||||
|
||||
const account = id ? await useAccount(client, id[0].id) : null;
|
||||
const accounts = await useAccountSearch(client, username);
|
||||
const account =
|
||||
(await accounts?.find((account) => account.acct === username)) ?? null;
|
||||
const formattedJoin = Intl.DateTimeFormat("en-US", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(new Date(account?.created_at ?? 0));
|
||||
|
||||
useServerSeoMeta({
|
||||
title: account?.display_name,
|
||||
description: account?.note,
|
||||
ogImage: account?.avatar,
|
||||
});
|
||||
|
||||
const isLoadingTimeline = ref(true);
|
||||
|
||||
const timelineParameters = ref({});
|
||||
const hasReachedEnd = ref(false);
|
||||
const { timeline, loadNext, loadPrev } = useAccountTimeline(
|
||||
client,
|
||||
account?.id ?? null,
|
||||
timelineParameters,
|
||||
);
|
||||
const skeleton = ref<HTMLSpanElement | null>(null);
|
||||
|
||||
const parsedNote = account ? await useParsedContent(account?.note, account?.emojis, []) : ref("");
|
||||
const parsedFields = await Promise.all(account?.fields.map(async (field) => ({
|
||||
name: await useParsedContent(field.name, account.emojis, []),
|
||||
value: await useParsedContent(field.value, account.emojis, []),
|
||||
})) ?? []);
|
||||
|
||||
onMounted(() => {
|
||||
useIntersectionObserver(skeleton, async (entries) => {
|
||||
if (
|
||||
entries[0].isIntersecting &&
|
||||
!hasReachedEnd.value &&
|
||||
!isLoadingTimeline.value
|
||||
) {
|
||||
isLoadingTimeline.value = true;
|
||||
await loadNext();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watch(timeline, (newTimeline, oldTimeline) => {
|
||||
isLoadingTimeline.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>
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const baseUrl = useBaseUrl()
|
||||
const baseUrl = useBaseUrl();
|
||||
|
||||
useServerSeoMeta({
|
||||
title: "Welcome to Lysand!",
|
||||
|
|
|
|||
|
|
@ -80,15 +80,17 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import { useRoute } from "vue-router";
|
||||
import { z } from "zod";
|
||||
import LoginInput from "../../components/LoginInput.vue";
|
||||
import { toTypedSchema } from '@vee-validate/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = toTypedSchema(z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(3),
|
||||
}));
|
||||
const schema = toTypedSchema(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(3),
|
||||
}),
|
||||
);
|
||||
|
||||
const query = useRoute().query;
|
||||
|
||||
|
|
@ -101,5 +103,5 @@ const error_description = decodeURIComponent(query.error_description as string);
|
|||
|
||||
const validUrlParameters = redirect_uri && response_type && client_id && scope;
|
||||
|
||||
const oauthProviders = await useOAuthProviders()
|
||||
const oauthProviders = await useOAuthProviders();
|
||||
</script>
|
||||
|
|
@ -97,7 +97,9 @@ const url = useRequestURL();
|
|||
const query = useRoute().query;
|
||||
|
||||
const application = "Soapbox"; //query.application;
|
||||
const website = query.website ? decodeURIComponent(query.website as string) : null;
|
||||
const website = query.website
|
||||
? decodeURIComponent(query.website as string)
|
||||
: null;
|
||||
const redirect_uri = query.redirect_uri as string;
|
||||
const client_id = query.client_id;
|
||||
const scope = query.scope ? decodeURIComponent(query.scope as string) : "";
|
||||
|
|
|
|||
|
|
@ -21,17 +21,24 @@ const isLoading = ref(true);
|
|||
|
||||
const timelineParameters = ref({});
|
||||
const hasReachedEnd = ref(false);
|
||||
const { timeline, loadNext, loadPrev } = usePublicTimeline(client, timelineParameters);
|
||||
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) {
|
||||
if (
|
||||
entries[0].isIntersecting &&
|
||||
!hasReachedEnd.value &&
|
||||
!isLoading.value
|
||||
) {
|
||||
isLoading.value = true;
|
||||
await loadNext();
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
watch(timeline, (newTimeline, oldTimeline) => {
|
||||
isLoading.value = false;
|
||||
|
|
|
|||
|
|
@ -84,28 +84,35 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LoginInput from "../../components/LoginInput.vue";
|
||||
import { toTypedSchema } from '@vee-validate/zod';
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import type { AxiosError } from "axios";
|
||||
import { z } from 'zod';
|
||||
import { z } from "zod";
|
||||
import LoginInput from "../../components/LoginInput.vue";
|
||||
|
||||
const schema = toTypedSchema(z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(3),
|
||||
password2: z.string().min(3),
|
||||
username: z.string().min(3).regex(/^[a-z0-9_]+$/),
|
||||
reason: z.string().optional(),
|
||||
tos: z.string(),
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.password !== data.password2) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, 'password2'],
|
||||
code: "custom",
|
||||
message: 'Passwords do not match',
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}));
|
||||
const schema = toTypedSchema(
|
||||
z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(3),
|
||||
password2: z.string().min(3),
|
||||
username: z
|
||||
.string()
|
||||
.min(3)
|
||||
.regex(/^[a-z0-9_]+$/),
|
||||
reason: z.string().optional(),
|
||||
tos: z.string(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.password !== data.password2) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, "password2"],
|
||||
code: "custom",
|
||||
message: "Passwords do not match",
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
);
|
||||
|
||||
const client = await useMegalodon();
|
||||
const instance = await useInstance(client);
|
||||
|
|
@ -126,7 +133,15 @@ const register = (result: {
|
|||
reason: string;
|
||||
}) => {
|
||||
isLoading.value = true;
|
||||
client.registerAccount(result.username, result.email, result.password, true, "en", result.reason || "Empty reason")
|
||||
client
|
||||
.registerAccount(
|
||||
result.username,
|
||||
result.email,
|
||||
result.password,
|
||||
true,
|
||||
"en",
|
||||
result.reason || "Empty reason",
|
||||
)
|
||||
.then(async (res) => {
|
||||
navigateTo("/register/success");
|
||||
})
|
||||
|
|
@ -135,8 +150,9 @@ const register = (result: {
|
|||
// @ts-ignore
|
||||
errors.value = error.response?.data || {};
|
||||
console.error(err);
|
||||
}).finally(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue