refac(frontend): refac ot use new api
jump to
@@ -1,5 +1,5 @@
{ - "name": "frontend", + "name": "@stealth-developers/frontend", "version": "0.0.0", "private": true, "type": "module",@@ -15,12 +15,12 @@ "lint:eslint": "eslint . --fix --cache",
"format": "oxfmt src/" }, "dependencies": { - "discord-api-types": "0.38.3", "pinia": "^3.0.4", "vue": "^3.5.32", "vue-router": "^5.0.4" }, "devDependencies": { + "@stealth-developers/api": "workspace:*", "@tsconfig/node24": "^24.0.4", "@types/node": "^24.12.2", "@vitejs/plugin-vue": "^6.0.6",
@@ -3,7 +3,6 @@ import { RouterView, RouterLink, useRoute } from 'vue-router'
import { useAuthStore } from './stores/auth' import { computed, onMounted, ref } from 'vue' -import { avatar } from './utils/discord' const authStore = useAuthStore() const route = useRoute()@@ -36,9 +35,9 @@ </div>
<div class="user" v-if="user"> <div class="profile-picture"> - <img :src="avatar(user)" alt="Profile Picture" /> + <img :src="user.avatarUrl" alt="Profile Picture" /> </div> - <div class="username">{{ user.nick || user.user.global_name || user.user.username }}</div> + <div class="username">{{ user.name }}</div> </div> </aside> <main class="main">
@@ -1,16 +1,12 @@
<script lang="ts" setup> import type { User } from '@/types' -import { avatar, name } from '@/utils/discord' -const props = defineProps<{ user: User }>() - -const avatarUrl = avatar(props.user) -const userName = name(props.user) +defineProps<{ user: User }>() </script> <template> <div class="user-chip"> - <img :src="avatarUrl" alt="avatar" /> - <span>{{ userName }}</span> + <img :src="user.avatarUrl" alt="avatar" /> + <span>{{ user.name }}</span> </div> </template>
@@ -1,13 +1,10 @@
<script lang="ts" setup> +import { computed } from 'vue' import type { Message, User } from '@/types' -import { avatar, name } from '@/utils/discord' -import { computed } from 'vue' const props = defineProps<{ messages: Message[] }>() const firstMessage = computed(() => props.messages[0]!) -const avatarUrl = computed(() => avatar(firstMessage.value.author)) -const userName = computed(() => name(firstMessage.value.author)) const formatDate = (dateString: string) => { const date = new Date(dateString)@@ -32,21 +29,16 @@ const parts: ParsedContent[] = []
let lastIndex = 0 let match - console.log(msg.mentions) - while ((match = regex.exec(msg.content)) !== null) { if (match.index > lastIndex) { parts.push({ type: 'text', content: msg.content.substring(lastIndex, match.index) }) } const id = match[1] - const user = msg.mentions?.[id!] + const user = msg.mentions.find(u => u.id === id) - if (user) { - parts.push({ type: 'mention', user }) - } else { - parts.push({ type: 'text', content: match[0] }) - } + if (user) parts.push({ type: 'mention', user }) + else parts.push({ type: 'text', content: match[0] }) lastIndex = regex.lastIndex }@@ -61,10 +53,10 @@ </script>
<template> <div v-if="messages.length > 0" :class="['message', firstMessage.authorType]"> - <img :src="avatarUrl" alt="avatar" class="avatar" /> + <img :src="firstMessage.author.avatarUrl" alt="avatar" class="avatar" /> <div class="message-content"> <div class="message-header"> - <span class="author-name">{{ userName }}</span> + <span class="author-name">{{ firstMessage.author.name }}</span> <span class="timestamp">{{ formatDate(firstMessage.createdAt) }}</span> <span v-if="firstMessage.authorType === 'system'" class="badge system">System</span> <span v-else-if="firstMessage.authorType === 'staff'" class="badge staff">Staff</span>@@ -75,7 +67,7 @@ <div class="body" v-if="msg.content">
<template v-for="(part, idx) in parseContent(msg)" :key="idx"> <span v-if="part.type === 'text'">{{ part.content }}</span> <span v-else-if="part.type === 'mention'" class="inline-mention"> - @{{ name(part.user) }} + @{{ part.user.name }} </span> </template> </div>
@@ -1,26 +1,26 @@
-import { ref } from "vue"; -import { defineStore } from "pinia"; -import type { User } from "@/types"; +import { ref } from 'vue' +import { defineStore } from 'pinia' +import type { MeResponse, User } from '@/types' -export const useAuthStore = defineStore("auth", () => { - const isAuthenticated = ref(false); - const isLoading = ref(true); - const user = ref<User | null>(null); +export const useAuthStore = defineStore('auth', () => { + const isAuthenticated = ref(false) + const isLoading = ref(true) + const user = ref<User | null>(null) async function getUser() { try { - const response = await fetch("/api/me"); - const data = await response.json(); - user.value = data.profile; - isAuthenticated.value = true; + const response = await fetch('/api/me') + const data = (await response.json()) as MeResponse + user.value = data + isAuthenticated.value = true } catch {} - isLoading.value = false; + isLoading.value = false } async function waitForAuth() { while (isLoading.value) { - await getUser(); + await getUser() } }@@ -29,5 +29,5 @@ isAuthenticated,
user, getUser, waitForAuth, - }; -}); + } +})
@@ -1,60 +1,1 @@
-import type { APIGuildMember } from 'discord-api-types/v10' -export type User = APIGuildMember - -export type Attachment = { - id: string - ownerType: 'ticket' | 'ticket_message' | 'bug' - ownerId: number - authorId: string | null - fileName: string - contentType: string - size: number - bucket: string - key: string - url: string - uploadedAt: string -} - -export type Ticket = { - id: number - anonymousId: string - ticketNumber: number - guildId: string - channelId: string - - type: 'general' - status: 'open' | 'archived' | 'closed' - topic: string | null - - authorId: User - openedBy: User - addedUsers: string[] - - claimedBy: User | null - claimedAt: string | null - closedAt: string - closedBy: User | 'reporter' | null - closeReason: string | null - privateReason: string | null - - thankedAt: string | null - warnedAt: string | null - - createdAt: string - attachments?: Attachment[] -} - -export type Message = { - id: number - ticketId: number - messageId: string | null - authorId: string - authorType: 'system' | 'staff' | 'reporter' - content: string - createdAt: string - editedAt: string | null - deletedAt: string | null - author: User - attachments?: Attachment[] - mentions?: Record<string, User> -} +export * from '@stealth-developers/api'
@@ -1,14 +0,0 @@
-import { type APIGuildMember, CDNRoutes, ImageFormat, RouteBases } from 'discord-api-types/v10' - -export function avatar(member: APIGuildMember) { - const _avatar = member.avatar ?? member.user.avatar - if (!_avatar) - return 'https://cdn.discordapp.com/icons/895998762630127616/36c6e14d0933eb338826599b632df818.webp?size=80&quality=lossless' - - const parts = [RouteBases.cdn, CDNRoutes.userAvatar(member.user.id, _avatar, ImageFormat.WebP)] - return parts.join('') -} - -export function name(member: APIGuildMember) { - return member.nick ?? member.user.global_name ?? member.user.username -}
@@ -1,24 +1,14 @@
<script setup lang="ts"> import { onMounted, ref } from 'vue' -import type { User } from '@/types' +import type { LeaderboardStat, OverallStatsResponse } from '@/types' import UserChip from '@/components/UserChip.vue' import { useAuthStore } from '@/stores/auth' const authStore = useAuthStore() -type StatsOverall = { - global: { mean: number; median: number; count: number } - hourly: { hour: number; mean: number; median: number; count: number }[] -} +type StatsOverall = OverallStatsResponse -type LeaderboardRow = { - rank: number - user: User - modId: string - mean: number - median: number - count: number -} +type LeaderboardRow = LeaderboardStat const overallStats = ref<StatsOverall | null>(null) const leaderboard = ref<LeaderboardRow[]>([])@@ -122,11 +112,10 @@ <th>Mean TTC</th>
</tr> </thead> <tbody> - <tr v-for="row in leaderboard" :key="row.modId"> + <tr v-for="row in leaderboard" :key="row.user.id"> <td class="user-cell"> <div class="user-content"> - <UserChip v-if="row.user" :user="row.user" /> - <span v-else>{{ row.modId }}</span> + <UserChip :user="row.user" /> </div> </td> <td>{{ row.count }}</td>
@@ -1,7 +1,7 @@
<script setup lang="ts"> -import type { Ticket, Message, User } from '@/types' import { onMounted, ref, computed } from 'vue' import { useRoute } from 'vue-router' +import type { Ticket, Message, User } from '@/types' import UserChip from '@/components/UserChip.vue' import UserMessage from '@/components/UserMessage.vue'@@ -12,11 +12,11 @@ const ticket = ref<Ticket | null>(null)
const messages = ref<Message[]>([]) const users = computed(() => { const messageAuthors = messages.value - .map((msg) => msg.authorId) + .map((msg) => msg.author.id) .filter((id, index, arr) => arr.indexOf(id) === index) return messageAuthors.reduce( (acc, id) => { - acc[id] = messages.value.find((msg) => msg.authorId === id)!.author + acc[id] = messages.value.find((msg) => msg.author.id === id)!.author return acc }, {} as Record<string, User>,@@ -46,14 +46,14 @@ }
const msg: Message = { ...rawMsg, - mentions: mentionMap, + mentions: Object.values(mentionMap), } if (currentGroup.length === 0) { currentGroup.push(msg) } else { const lastMsg = currentGroup[currentGroup.length - 1]! - if (lastMsg.authorId === msg.authorId && lastMsg.authorType === msg.authorType) { + if (lastMsg.author.id === msg.author.id && lastMsg.authorType === msg.authorType) { currentGroup.push(msg) } else { groups.push(currentGroup)@@ -73,34 +73,26 @@ if (!dateString) return '/'
const date = new Date(dateString) return date.toLocaleString() } - -const formatSize = (bytes: number) => { - if (bytes === 0) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] -} </script> <template> <div class="ticket-head" v-if="ticket"> <div class="ticket-heading"> - <h1>Ticket #{{ ticket.id }}</h1> + <h1>Ticket #{{ ticket.number }}</h1> <p :class="['status', ticket.status]">{{ ticket.status }}</p> </div> <div class="ticket-meta-table"> <div class="meta-row"> <div class="meta-label">Opened</div> <div class="meta-value"> - <UserChip :user="ticket.authorId" /> - <span class="meta-date">at {{ formatDate(ticket.createdAt) }}</span> + <UserChip :user="ticket.subject" /> + <span class="meta-date">at {{ formatDate(ticket.openedAt) }}</span> </div> </div> <div class="meta-row" v-if="ticket.closedBy"> <div class="meta-label">Closed</div> <div class="meta-value"> - <UserChip :user="ticket.closedBy === 'reporter' ? ticket.authorId : ticket.closedBy" /> + <UserChip :user="ticket.closedBy" /> <span class="meta-date">at {{ formatDate(ticket.closedAt) }}</span> </div> </div>@@ -114,29 +106,6 @@ </div>
<div class="reason" v-if="ticket.privateReason"> <div class="label">Private Reason</div> <div class="value">{{ ticket.privateReason }}</div> - </div> - </div> - - <div class="ticket-attachments" v-if="ticket.attachments && ticket.attachments.length > 0"> - <div class="label">Attachments</div> - <div class="attachments"> - <template v-for="att in ticket.attachments" :key="att.id"> - <div v-if="att.contentType.startsWith('video/')" class="attachment"> - <video :src="att.url" controls class="attachment-media"></video> - </div> - <a v-else :href="att.url" target="_blank" class="attachment link-attachment"> - <img - v-if="att.contentType.startsWith('image/')" - :src="att.url" - :alt="att.fileName" - class="attachment-media" - /> - <div class="attachment-file" v-else> - <span class="filename">{{ att.fileName }}</span> - <span class="size">{{ formatSize(att.size) }}</span> - </div> - </a> - </template> </div> </div> </div>
@@ -1,41 +1,13 @@
<script setup lang="ts"> import { onMounted, ref } from 'vue' -import type { User } from '@/types' +import type { Ticket } from '@/types' import UserChip from '@/components/UserChip.vue' -type Ticket = { - id: number - anonymousId: string - ticketNumber: number - guildId: string - channelId: string - - type: 'general' - status: 'open' | 'archived' | 'closed' - topic: string | null - - authorId: User - openedBy: User - addedUsers: string[] - - claimedBy: User | null - claimedAt: string | null - closedAt: string - closedBy: User | 'reporter' | null - closeReason: string | null - privateReason: string | null - - thankedAt: string | null - warnedAt: string | null - - createdAt: string -} - const tickets = ref<Ticket[]>([]) onMounted(async () => { const response = await fetch('/api/tickets') const data = await response.json() - tickets.value = data + tickets.value = data.tickets }) const formatTimeAgo = (dateString: string | null) => {@@ -58,13 +30,13 @@ <template>
<h1>Tickets</h1> <div class="ticket-container"> - <div v-for="ticket in tickets" :key="ticket.id" class="ticket"> + <div v-for="ticket in tickets" :key="ticket.number" class="ticket"> <div class="head"> - <span class="id">{{ ticket.ticketNumber }}</span> + <span class="id">{{ ticket.number }}</span> <span :class="['status', ticket.status]">{{ ticket.status }}</span> <div class="time-ago"> <template v-if="ticket.status === 'open'"> - for {{ formatTimeAgo(ticket.createdAt) }} + for {{ formatTimeAgo(ticket.openedAt) }} </template> <template v-else> {{ formatTimeAgo(ticket.closedAt) }} ago </template> </div>@@ -74,7 +46,7 @@ <div class="details">
<div class="keypair"> <span class="key">Opened by</span> <span class="value"> - <UserChip :user="ticket.openedBy" /> + <UserChip :user="ticket.openedBy ?? ticket.subject" /> </span> </div>@@ -83,7 +55,7 @@ <span class="key">Closed by</span>
<span class="value"> <UserChip v-if="ticket.closedBy" - :user="ticket.closedBy === 'reporter' ? ticket.openedBy : ticket.closedBy" + :user="ticket.closedBy" /> <span v-else>/</span> </span>@@ -91,7 +63,7 @@ </div>
</div> <div class="footer"> - <RouterLink :to="`/tickets/${ticket.id}`">View Ticket</RouterLink> + <RouterLink :to="`/tickets/${ticket.number}`">View Ticket</RouterLink> </div> </div> </div>