all repos — stealth-developers @ 9c88eb5426cd3c1798852ca45544c4486883809c

web: parity w old version
vi did:web:vt3e.cat
Tue, 30 Jun 2026 21:47:51 +0100
commit

9c88eb5426cd3c1798852ca45544c4486883809c

parent

05020a60748d4790c6826660a9b0d87a3aa2fb99

M apps/web/src/App.vueapps/web/src/App.vue

@@ -1,49 +1,113 @@

<script setup lang="ts"> -import { onMounted } from "vue"; -import { computed } from "vue"; -import { RouterView } from "vue-router"; +import { computed, onMounted, ref } from "vue"; +import { RouterLink, RouterView, useRoute, useRouter } from "vue-router"; +import UserChip from "./components/UserChip.vue"; import { useApiStore } from "./stores/api"; + const api = useApiStore(); +const route = useRoute(); +const router = useRouter(); -const actors = computed(() => api.actors); +const guildActors = computed(() => api.actors); +const firstActor = computed(() => Object.values(guildActors.value)[0]); + +const isSidebarOpen = ref(false); +const toggleSidebar = () => { + isSidebarOpen.value = !isSidebarOpen.value; +}; +const closeSidebar = () => { + isSidebarOpen.value = false; +}; + +const isGuildActive = (discordId: string) => { + return route.params.guildId === discordId; +}; + +const isTicketsActive = (discordId: string) => { + return route.path.startsWith(`/guilds/${discordId}/tickets`); +}; + +const isStatsActive = (discordId: string) => { + return route.path === `/guilds/${discordId}`; +}; onMounted(async () => { - await api.getUser(); + await api.waitForAuth(); + if (api.isAuthenticated) { + const firstGuild = Object.values(api.actors)[0]; + if (firstGuild && window.location.pathname === "/") + router.push(`/guilds/${firstGuild.guild!.discordId}/tickets`); + } }); </script> <template> - <div class="layout"> - <div class="sidebar-overlay"></div> + <div class="layout" :class="{ 'sidebar-open': isSidebarOpen }"> + <div class="sidebar-overlay" @click="closeSidebar"></div> + <aside class="sidebar"> - <div class="links"> - <div class="guild-group" :key="actor.actorId" v-for="actor in actors"> - <template v-if="actor.guild"> - <RouterLink :to="`/guilds/${actor.guild.discordId}`" class="guild-link"> - <div class="icon"> - <img v-if="actor.guild?.icon" :src="actor.guild.icon" alt="" /> - <span v-else class="fallback-text"> - {{ actor.guild?.name?.charAt(0) || "G" }} - </span> - </div> - <span class="guild-name">{{ actor.guild?.name }}</span> - </RouterLink> + <div class="link-group"> + <div + class="guild-group" + :class="{ 'is-active-guild': isGuildActive(actor.guild!.discordId) }" + :key="actor.actorId" + v-for="actor in guildActors" + > + <RouterLink + :to="`/guilds/${actor.guild.discordId}`" + class="guild-link" + :class="{ 'router-link-active': isGuildActive(actor.guild.discordId) }" + v-if="actor.guild" + > + <div class="icon"> + <img v-if="actor.guild.icon" :src="actor.guild.icon" alt="" /> + <span v-else class="fallback-text"> + {{ actor.guild.name.charAt(0) || "G" }} + </span> + </div> + <span class="guild-name">{{ actor.guild.name }}</span> + <svg + class="chevron-icon" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2" + > + <polyline points="9 18 15 12 9 6"></polyline> + </svg> + </RouterLink> - <div class="sub-links-wrapper"> + <div class="sub-links-wrapper" v-if="actor.guild"> + <div class="sub-links-container"> <div class="sub-links"> - <RouterLink :to="`/guilds/${actor.guild.discordId}`"> + <RouterLink + :to="`/guilds/${actor.guild.discordId}`" + :class="{ 'router-link-exact-active': isStatsActive(actor.guild.discordId) }" + @click="closeSidebar" + > <div class="inner">Stats</div> </RouterLink> - <RouterLink :to="`/guilds/${actor.guild.discordId}/tickets`"> + <RouterLink + :to="`/guilds/${actor.guild.discordId}/tickets`" + :class="{ 'router-link-exact-active': isTicketsActive(actor.guild.discordId) }" + @click="closeSidebar" + > <div class="inner">Tickets</div> </RouterLink> </div> </div> - </template> + </div> </div> </div> + + <div class="link-group footer-links"> + <RouterLink to="/privacy" @click="closeSidebar"> Privacy Policy </RouterLink> + <RouterLink to="/tos" @click="closeSidebar"> Terms of Service </RouterLink> + <UserChip class="profile" v-if="firstActor" :user="firstActor" /> + </div> </aside> + <main class="main"> <RouterView v-slot="{ Component }"> <KeepAlive include="TicketsView">

@@ -51,6 +115,17 @@ <component :is="Component" />

</KeepAlive> </RouterView> </main> + + <header class="mobile-header" @click="toggleSidebar"> + <button class="menu-toggle" @click.stop="toggleSidebar" aria-label="Toggle navigation menu"> + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> + <line x1="4" y1="12" x2="20" y2="12"></line> + <line x1="4" y1="6" x2="20" y2="6"></line> + <line x1="4" y1="18" x2="20" y2="18"></line> + </svg> + </button> + <span class="mobile-logo">{{ route.name }}</span> + </header> </div> </template>

@@ -64,6 +139,7 @@

height: 100dvh; padding: 0.5rem; gap: 0.5rem; + overflow: hidden; .sidebar { flex: 0 0 auto;

@@ -89,18 +165,72 @@ overflow-y: auto;

} } -.links { +.mobile-header { + display: none; + align-items: center; + padding: 1rem 1rem; + background-color: hsla(var(--surface0)); + border-radius: 5rem; + gap: 1rem; + flex-shrink: 0; + + .menu-toggle { + background: none; + border: none; + color: hsla(var(--text)); + cursor: pointer; + padding: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 1.5rem; + height: 1.5rem; + } + } + + .mobile-logo { + font-weight: 700; + font-size: 1.1rem; + color: hsla(var(--text)); + } +} + +.link-group { display: flex; flex-direction: column; align-items: stretch; - gap: 0.5rem; + gap: 0.25rem; + + &.footer-links { + border-top: 1px solid hsla(var(--surface0) / 0.4); + padding-top: 1rem; + + a, + .profile { + font-size: 0.85rem; + color: hsla(var(--subtext0)); + padding: 0.75rem 1rem; + font-weight: 800; + border-radius: 5rem; + + &:hover { + color: hsla(var(--text)); + background-color: hsla(var(--surface0) / 0.4); + } + + &.router-link-active { + color: hsla(var(--crust)); + background-color: hsla(var(--accent) / 1); + } + } + } .guild-group { display: flex; flex-direction: column; align-items: stretch; - padding: 0.25rem; - border-radius: 1rem; .sub-links-wrapper { display: grid;

@@ -109,7 +239,7 @@ transition: grid-template-rows var(--transition);

overflow: hidden; } - &:has(.router-link-active) { + &.is-active-guild { .sub-links-wrapper { grid-template-rows: 1fr; }

@@ -117,26 +247,39 @@

.guild-link { background-color: hsla(var(--accent)); color: hsla(var(--crust)); + + .fallback-text { + color: hsla(var(--crust)); + } + + .chevron-icon { + transform: rotate(90deg); + color: hsla(var(--crust)); + } } + + .sub-links { + margin-top: 0.25rem; + margin-bottom: 0.25rem; + } + } + + .sub-links-container { + min-height: 0; } .sub-links { - min-height: 0; display: flex; flex-direction: column; align-items: stretch; + border-radius: 1rem; + overflow: hidden; + padding: 0.25rem; gap: 0.25rem; - margin-left: 1.35rem; - padding-left: 0.75rem; - border-left: 2px solid hsla(var(--surface0) / 0.6); - margin-top: 0.25rem; - margin-bottom: 0.25rem; - transition: border-color var(--transition); - - &:has(.router-link-exact-active) { - border-left-color: hsla(var(--accent) / 0.5); - } + margin-top: 0px; + margin-bottom: 0px; + transition: margin var(--transition); } }

@@ -150,16 +293,16 @@ }

.guild-link { gap: 0.5rem; - padding: 0.5rem; - border-radius: 0.75rem; + padding: 0.25rem; + border-radius: 5rem; font-weight: 600; font-size: 0.95rem; color: hsla(var(--subtext1)); .icon { - width: 1.75rem; - height: 1.75rem; - border-radius: 0.5rem; + width: 2rem; + height: 2rem; + border-radius: 50%; overflow: hidden; flex-shrink: 0; display: flex;

@@ -192,9 +335,24 @@ overflow: hidden;

text-overflow: ellipsis; } + .chevron-icon { + width: 1rem; + height: 1rem; + margin-left: auto; + flex-shrink: 0; + color: hsla(var(--subtext1) / 0.6); + transition: + transform var(--transition, 0.2s ease), + color var(--transition, 0.2s ease); + } + &:hover { color: hsla(var(--text)); background-color: hsla(var(--surface0) / 0.4); + + .chevron-icon { + color: hsla(var(--text)); + } } &:active { background-color: hsla(var(--surface0) / 0.3);

@@ -208,11 +366,9 @@ }

.sub-links a { background-color: transparent; - margin-left: 0.5rem; - transition: color var(--transition); .inner { - padding: 0.3rem 1.25rem; + padding: 0.5rem 1.25rem; font-weight: 500; color: hsla(var(--subtext0)); border-radius: 5rem;

@@ -232,6 +388,51 @@ &.router-link-exact-active .inner {

background-color: hsla(var(--accent) / 1); color: hsla(var(--on-accent)); font-weight: 600; + } + } +} + +@media (max-width: 768px) { + .layout { + flex-direction: column; + padding: 0.5rem; + gap: 0.5rem; + + .mobile-header { + display: flex; + } + + .sidebar { + position: fixed; + top: 0.5rem; + left: -280px; + bottom: 0.5rem; + z-index: 100; + justify-content: flex-end; + background-color: hsla(var(--crust)); + border-radius: 1.5rem; + padding: 0.25rem; + height: auto; + } + + &.sidebar-open { + .sidebar { + left: 0.5rem; + } + + .sidebar-overlay { + display: block; + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + z-index: 99; + transition: 0.2s; + + @starting-style { + opacity: 0; + } + } } } }
A apps/web/src/lib/constants.ts

@@ -0,0 +1,2 @@

+export const IS_DEVELOPMENT = import.meta.env.DEV; +export const LOGIN_LINK = IS_DEVELOPMENT ? "http://127.0.0.1:3000/auth/login" : "/auth/login";
M apps/web/src/router/index.tsapps/web/src/router/index.ts

@@ -36,19 +36,14 @@ requiresAuth: true,

}, }, { - path: "/login", - name: "Login", - component: () => import("../views/AboutView.vue"), - }, - { path: "/tos", name: "Terms of Service", - component: () => import("../views/AboutView.vue"), + component: () => import("../views/TermsOfService.vue"), }, { path: "/privacy", name: "Privacy", - component: () => import("../views/AboutView.vue"), + component: () => import("../views/PrivacyPolicy.vue"), }, ], });
M apps/web/src/views/HomeView.vueapps/web/src/views/HomeView.vue

@@ -1,3 +1,89 @@

+<script setup lang="ts"> +import { computed } from "vue"; + +import { LOGIN_LINK } from "@/lib/constants"; +import { useApiStore } from "@/stores/api"; + +const api = useApiStore(); +const user = computed(() => api.user); +const actors = computed(() => api.actors); + +const isAuthenticated = computed(() => api.isAuthenticated); +</script> + <template> - <p>Select a guild.</p> + <div class="login" v-if="!isAuthenticated"> + <h1>You're signed out</h1> + <p>To access the ticket system, you must be logged in.</p> + <a :href="LOGIN_LINK">Login</a> + </div> + + <div class="hi" v-if="isAuthenticated"> + <h1>hi {{ user?.displayName }}</h1> + <div class="guilds"> + <RouterLink + v-for="actor in actors" + :key="actor.actorId" + :to="`/guilds/${actor.guild?.discordId}/tickets`" + class="guild" + > + <div class="guild-icon"> + <img :src="actor.guild?.icon!" alt="guild icon" /> + </div> + <div class="guild-name">Go to {{ actor.guild?.name }}</div> + </RouterLink> + </div> + </div> </template> + +<style scoped> +.guilds { + display: inline-flex; + flex-direction: column; + gap: 0.5rem; + + .guild { + background-color: hsl(var(--crust)); + border-radius: 5rem; + + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + padding-right: 1.5rem; + text-decoration: none; + color: hsl(var(--text)); + + .guild-icon { + flex-shrink: 0; + width: 3rem; + height: 3rem; + img { + width: 100%; + height: 100%; + border-radius: 50%; + } + } + + &:hover { + background-color: hsl(var(--surface1)); + &:active { + background-color: hsla(var(--surface1) / 0.75); + } + } + } +} + +.login { + a { + display: inline-block; + margin-top: 0.5rem; + background-color: hsl(var(--accent)); + color: hsl(var(--surface0)); + padding: 0.5rem 1.5rem; + border-radius: 5rem; + text-decoration: none; + } +} +</style>
A apps/web/src/views/PrivacyPolicy.vue

@@ -0,0 +1,195 @@

+<script lang="ts" setup> +const lastUpdated = new Date(1781213982679); +</script> + +<template> + <div class="content"> + <section> + <h1>Privacy Policy</h1> + <p> + Last updated: + <time :datetime="lastUpdated.toISOString()">{{ lastUpdated.toLocaleDateString() }}</time> + </p> + + <p> + This Privacy Policy explains how we collect, use, store, and protect your information when + you use our Discord bot (the "Bot") and its associated website (the "Website"). The Bot and + Website are designed solely for use within the Stealth Developers community. + </p> + </section> + + <section> + <h2>1. Data we collect</h2> + <p> + We only collect the minimum amount of data necessary to provide ticket-handling services and + website authentication. + </p> + + <h3>1.1. From the Discord Bot</h3> + <p> + When you interact with the bot (such as by opening a ticket or sending messages) in a ticket + channel, we collect: + </p> + <ul> + <li>The text content of messages sent within the ticket channel.</li> + <li>Any files or images sent within the ticket channel.</li> + <li>Your Discord user ID and username.</li> + </ul> + + <h3>1.2. From the Website</h3> + <p>When you log into or access the Website:</p> + <ul> + <li>We collect your Discord user ID and username for authentication purposes.</li> + <li> + We log standard connection data such as your IP address, browser user agent, and the + date/time of your request. + </li> + </ul> + </section> + + <section> + <h2>2. How and why we use your data</h2> + <p> + Under the UK General Data Protection Regulation (UK GDPR), our lawful basis for processing + this data is Legitimate Interests. Specifically, we process your data: + </p> + <ul> + <li>To operate, maintain, and secure the ticket support system.</li> + <li> + To allow moderators to review support history, handle player reports, and resolve + inquiries. + </li> + <li>To authenticate your access to the Website.</li> + <li> + To ensure the safety and security of our Roblox games and Discord server, and to prevent + abuse. + </li> + </ul> + <p> + Your personal data is never sold to third parties, nor is it shared with anyone outside of + the infrastructure necessary to host our services (such as our cloud hosting providers). + </p> + </section> + + <section> + <h2>3. Data access and security</h2> + <p> + Access to saved ticket data and transcripts is strictly restricted. The only entities with + access are: + </p> + <ul> + <li>The ticket author (you).</li> + <li>The moderators of Stealth Developers.</li> + <li>The developer and host of the Bot/Website.</li> + </ul> + <p> + We implement reasonable technical and organizational measures—such as secure database access + controls, encrypted connections (HTTPS), and strict token management—to protect your data + from unauthorized access. + </p> + </section> + + <section> + <h2>4. Data retention and deletion</h2> + <p> + Ticket transcripts and associated attachments are kept for a maximum of one year from the + creation of the ticket to allow for moderation history and appeal. After one year, + attachments and text transcripts are pseudonymised by permanently removing all direct user + identifiers (such as your Discord ID and username) from the record. + </p> + <p> + You may request the immediate deletion of your ticket transcripts and associated attachments + at any time. This can be done directly within Discord by using the + <code>/ticket manage delete</code> command, or by contacting the Bot developer. + </p> + </section> + + <section> + <h2>5. Children's Privacy</h2> + <p> + Our services are not intended for children under the age of 13. We do not knowingly collect + personal data from children under 13. If you are a parent or guardian and believe that your + child has provided us with personal data without your consent, please contact us so that we + can take steps to delete it. + </p> + </section> + + <section> + <h2>6. Your rights</h2> + <p>Under UK GDPR, you have the following rights regarding your data:</p> + <ul> + <li> + You have the right to request a copy of the personal data we hold about you. You can + obtain this using the <code>/ticket manage copy</code> command in Discord. + </li> + <li> + You have the right to request that we delete your personal data. You can trigger this + using the <code>/ticket manage delete</code> command in Discord. + </li> + <li> + You have the right to object to or ask us to restrict how we process your data. Exercising + this right may require us to delete or close your active tickets, and decline you access + to the Bot and Website. + </li> + </ul> + </section> + + <section> + <h2>7. Contact</h2> + <p> + If you have any questions about this Privacy Policy, or if you wish to make a manual data + request, you can contact us: + </p> + <ul> + <li>Email: <a href="mailto:apr@vt3e.cat">apr@vt3e.cat</a></li> + <li>Discord: either by creating a ticket or direct messaging @vt3e.cat</li> + </ul> + </section> + </div> +</template> + +<style> +.content { + max-width: 80ch; + display: flex; + flex-direction: column; + gap: 1rem; +} + +section { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-width: 80ch; + padding-bottom: 1rem; + + position: relative; + &:not(:last-child)::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 6px; + border-radius: 1rem; + background-color: hsla(var(--surface0) / 1); + } + + p { + margin-left: 1ch; + } + + h3 { + margin-left: 1ch; + } + + ul { + margin-left: 3ch; + list-style-type: disc; + + li { + margin-bottom: 0.25rem; + } + } +} +</style>
A apps/web/src/views/TermsOfService.vue

@@ -0,0 +1,171 @@

+<script lang="ts" setup> +const lastUpdated = new Date(1781213982679); +</script> + +<template> + <div class="content"> + <section> + <h1>Terms of Service</h1> + <p> + Last updated: + <time :datetime="lastUpdated.toISOString()">{{ lastUpdated.toLocaleDateString() }}</time> + </p> + + <p> + These Terms of Service ("Terms") govern your use of our Discord bot (the "Bot") and its + associated website (the "Website"). The Bot and Website are operated by the Stealth + Developers community to provide support and ticket-handling services. + </p> + <p> + By using the Bot or accessing the Website, you agree to comply with and be bound by these + Terms. If you do not agree, please do not use these services. + </p> + </section> + + <section> + <h2>1. Eligibility and Accounts</h2> + <p> + To use our Bot and Website, you must meet the minimum age requirements set by Discord (which + is 13 years of age in most jurisdictions) and Roblox. + </p> + <p> + You are responsible for maintaining the security of your Discord account. Any actions taken + through your account within our ticket system are considered your responsibility. + </p> + </section> + + <section> + <h2>2. Acceptable Use and Conduct</h2> + <p> + Our ticket system is designed to handle legitimate support inquiries, player reports, and + community questions. When opening a ticket or interacting with the Bot or Website, you agree + not to: + </p> + <ul> + <li>Spam, flood, or abuse the ticket creation system.</li> + <li>Submit intentionally false, misleading, or malicious player reports.</li> + <li>Harass, abuse, threaten, or insult the community moderators or developers.</li> + <li> + Upload, send, or share illegal, harmful, offensive, or malicious content (including + viruses, malware, or phishing links). + </li> + <li> + Attempt to exploit, reverse-engineer, or bypass any security features of the Bot or + Website. + </li> + </ul> + </section> + + <section> + <h2>3. Adherence to Platform Policies</h2> + <p> + Because our services operate in conjunction with external platforms, your use of the Bot and + Website must also strictly adhere to: + </p> + <ul> + <li>The Discord Terms of Service and Community Guidelines.</li> + <li>The Roblox Terms of Use and Community Standards.</li> + </ul> + <p> + Violations of these platform policies within our ticket system may be reported to the + respective platforms. + </p> + </section> + + <section> + <h2>4. Moderation and Service Access</h2> + <p> + We reserve the right, at our discretion, to suspend, terminate, or restrict your access to + the Bot, or Website if we believe you have violated these Terms or behave disruptively + within the community. + </p> + <p> + Active tickets may be closed or deleted by staff at any time if they are resolved, inactive, + or determined to be in violation of our guidelines. + </p> + </section> + + <section> + <h2>5. Disclaimer of Warranties and Limitation of Liability</h2> + <p> + The Bot and Website are provided on an "as is" and "as available" basis. While we strive to + provide a stable service, we make no guarantees that the services will be uninterrupted, + secure, or completely error-free. + </p> + <p> + To the maximum extent permitted by applicable law, the developers and community moderators + shall not be liable for any data loss, service interruptions, or indirect damages arising + from your use of or inability to use the Bot or Website. + </p> + <p></p> + </section> + + <section> + <h2>6. Changes to Terms</h2> + <p> + We may update these Terms from time to time to reflect changes in our services or legal + requirements. When we make changes, we will update the "Last updated" date at the top of + this page. Continued use of our Bot or Website after updates are posted constitutes your + acceptance of the revised Terms. + </p> + </section> + + <section> + <h2>7. Contact</h2> + <p> + If you have any questions or concerns regarding these Terms of Service, please reach out to + us: + </p> + <ul> + <li>Email: <a href="mailto:apr@vt3e.cat">apr@vt3e.cat</a></li> + <li>Discord: either by creating a support ticket or direct messaging @vt3e.cat</li> + </ul> + </section> + </div> +</template> + +<style> +.content { + max-width: 80ch; + display: flex; + flex-direction: column; + gap: 1rem; +} + +section { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-width: 80ch; + padding-bottom: 1rem; + + position: relative; + &:not(:last-child)::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 6px; + border-radius: 1rem; + background-color: hsla(var(--surface0) / 1); + } + + p { + margin-left: 1ch; + } + + h3 { + margin-left: 1ch; + } + + ul { + margin-left: 3ch; + list-style-type: disc; + + li { + margin-bottom: 0.25rem; + } + } +} +</style>
M apps/web/src/views/TicketDetailView.vueapps/web/src/views/TicketDetailView.vue

@@ -182,7 +182,7 @@ return `https://cdn.discordapp.com/embed/avatars/${Number(actor.discordId || 0) % 5}.png`;

}; const getAttachmentUrl = (att: SanitisedTicketAttachment) => { - return `https://api.media.vt3e.cat/sd-prod/${att.key}` + return `https://api.media.vt3e.cat/sd-prod/${att.key}`; }; const formattedAttachments = computed(() => {

@@ -342,7 +342,7 @@ </span>

<span class="author-role" :class="group[0]?.actorRole"> {{ group[0]?.actorRole }} </span> - <span class="private-badge" v-if="group[0]?.isPrivate"> Private Note </span> + <span class="private-badge" v-if="group[0]?.isPrivate"> Private </span> </div> <span class="group-time"> <TimeAgo :time="group[0]!.createdAt!" />

@@ -420,14 +420,14 @@ }

} .ticket-meta-grid { - display: flex; - justify-content: space-between; - align-items: stretch; - flex-wrap: wrap; - gap: 0.5rem; + display: flex; + justify-content: space-between; + align-items: stretch; + flex-wrap: wrap; + gap: 0.5rem; .meta-row { - flex: 1 1 250px; + flex: 1 1 250px; display: flex; flex-direction: column; background-color: hsl(var(--crust));

@@ -466,18 +466,18 @@ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));

} .reason-card { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; gap: 0.4rem; - background-color: hsl(var(--crust)); - padding: 1rem; - border-radius: 1.5rem; + background-color: hsl(var(--crust)); + padding: 1rem; + border-radius: 1.5rem; .reason-label { - font-size: 0.75rem; - font-weight: 900; - text-transform: uppercase; - color: hsl(var(--subtext0)); + font-size: 0.75rem; + font-weight: 900; + text-transform: uppercase; + color: hsl(var(--subtext0)); } .reason-value {

@@ -671,12 +671,12 @@ }

} .ticket-comments { - display: flex; - flex-direction: column; - background-color: hsl(var(--crust)); - border-radius: 1.5rem; - overflow: hidden; - margin-bottom: 0.5rem; + display: flex; + flex-direction: column; + background-color: hsl(var(--crust)); + border-radius: 1.5rem; + overflow: hidden; + margin-bottom: 0.5rem; h2 { padding: 0.5rem 1rem;

@@ -686,11 +686,11 @@ color: hsl(var(--text));

} .comments-list { - display: flex; - flex-direction: column; - background-color: hsl(var(--crust)); + display: flex; + flex-direction: column; + background-color: hsl(var(--crust)); - .comment { + .comment { display: flex; flex-direction: column; gap: 0.75rem;

@@ -705,7 +705,7 @@ }

} .ticket-messages { - display: flex; + display: flex; flex-direction: column; background-color: hsl(var(--crust));
M apps/web/src/views/TicketsView.vueapps/web/src/views/TicketsView.vue

@@ -52,7 +52,9 @@ </span>

</div> </div> <div class="ticket-footer"> - <RouterLink class="link" :to="`./tickets/${ticket.localId}`">View Ticket</RouterLink> + <RouterLink class="link" :to="`/guilds/${guildId}/tickets/${ticket.localId}`" + >View Ticket</RouterLink + > </div> </div> </div>