feat: Create new user profile view, refine components, add dropdown to notes

This commit is contained in:
Jesse Wierzbinski 2024-04-24 20:56:01 -10:00
parent a0d0737683
commit a17df9fff8
No known key found for this signature in database
21 changed files with 470 additions and 133 deletions

View file

@ -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>

View file

@ -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>

View file

@ -56,7 +56,7 @@
</template>
<script setup lang="ts">
const baseUrl = useBaseUrl()
const baseUrl = useBaseUrl();
useServerSeoMeta({
title: "Welcome to Lysand!",

View file

@ -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>

View file

@ -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) : "";

View file

@ -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;

View file

@ -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>