✨ User personal page
This commit is contained in:
		| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="text-xs text-center opacity-80"> |   <div class="text-xs text-center opacity-80"> | ||||||
|     <p>Copyright © {{ new Date().getFullYear() }} Solsynth</p> |     <p>Copyright © {{ new Date().getFullYear() }} Solsynth</p> | ||||||
|     <p>Powered by <a class="underline" href="#">Hydrogen.Identity</a></p> |     <p>Powered by <a class="underline" href="#">Hydrogen</a></p> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|   | |||||||
| @@ -1,12 +1,14 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="flex gap-3"> |   <div class="flex gap-3"> | ||||||
|     <div> |     <div> | ||||||
|       <v-avatar |       <router-link :to="{ name: 'users.page', params: { alias: props.item?.author.name ?? 'ghost' } }"> | ||||||
|         color="grey-lighten-2" |         <v-avatar | ||||||
|         icon="mdi-account-circle" |           color="grey-lighten-2" | ||||||
|         class="rounded-card" |           icon="mdi-account-circle" | ||||||
|         :image="props.item?.author.avatar" |           class="rounded-card" | ||||||
|       /> |           :image="props.item?.author.avatar" | ||||||
|  |         /> | ||||||
|  |       </router-link> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="flex-grow-1"> |     <div class="flex-grow-1"> | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|     <v-infinite-scroll :items="props.posts" :onLoad="props.loader"> |     <v-infinite-scroll :items="props.posts" :onLoad="props.loader"> | ||||||
|       <template v-for="(item, idx) in props.posts" :key="item.id"> |       <template v-for="(item, idx) in props.posts" :key="item.id"> | ||||||
|         <div class="mb-3 px-[8px]"> |         <div class="mb-3 px-[8px]"> | ||||||
|           <v-card> |           <v-card :variant="props.variant ?? 'elevated'"> | ||||||
|             <template #text> |             <template #text> | ||||||
|               <post-item brief :item="item" @update:item="(val) => updateItem(idx, val)" /> |               <post-item brief :item="item" @update:item="(val) => updateItem(idx, val)" /> | ||||||
|             </template> |             </template> | ||||||
| @@ -17,7 +17,7 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import PostItem from "@/components/posts/PostItem.vue" | import PostItem from "@/components/posts/PostItem.vue" | ||||||
|  |  | ||||||
| const props = defineProps<{ posts: any[]; loader: (opts: any) => Promise<any> }>() | const props = defineProps<{ variant?: string, posts: any[]; loader: (opts: any) => Promise<any> }>() | ||||||
| const emits = defineEmits(["update:posts"]) | const emits = defineEmits(["update:posts"]) | ||||||
|  |  | ||||||
| function updateItem(idx: number, data: any) { | function updateItem(idx: number, data: any) { | ||||||
|   | |||||||
| @@ -34,11 +34,14 @@ | |||||||
|         </div> |         </div> | ||||||
|       </v-toolbar> |       </v-toolbar> | ||||||
|  |  | ||||||
|       <v-list class="flex-grow-1" :opened="drawerMini ? [] : expanded" @update:opened="(val) => expanded = val"> |       <div class="flex-grow-1"> | ||||||
|         <channel-list /> |         <v-list density="compact" :opened="drawerMini ? [] : expanded" | ||||||
|         <v-divider class="border-opacity-75 my-2" /> |                 @update:opened="(val) => expanded = val"> | ||||||
|         <realm-list /> |           <channel-list /> | ||||||
|       </v-list> |           <v-divider class="border-opacity-75 my-2" /> | ||||||
|  |           <realm-list /> | ||||||
|  |         </v-list> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|       <!-- User info --> |       <!-- User info --> | ||||||
|       <v-list |       <v-list | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								src/router/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/router/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | export const authRouter = [ | ||||||
|  |   { | ||||||
|  |     path: "sign-in", | ||||||
|  |     name: "auth.sign-in", | ||||||
|  |     component: () => import("@/views/auth/sign-in.vue"), | ||||||
|  |     meta: { public: true, title: "Sign in" } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "sign-up", | ||||||
|  |     name: "auth.sign-up", | ||||||
|  |     component: () => import("@/views/auth/sign-up.vue"), | ||||||
|  |     meta: { public: true, title: "Sign up" } | ||||||
|  |   } | ||||||
|  | ] | ||||||
							
								
								
									
										7
									
								
								src/router/chat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/router/chat.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | export const chatRouter = [ | ||||||
|  |   { | ||||||
|  |     path: "", | ||||||
|  |     name: "chat.channel", | ||||||
|  |     component: () => import("@/views/chat/page.vue"), | ||||||
|  |   } | ||||||
|  | ] | ||||||
| @@ -3,6 +3,10 @@ import MasterLayout from "@/layouts/master.vue" | |||||||
|  |  | ||||||
| import nprogress from "nprogress"; | import nprogress from "nprogress"; | ||||||
|  |  | ||||||
|  | import { authRouter } from "@/router/auth" | ||||||
|  | import { plazaRouter } from "@/router/plaza" | ||||||
|  | import { chatRouter } from "@/router/chat" | ||||||
|  |  | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|   history: createWebHistory(import.meta.env.BASE_URL), |   history: createWebHistory(import.meta.env.BASE_URL), | ||||||
|   routes: [ |   routes: [ | ||||||
| @@ -10,63 +14,27 @@ const router = createRouter({ | |||||||
|       path: "/", |       path: "/", | ||||||
|       component: MasterLayout, |       component: MasterLayout, | ||||||
|       children: [ |       children: [ | ||||||
|  |         { | ||||||
|  |           path: "/u/:alias", | ||||||
|  |           name: "users.page", | ||||||
|  |           component: () => import("@/views/users/page.vue") | ||||||
|  |         }, | ||||||
|  |  | ||||||
|         { |         { | ||||||
|           path: "/", |           path: "/", | ||||||
|           component: () => import("@/layouts/plaza.vue"), |           component: () => import("@/layouts/plaza.vue"), | ||||||
|           children: [ |           children: plazaRouter, | ||||||
|             { |  | ||||||
|               path: "/", |  | ||||||
|               name: "explore", |  | ||||||
|               component: () => import("@/views/explore.vue") |  | ||||||
|             }, |  | ||||||
|  |  | ||||||
|             { |  | ||||||
|               path: "/p/moments/:alias", |  | ||||||
|               name: "posts.details.moments", |  | ||||||
|               component: () => import("@/views/posts/moments.vue") |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               path: "/p/articles/:alias", |  | ||||||
|               name: "posts.details.articles", |  | ||||||
|               component: () => import("@/views/posts/articles.vue") |  | ||||||
|             }, |  | ||||||
|  |  | ||||||
|             { |  | ||||||
|               path: "/realms/:realmId", |  | ||||||
|               name: "realms.page", |  | ||||||
|               component: () => import("@/views/realms/page.vue") |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         { |         { | ||||||
|           path: "/chat/:channel", |           path: "/chat/:channel", | ||||||
|           component: () => import("@/layouts/chat.vue"), |           component: () => import("@/layouts/chat.vue"), | ||||||
|           children: [ |           children: chatRouter, | ||||||
|             { |  | ||||||
|               path: "", |  | ||||||
|               name: "chat.channel", |  | ||||||
|               component: () => import("@/views/chat/page.vue"), |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         { |         { | ||||||
|           path: "/auth", |           path: "/auth", | ||||||
|           children: [ |           children: authRouter, | ||||||
|             { |  | ||||||
|               path: "sign-in", |  | ||||||
|               name: "auth.sign-in", |  | ||||||
|               component: () => import("@/views/auth/sign-in.vue"), |  | ||||||
|               meta: { public: true, title: "Sign in" } |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               path: "sign-up", |  | ||||||
|               name: "auth.sign-up", |  | ||||||
|               component: () => import("@/views/auth/sign-up.vue"), |  | ||||||
|               meta: { public: true, title: "Sign up" } |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								src/router/plaza.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/router/plaza.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | export const plazaRouter = [ | ||||||
|  |   { | ||||||
|  |     path: "/", | ||||||
|  |     name: "explore", | ||||||
|  |     component: () => import("@/views/explore.vue") | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   { | ||||||
|  |     path: "/p/moments/:alias", | ||||||
|  |     name: "posts.details.moments", | ||||||
|  |     component: () => import("@/views/posts/moments.vue") | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/p/articles/:alias", | ||||||
|  |     name: "posts.details.articles", | ||||||
|  |     component: () => import("@/views/posts/articles.vue") | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   { | ||||||
|  |     path: "/realms/:realmId", | ||||||
|  |     name: "realms.page", | ||||||
|  |     component: () => import("@/views/realms/page.vue") | ||||||
|  |   } | ||||||
|  | ] | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container class="flex max-md:flex-col gap-3 overflow-auto h-auto no-scrollbar"> |   <v-container class="wrapper overflow-auto h-auto no-scrollbar"> | ||||||
|     <div class="timeline flex-grow-1 mt-[-16px]"> |     <div class="timeline mt-[-16px]"> | ||||||
|       <post-list v-model:posts="posts" :loader="readMore" /> |       <post-list v-model:posts="posts" :loader="readMore" /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="aside w-full h-full md:min-w-[320px] md:max-w-[320px] max-md:order-first"> |     <div class="aside w-full max-md:order-first"> | ||||||
|       <v-card title="Categories"> |       <v-card title="Categories"> | ||||||
|         <v-list density="compact"> |         <v-list density="compact"> | ||||||
|           <v-list-item title="All" prepend-icon="mdi-apps" active></v-list-item> |           <v-list-item title="All" prepend-icon="mdi-apps" active></v-list-item> | ||||||
| @@ -28,10 +28,10 @@ async function readPosts() { | |||||||
|   const res = await request( |   const res = await request( | ||||||
|     "interactive", |     "interactive", | ||||||
|     `/api/feed?` + |     `/api/feed?` + | ||||||
|       new URLSearchParams({ |     new URLSearchParams({ | ||||||
|         take: pagination.pageSize.toString(), |       take: pagination.pageSize.toString(), | ||||||
|         offset: ((pagination.page - 1) * pagination.pageSize).toString() |       offset: ((pagination.page - 1) * pagination.pageSize).toString() | ||||||
|       }) |     }) | ||||||
|   ) |   ) | ||||||
|   if (res.status !== 200) { |   if (res.status !== 200) { | ||||||
|     error.value = await res.text() |     error.value = await res.text() | ||||||
| @@ -62,3 +62,18 @@ async function readMore({ done }: any) { | |||||||
|  |  | ||||||
| readPosts() | readPosts() | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .wrapper { | ||||||
|  |   display: grid; | ||||||
|  |   grid-template-columns: 2fr 1fr; | ||||||
|  |  | ||||||
|  |   gap: 0.75rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .wrapper { | ||||||
|  |     grid-template-columns: 1fr; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -18,7 +18,12 @@ | |||||||
|             <v-divider class="my-5 mx-[-16px] border-opacity-50" /> |             <v-divider class="my-5 mx-[-16px] border-opacity-50" /> | ||||||
|  |  | ||||||
|             <div class="px-3 text-xs opacity-80 flex gap-1"> |             <div class="px-3 text-xs opacity-80 flex gap-1"> | ||||||
|               <span>Written by {{ post?.author?.nick }}</span> |               <router-link | ||||||
|  |                 class="underline" | ||||||
|  |                 :to="{ name: 'users.page', params: { alias: post?.author.name ?? 'ghost' } }" | ||||||
|  |               > | ||||||
|  |                 <span>Written by {{ post?.author?.nick }}</span> | ||||||
|  |               </router-link> | ||||||
|               <span>·</span> |               <span>·</span> | ||||||
|               <span>Published at {{ new Date(post?.created_at).toLocaleString() }}</span> |               <span>Published at {{ new Date(post?.created_at).toLocaleString() }}</span> | ||||||
|             </div> |             </div> | ||||||
|   | |||||||
| @@ -6,12 +6,14 @@ | |||||||
|           <v-card-text> |           <v-card-text> | ||||||
|             <div class="flex justify-between px-3"> |             <div class="flex justify-between px-3"> | ||||||
|               <div class="flex gap-1"> |               <div class="flex gap-1"> | ||||||
|                 <v-avatar |                 <router-link :to="{ name: 'users.page', params: { alias: post?.author.name ?? 'ghost' } }"> | ||||||
|                   color="grey-lighten-2" |                   <v-avatar | ||||||
|                   icon="mdi-account-circle" |                     color="grey-lighten-2" | ||||||
|                   class="rounded-card me-2" |                     icon="mdi-account-circle" | ||||||
|                   :image="post?.author.avatar" |                     class="rounded-card me-2" | ||||||
|                 /> |                     :image="post?.author.avatar" | ||||||
|  |                   /> | ||||||
|  |                 </router-link> | ||||||
|  |  | ||||||
|                 <div> |                 <div> | ||||||
|                   <p class="font-bold">{{ post?.author.nick }}</p> |                   <p class="font-bold">{{ post?.author.nick }}</p> | ||||||
| @@ -26,6 +28,7 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|  |  | ||||||
|             <v-divider class="mb-5 mt-3.5 mx-[-16px] border-opacity-50" /> |             <v-divider class="mb-5 mt-3.5 mx-[-16px] border-opacity-50" /> | ||||||
|  |  | ||||||
|             <div class="px-3"> |             <div class="px-3"> | ||||||
|   | |||||||
							
								
								
									
										203
									
								
								src/views/users/page.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/views/users/page.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container class="wrapper overflow-auto h-auto no-scrollbar"> | ||||||
|  |     <div class="name-card col-span-2"> | ||||||
|  |       <v-card class="w-full"> | ||||||
|  |         <v-img v-if="accountBanner" cover height="280px" :src="accountBanner" /> | ||||||
|  |  | ||||||
|  |         <v-card-text class="flex px-5 gap-1"> | ||||||
|  |           <v-avatar | ||||||
|  |             color="grey-lighten-2" | ||||||
|  |             icon="mdi-account-circle" | ||||||
|  |             class="rounded-card me-2" | ||||||
|  |             :image="accountPicture ?? ''" | ||||||
|  |           /> | ||||||
|  |  | ||||||
|  |           <div> | ||||||
|  |             <div class="flex items-center gap-1"> | ||||||
|  |               <h2 class="text-lg font-medium">{{ metadata?.nick }}</h2> | ||||||
|  |               <span class="text-sm opacity-80">@{{ metadata?.name }}</span> | ||||||
|  |             </div> | ||||||
|  |             <p v-if="metadata?.description" class="mt-[-4px]">{{ metadata?.description }}</p> | ||||||
|  |             <p v-else class="mt-[-4px] italic">No description yet.</p> | ||||||
|  |           </div> | ||||||
|  |         </v-card-text> | ||||||
|  |       </v-card> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <v-card class="browser h-fit"> | ||||||
|  |       <v-tabs v-model="tab" align-tabs="center" bg-color="grey-lighten-4"> | ||||||
|  |         <v-tab value="page">Personal Page</v-tab> | ||||||
|  |         <v-tab value="timeline">Timeline</v-tab> | ||||||
|  |       </v-tabs> | ||||||
|  |  | ||||||
|  |       <v-card-text class="content"> | ||||||
|  |         <v-window v-model="tab" content-class="px-5"> | ||||||
|  |           <v-window-item value="page"> | ||||||
|  |             <div class="px-3"> | ||||||
|  |               <article v-if="page?.content" class="prose max-w-none" v-html="parseContent(page?.content)" /> | ||||||
|  |               <article v-else> | ||||||
|  |                 <v-alert variant="tonal" type="info"> | ||||||
|  |                   The user didn't customize its personal page. | ||||||
|  |                 </v-alert> | ||||||
|  |               </article> | ||||||
|  |             </div> | ||||||
|  |           </v-window-item> | ||||||
|  |  | ||||||
|  |           <v-window-item value="timeline"> | ||||||
|  |             <post-list class="mt-[-16px]" variant="outlined" :loader="readMore" v-model:posts="posts" /> | ||||||
|  |           </v-window-item> | ||||||
|  |         </v-window> | ||||||
|  |       </v-card-text> | ||||||
|  |     </v-card> | ||||||
|  |  | ||||||
|  |     <div class="aside h-fit max-md:order-first"> | ||||||
|  |       <v-card prepend-icon="mdi-account-details" title="Bio"> | ||||||
|  |         <v-card-text class="flex flex-col gap-2.5"> | ||||||
|  |           <div> | ||||||
|  |             <h3 class="font-bold">Power level</h3> | ||||||
|  |             <v-chip :color="parsePowerLevel(metadata?.power_level).color" size="small"> | ||||||
|  |               <span>{{ parsePowerLevel(metadata?.power_level).title }}</span> | ||||||
|  |               <span> · </span> | ||||||
|  |               <span class="font-mono">{{ metadata?.power_level }}</span> | ||||||
|  |             </v-chip> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <div> | ||||||
|  |             <h3 class="font-bold">Joined at</h3> | ||||||
|  |             <p>{{ new Date(metadata?.created_at).toLocaleString() }}</p> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <div> | ||||||
|  |             <h3 class="font-bold">UID</h3> | ||||||
|  |             <p class="text-mono opacity-90">#{{ metadata?.id.toString().padStart(12, '0') }}</p> | ||||||
|  |           </div> | ||||||
|  |         </v-card-text> | ||||||
|  |       </v-card> | ||||||
|  |     </div> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, reactive, ref } from "vue" | ||||||
|  | import { buildRequestUrl, request } from "@/scripts/request" | ||||||
|  | import { useRoute } from "vue-router" | ||||||
|  | import PostList from "@/components/posts/PostList.vue" | ||||||
|  | import { parse } from "marked" | ||||||
|  | import Articles from "@/views/posts/articles.vue" | ||||||
|  |  | ||||||
|  | const route = useRoute() | ||||||
|  |  | ||||||
|  | const error = ref<null | string>(null) | ||||||
|  | const loading = ref(false) | ||||||
|  |  | ||||||
|  | const tab = ref("page") | ||||||
|  |  | ||||||
|  | const pagination = reactive({ page: 1, pageSize: 5, total: 0 }) | ||||||
|  |  | ||||||
|  | const metadata = ref<any>(null) | ||||||
|  | const page = ref<any>(null) | ||||||
|  | const posts = ref<any[]>([]) | ||||||
|  |  | ||||||
|  | const accountPicture = computed(() => metadata.value?.avatar ? | ||||||
|  |   buildRequestUrl("identity", `/api/avatar/${metadata.value?.avatar}`) : | ||||||
|  |   null | ||||||
|  | ) | ||||||
|  | const accountBanner = computed(() => metadata.value?.banner ? | ||||||
|  |   buildRequestUrl("identity", `/api/avatar/${metadata.value?.banner}`) : | ||||||
|  |   null | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | async function readMetadata() { | ||||||
|  |   loading.value = true | ||||||
|  |   const res = await request("identity", `/api/users/${route.params.alias}`) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     metadata.value = await res.json() | ||||||
|  |   } | ||||||
|  |   loading.value = false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function readPage() { | ||||||
|  |   loading.value = true | ||||||
|  |   const res = await request("identity", `/api/users/${route.params.alias}/page`) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     page.value = await res.json() | ||||||
|  |   } | ||||||
|  |   loading.value = false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function readPosts() { | ||||||
|  |   const res = await request( | ||||||
|  |     "interactive", | ||||||
|  |     `/api/feed?` + | ||||||
|  |     new URLSearchParams({ | ||||||
|  |       take: pagination.pageSize.toString(), | ||||||
|  |       offset: ((pagination.page - 1) * pagination.pageSize).toString(), | ||||||
|  |       authorId: route.params.alias as string | ||||||
|  |     }) | ||||||
|  |   ) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     error.value = null | ||||||
|  |     const data = await res.json() | ||||||
|  |     pagination.total = data["count"] | ||||||
|  |     posts.value.push(...(data["data"] ?? [])) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function readMore({ done }: any) { | ||||||
|  |   // Reach the end of data | ||||||
|  |   if (pagination.total <= pagination.page * pagination.pageSize) { | ||||||
|  |     done("empty") | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pagination.page++ | ||||||
|  |   await readPosts() | ||||||
|  |  | ||||||
|  |   if (error.value != null) done("error") | ||||||
|  |   else { | ||||||
|  |     if (pagination.total > 0) done("ok") | ||||||
|  |     else done("empty") | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Promise.all([readMetadata(), readPage(), readPosts()]) | ||||||
|  |  | ||||||
|  | function parsePowerLevel(level: number): { color: string, title: string } { | ||||||
|  |   if (level < 50) { | ||||||
|  |     return { color: "green", title: "User" } | ||||||
|  |   } else if (level < 100) { | ||||||
|  |     return { color: "orange", title: "Moderator" } | ||||||
|  |   } else { | ||||||
|  |     return { color: "red", title: "Administrator" } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function parseContent(src: string): string { | ||||||
|  |   return parse(src) as string | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .wrapper { | ||||||
|  |   display: grid; | ||||||
|  |   grid-template-columns: 2fr 1fr; | ||||||
|  |  | ||||||
|  |   gap: 0.75rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .wrapper { | ||||||
|  |     grid-template-columns: 1fr; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .rounded-card { | ||||||
|  |   border-radius: 8px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
		Reference in New Issue
	
	Block a user