fixed a bunch of bugs, yay!!!

This commit is contained in:
Zoe
2023-01-13 03:34:54 -06:00
parent 3bad12c646
commit c39da0678d
24 changed files with 526 additions and 267 deletions

View File

@@ -0,0 +1,8 @@
<template>
<li>
<button
class="w-full cursor-pointer bg-[hsl(225,7.7%,10.2%)] hover:bg-[hsl(225,7.7%,17.4%)] text-left px-3 py-1.5 rounded-md flex items-center">
<slot />
</button>
</li>
</template>

View File

@@ -0,0 +1,48 @@
<template>
<Transition name="pop-in">
<div ref="dropdown" class="z-[2] absolute m-2 bg-[hsl(225,7.7%,10.2%)] w-[calc(100%-1rem)] p-3 rounded text-left"
:class="(inverted) ? 'dropdown-inverse' : 'dropdown'"
v-if="opened">
<slot />
</div>
</Transition>
</template>
<script lang="ts">
export default {
props: ['opened', 'inverted'],
}
</script>
<style>
.dropdown {
transform-origin: top center;
}
.dropdown-inverse {
transform-origin: bottom center;
}
.dropdown-inverse > ul {
display: flex;
flex-direction: column-reverse;
}
.pop-in-enter-active {
animation: pop-in 150ms cubic-bezier(.81, .5, .44, .83);
}
.pop-in-leave-active {
animation: pop-in 150ms reverse cubic-bezier(.81, .5, .44, .83);
}
@keyframes pop-in {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
</style>

53
components/InviteCard.vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<div class="w-6/12 bg-[hsl(223,6.9%,19.8%)] p-4 rounded-md shadow-md mr-2">
<p class="text-sm font-semibold text-zinc-100">You've been invited to join a
server</p>
<span class="text-xl font-bold capitalize leading-loose">{{ invite.server.name }}</span>
<div class="flex items-center">
<span
class="before:bg-[hsl(214,9.9%,50.4%)] before:h-2 before:w-2 before:inline-block before:my-auto before:rounded-full before:mr-1"></span>
<span>{{ invite.server.participants.length }} Members</span>
</div>
<div class="flex w-full justify-end">
<button @click="joinServer(invite)"
class="font-semibold rounded px-4 py-2 transition-colors"
:class="(userInServer) ? 'bg-green-800 cursor-not-allowed' : 'bg-green-700 hover:bg-green-600'">
<span v-if="userInServer">
Joined
</span>
<span v-else>
Join
</span>
</button>
</div>
</div>
</template>
<script lang="ts">
import { useGlobalStore } from '~/stores/store'
import { IInviteCode, IUser } from '~/types'
export default {
props: ['invite'],
data() {
return {
user: storeToRefs(useGlobalStore()).user,
servers: storeToRefs(useGlobalStore()).servers,
}
},
computed: {
userInServer(): boolean {
return !!this.invite.server.participants.find((e: IUser) => e.id === this.user.id)
}
},
methods: {
async joinServer(invite: IInviteCode) {
if (this.userInServer) return;
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id }, headers })
if (!server) return;
this.servers?.push(server)
},
}
}
</script>

View File

@@ -1,5 +1,17 @@
<template> <template>
<div class="h-full bg-[hsl(220,calc(1*7.7%),22.9%)] relative text-white"> <div class="h-full bg-[hsl(220,calc(1*7.7%),22.9%)] relative text-white">
<div class="bg-[hsl(220,calc(1*7.7%),22.9%)] absolute w-full shadow px-4 py-3 flex items-center z-[1] shadow-zinc-900/50">
<span>
<svg class="text-zinc-300/80 my-auto" xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24">
<path fill="currentColor"
d="m5.41 21l.71-4h-4l.35-2h4l1.06-6h-4l.35-2h4l.71-4h2l-.71 4h6l.71-4h2l-.71 4h4l-.35 2h-4l-1.06 6h4l-.35 2h-4l-.71 4h-2l.71-4h-6l-.71 4h-2M9.53 9l-1.06 6h6l1.06-6h-6Z" />
</svg>
</span>
<span class="text-zinc-100 font-semibold">{{ server.name }}</span>
</div>
<div class="w-full h-[calc(100%-60px)] overflow-y-scroll pb-1" <div class="w-full h-[calc(100%-60px)] overflow-y-scroll pb-1"
id="conversation-pane"> id="conversation-pane">
<div> <div>
@@ -16,41 +28,18 @@
</p> </p>
<p class="break-words max-w-full">{{ message.body }}</p> <p class="break-words max-w-full">{{ message.body }}</p>
</div> </div>
<div> <div v-for="invite in message.invites">
<div v-for="invite in message.invites"> <InviteCard :invite="invite" />
<div class="w-6/12 bg-[hsl(223,6.9%,19.8%)] p-4 rounded-md shadow-md mr-2">
<p class="text-sm font-semibold text-zinc-100">You've been invited to join a
server</p>
<span class="text-xl font-bold capitalize">{{ invite.server.name }}</span>
<div class="flex items-center">
<span
class="before:bg-[hsl(214,9.9%,50.4%)] before:h-2 before:w-2 before:inline-block before:my-auto before:rounded-full before:mr-1"></span>
<span>{{ invite.server.participants.length }} Members</span>
</div>
<div class="flex w-full justify-end">
<button @click="joinServer(invite)"
class="font-semibold rounded px-4 py-2 transition-colors"
:class="(invite.server.participants.find((e) => e.id === user.id)) ? 'bg-green-800 cursor-not-allowed' : 'bg-green-700 hover:bg-green-600'">
<span v-if="invite.server.participants.find((e) => e.id === user.id)">
Joined
</span>
<span v-else>
Join
</span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="conversation-input w-[calc(100vw-88px-240px)]"> <div class="conversation-input w-[calc(100vw-88px-240px)] h-[61.1px]">
<form @submit.prevent="sendMessage" <form @submit.prevent="sendMessage"
@keydown.enter.exact.prevent="sendMessage" @keydown.enter.exact.prevent="sendMessage"
class="relative px-4 w-full"> class="relative px-4 w-full pt-1.5">
<div id="textbox" <div id="textbox"
class="px-4 rounded-md w-full h-[44px] bg-[hsl(218,calc(1*7.9%),27.3%)] placeholder:text-[hsl(218,calc(1*4.6%),46.9%)] flex flex-row"> class="px-4 rounded-md w-full h-[44px] bg-[hsl(218,calc(1*7.9%),27.3%)] placeholder:text-[hsl(218,calc(1*4.6%),46.9%)] flex flex-row">
<textarea type="text" <textarea type="text"
@@ -121,47 +110,22 @@ export default {
if (!lastElementChild) return; if (!lastElementChild) return;
setTimeout(() => { setTimeout(() => {
if (conversationDiv.scrollTop + 11.2 < (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight) return; if (conversationDiv.scrollTop + 20 < (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight) return;
conversationDiv.scrollTop = conversationDiv.scrollHeight; conversationDiv.scrollTop = conversationDiv.scrollHeight;
}) })
}); });
}, },
// updated() { unmounted() {
// const route = useRoute() const socket = io();
// const socket = io(); socket.removeAllListeners();
},
// const conversationDiv = document.getElementById('conversation-pane');
// if (!conversationDiv) throw new Error('conversation div not found')
// this.scrollToBottom()
// socket.removeAllListeners('connect')
// socket.on('connect', () => {
// // listen for messages from the server
// socket.on(`message-${route.params.id}`, (ev) => {
// const { message } = ev
// console.log(message.userId, this.user.id, message, this.conversation)
// if (message.userId == this.user.id) return;
// this.conversation.push(message)
// const lastElementChild = conversationDiv.children[0]?.lastElementChild
// if (!lastElementChild) return;
// setTimeout(() => {
// console.log(conversationDiv.scrollTop, conversationDiv.scrollHeight, conversationDiv.clientHeight, lastElementChild.clientHeight, (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight)
// if (conversationDiv.scrollTop + 11.2 < (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight) return;
// conversationDiv.scrollTop = conversationDiv.scrollHeight;
// })
// })
// });
// },
methods: { methods: {
async sendMessage() { async sendMessage() {
const route = useRoute() const route = useRoute()
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
if (!this.messageContent) return; if (!this.messageContent) return;
const message: IMessage = await $fetch(`/api/channels/sendMessage`, { method: 'post', body: { body: this.messageContent, channelId: route.params.id } }) const message: IMessage = await $fetch(`/api/channels/sendMessage`, { method: 'post', body: { body: this.messageContent, channelId: route.params.id }, headers })
if (!message) return; if (!message) return;
if (this.conversation.includes(message)) return; if (this.conversation.includes(message)) return;
@@ -174,17 +138,10 @@ export default {
conversationDiv.scrollTop = conversationDiv.scrollHeight; conversationDiv.scrollTop = conversationDiv.scrollHeight;
}) })
}, },
async joinServer(invite: IInviteCode) {
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id } })
if (!server) return;
this.servers?.push(server)
},
scrollToBottom() { scrollToBottom() {
const conversationDiv = document.getElementById('conversation-pane'); const conversationDiv = document.getElementById('conversation-pane');
if (!conversationDiv) throw new Error('wtf'); if (!conversationDiv) throw new Error('wtf');
setTimeout(() => { conversationDiv.scrollTo(0, conversationDiv.scrollHeight);
conversationDiv.scrollTop = conversationDiv.scrollHeight;
})
} }
// resizeTextarea() { // resizeTextarea() {
// const textArea = document.getElementById('messageBox') // const textArea = document.getElementById('messageBox')
@@ -208,7 +165,6 @@ export default {
flex-direction: row; flex-direction: row;
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
height: 60px;
background-color: hsl(220, calc(1 * 7.7%), 22.9%); background-color: hsl(220, calc(1 * 7.7%), 22.9%);
bottom: calc(0px - 0.5rem); bottom: calc(0px - 0.5rem);
} }

View File

@@ -5,21 +5,36 @@
<nuxt-link to="/channel/@me"> <nuxt-link to="/channel/@me">
<div @click="openServer('@me', 'dms')" <div @click="openServer('@me', 'dms')"
class="bg-zinc-600/80 p-3 rounded-full transition-all hover:rounded-2xl ease-in-out hover:bg-zinc-500/60 duration-300"> class="bg-zinc-600/80 p-3 rounded-full transition-all hover:rounded-2xl ease-in-out hover:bg-zinc-500/60 duration-300">
<svg width="32" <span>
height="32" <svg width="32"
viewBox="0 0 24 24"> height="32"
<path fill="none" viewBox="0 0 24 24">
stroke="currentColor" <defs>
stroke-linecap="round" <linearGradient id="fire"
stroke-linejoin="round" x1="-2.778%"
stroke-width="2" x2="100%"
d="M12 12c2-2.96 0-7-1-8c0 3.038-1.773 4.741-3 6c-1.226 1.26-2 3.24-2 5a6 6 0 1 0 12 0c0-1.532-1.056-3.94-2-5c-1.786 3-2.791 3-4 2z" /> y1="24%"
</svg> y2="48%">
<stop offset="0%"
stop-color="#ff0c41" />
<stop offset="100%"
stop-color="#ff6b0c" />
</linearGradient>
</defs>
<path fill="none"
stroke="url(#fire)"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 12c2-2.96 0-7-1-8c0 3.038-1.773 4.741-3 6c-1.226 1.26-2 3.24-2 5a6 6 0 1 0 12 0c0-1.532-1.056-3.94-2-5c-1.786 3-2.791 3-4 2z" />
</svg>
</span>
</div> </div>
</nuxt-link> </nuxt-link>
</div> </div>
<div class="overflow-y-scroll my-2 flex gap-y-2 flex-col"> <div class="overflow-y-scroll my-2 flex gap-y-2 flex-col">
<nuxt-link v-for="server in servers" :to="'/channel/' + server.channels[0].id"> <nuxt-link v-for="server in servers"
:to="'/channel/' + server.channels[0].id">
<div :key="server.id" <div :key="server.id"
@click="openServer(server.id, 'servers')" @click="openServer(server.id, 'servers')"
class="bg-zinc-600/80 p-3 rounded-full transition-all hover:rounded-2xl ease-in-out hover:bg-zinc-500/60 duration-300 h-[56px] w-[56px]"> class="bg-zinc-600/80 p-3 rounded-full transition-all hover:rounded-2xl ease-in-out hover:bg-zinc-500/60 duration-300 h-[56px] w-[56px]">
@@ -63,9 +78,6 @@
<div v-if="createServerModelOpen" <div v-if="createServerModelOpen"
class="absolute z-10 top-0 bottom-0 left-0 right-0"> class="absolute z-10 top-0 bottom-0 left-0 right-0">
<div class="bg-zinc-900/80 w-screen h-screen"
@click="createServerModelOpen = false">
</div>
<div <div
class="p-4 z-20 absolute bg-zinc-800 shadow-md rounded-md -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-white"> class="p-4 z-20 absolute bg-zinc-800 shadow-md rounded-md -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-white">
<h2 class="font-semibold text-xl"> <h2 class="font-semibold text-xl">
@@ -83,7 +95,11 @@
</form> </form>
</div> </div>
</div> </div>
<div class="bg-zinc-900/80 w-screen h-screen"
@click="createServerModelOpen = false">
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -101,12 +117,13 @@ export default {
methods: { methods: {
async createServer() { async createServer() {
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const server: IServer = await $fetch('/api/channels/create', { method: 'post', body: { serverName: this.serverName } }) const headers = useRequestHeaders(['cookie']) as Record<string, string>;
const server: IServer = await $fetch('/api/channels/create', { method: 'post', body: { serverName: this.serverName }, headers })
this.createServerModelOpen = false; this.createServerModelOpen = false;
this.serverName = ''; this.serverName = '';
globalStore.addServer(server) globalStore.addServer(server)
}, },
openServer(id: string, type: string): void { openServer(id: string, type: "servers" | "dms"): void {
useGlobalStore().setActive(type, id) useGlobalStore().setActive(type, id)
} }
}, },

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <aside
class="bg-[hsl(223,calc(1*6.9%),19.8%)] min-w-60 w-60 h-screen shadow-sm text-white select-none grid grid-rows-[93.5%_1fr]"> class="bg-[hsl(223,calc(1*6.9%),19.8%)] min-w-60 w-60 h-screen shadow-sm text-white select-none grid grid-rows-[93.5%_1fr] relative z-[2]">
<div v-if="!server.id || server.DM == true"> <div v-if="serverType === 'dms' || !server.id">
<div> <div>
<nuxt-link v-for="dm in dms" <nuxt-link v-for="dm in dms"
:to="'/channel/@me/' + dm.id"> :to="'/channel/@me/' + dm.id">
@@ -14,36 +14,14 @@
</div> </div>
<div class="w-full" <div class="w-full"
v-else> v-else>
<div class="flex p-4 border-b border-zinc-600/80"> <h4 @click="serverDropdownOpen = !serverDropdownOpen"
<h4 class="text-lg font-semibold grid gap-1 grid-cols-[1fr_28px] w-full"> class="py-3 px-4 font-semibold grid gap-1 grid-cols-[1fr_28px] w-full items-center cursor-pointer p-1 bg-[hsl(223,calc(1*6.9%),19.8%)] transition-all"
<span>{{ server.name }}</span> :class="(!serverDropdownOpen) ? 'hover:bg-[hsl(223,calc(1*6.9%),26.4%)]' : 'bg-[hsl(223,calc(1*6.9%),26.4%)]'">
<button class="cursor-pointer p-1 bg-[hsl(223,calc(1*6.9%),19.8%)] hover:bg-[hsl(223,calc(1*6.9%),26.4%)] transition-all"> <span>{{ server.name }}</span>
<span class="h-fit w-[20px]"> <button>
<svg xmlns="http://www.w3.org/2000/svg" <span v-if="!serverDropdownOpen"
width="20" class="h-fit w-[20px]">
height="20" <svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</span>
</button>
</h4>
</div>
<div class="flex gap-y-1.5 px-1.5 mt-2 flex-col">
<button @click="createInvite"
v-if="userIsOwner || userIsAdmin">make invite</button>
<button
class="flex text-center hover:bg-[hsl(223,calc(1*6.9%),26.4%)] px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm gap-1/5 cursor-pointer"
v-for="channel in server.channels"
@click="openChannel(channel.id)"
:key="channel.id">
<span>
<svg class="text-zinc-300 my-auto"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
@@ -52,16 +30,79 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16" /> d="m6 9l6 6l6-6" />
</svg>
</span>
<span class="h-fit w-[20px]"
v-else>
<svg xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12" />
</svg>
</span>
</button>
</h4>
<div>
<DropdownMenu :opened="serverDropdownOpen">
<div>
<ul class="flex flex-col gap-y-1">
<DropdownItem v-if="userIsOwner || userIsAdmin"
@click="createInvite">
<span class="mr-1.5 h-fit">
<svg xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24">
<g fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2">
<circle cx="9"
cy="7"
r="4" />
<path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2m1-10h6m-3-3v6" />
</g>
</svg>
</span>
<span>
Invite a friend
</span>
</DropdownItem>
</ul>
</div>
</DropdownMenu>
</div>
<div class="flex gap-y-1.5 px-1.5 mt-2 flex-col">
<button
class="flex text-center hover:bg-[hsl(223,calc(1*6.9%),26.4%)] px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm gap-1/5 cursor-pointer items-center"
v-for="channel in server.channels"
@click="openChannel(channel.id)"
:key="channel.id">
<span class="h-fit">
<svg class="text-zinc-300/80 my-auto"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24">
<path fill="currentColor"
d="m5.41 21l.71-4h-4l.35-2h4l1.06-6h-4l.35-2h4l.71-4h2l-.71 4h6l.71-4h2l-.71 4h4l-.35 2h-4l-1.06 6h4l-.35 2h-4l-.71 4h-2l.71-4h-6l-.71 4h-2M9.53 9l-1.06 6h6l1.06-6h-6Z" />
</svg> </svg>
</span> </span>
<span>{{ channel.name }}</span> <span>{{ channel.name }}</span>
</button> </button>
<button v-if="userIsOwner || userIsAdmin" <button v-if="userIsOwner || userIsAdmin"
@click="openCreateChannelModel" @click="openCreateChannelModel"
class="flex text-center hover:bg-[hsl(223,calc(1*6.9%),26.4%)] px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm cursor-pointer"> class="flex text-center hover:bg-[hsl(223,calc(1*6.9%),26.4%)] px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm cursor-pointer items-center">
<span> <span>
<svg xmlns="http://www.w3.org/2000/svg" <svg class="text-zinc-300/80 my-auto" xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
@@ -78,12 +119,44 @@
</div> </div>
</div> </div>
<div> <div class="relative">
<DropdownMenu class="bottom-full"
:inverted="true"
:opened="userDropdownOpen">
<div>
<ul class="flex flex-col gap-y-1">
<DropdownItem v-if="userIsOwner || userIsAdmin"
@click="createInvite">
<span class="mr-1.5 h-fit">
<svg xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24">
<g fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2">
<circle cx="9"
cy="7"
r="4" />
<path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2m1-10h6m-3-3v6" />
</g>
</svg>
</span>
<span>
Invite a friend
</span>
</DropdownItem>
</ul>
</div>
</DropdownMenu>
<div class="bg-[hsl(220,calc(1*6.8%),17.3%)] h-full p-3"> <div class="bg-[hsl(220,calc(1*6.8%),17.3%)] h-full p-3">
<div class="grid grid-cols-[32px_1fr_32px] gap-x-2 items-center"> <div class="grid grid-cols-[32px_1fr_32px] gap-x-2 items-center">
<span class="bg-[hsl(220,calc(1*6.8%),22.6%)] w-[32px] h-[32px] rounded-full"></span> <span class="bg-[hsl(220,calc(1*6.8%),22.6%)] w-[32px] h-[32px] rounded-full"></span>
<span class="h-fit w-fit overflow-ellipsis">{{ user.username }}</span> <span class="h-fit w-fit overflow-ellipsis">{{ user.username }}</span>
<span class="text-zinc-300 hover:bg-[hsl(220,calc(1*6.8%),14.3%)] p-1 transition-colors"> <button @click="userDropdownOpen = !userDropdownOpen"
class="text-zinc-300 hover:bg-[hsl(220,calc(1*6.8%),14.3%)] p-1 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
@@ -100,11 +173,11 @@
r="3" /> r="3" />
</g> </g>
</svg> </svg>
</span> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </aside>
<div v-if="createChannelModelOpen" <div v-if="createChannelModelOpen"
class="absolute z-10 top-0 bottom-0 left-0 right-0"> class="absolute z-10 top-0 bottom-0 left-0 right-0">
@@ -139,31 +212,30 @@ export default {
data() { data() {
return { return {
server: storeToRefs(useGlobalStore()).activeServer, server: storeToRefs(useGlobalStore()).activeServer,
serverType: storeToRefs(useGlobalStore()).activeServerType,
user: storeToRefs(useGlobalStore()).user, user: storeToRefs(useGlobalStore()).user,
dms: storeToRefs(useGlobalStore()).dms, dms: storeToRefs(useGlobalStore()).dms,
createChannelModelOpen: false, createChannelModelOpen: false,
serverDropdownOpen: false,
userDropdownOpen: false,
channelName: '', channelName: '',
userIsOwner: false,
userIsAdmin: false,
} }
}, },
async mounted() { computed: {
const that = this; userIsOwner() {
var interval = setInterval(function () { return this.server && this.serverType === "servers" && this.server.roles?.find((e: IRole) => e.users.some((el) => el.id === this.user.id))?.owner
// get elem },
if (typeof that.server.roles == 'undefined') return; userIsAdmin() {
clearInterval(interval); return this.server && this.serverType === "servers" && this.server.roles?.find((e: IRole) => e.users.some((el) => el.id === this.user.id))?.administer
}
that.userIsOwner = that.server.roles?.find((e: IRole) => e.users.some((el) => el.id === that.user.id))?.owner || false
that.userIsAdmin = that.server.roles?.find((e: IRole) => e.users.some((el) => el.id === that.user.id))?.administer || false
}, 10);
}, },
methods: { methods: {
openCreateChannelModel() { openCreateChannelModel() {
this.createChannelModelOpen = true; this.createChannelModelOpen = true;
}, },
async createChannel() { async createChannel() {
const channel = await $fetch(`/api/guilds/${this.server.id}/addChannel`, { method: 'POST', body: { channelName: this.channelName } }) as IChannel const headers = useRequestHeaders(['cookie']) as Record<string, string>;
const channel = await $fetch(`/api/guilds/${this.server.id}/addChannel`, { method: 'POST', body: { channelName: this.channelName }, headers }) as IChannel
if (!channel) return; if (!channel) return;
@@ -176,7 +248,8 @@ export default {
router.push({ params: { id } }) router.push({ params: { id } })
}, },
async createInvite() { async createInvite() {
const inviteCode = await $fetch(`/api/guilds/${this.server.id}/createInvite`, { method: 'POST' }) const headers = useRequestHeaders(['cookie']) as Record<string, string>
const inviteCode = await $fetch(`/api/guilds/${this.server.id}/createInvite`, { method: 'POST', headers })
}, },
}, },
} }

View File

@@ -1,7 +1,6 @@
<template> <template>
<Suspense> <Suspense>
<div v-if="user.id" <div class="flex h-screen max-h-screen text-white">
class="flex h-screen max-h-screen text-white">
<Nav /> <Nav />
<Sidebar /> <Sidebar />
<div class="w-[calc(100vw-88px-240px)] h-full"> <div class="w-[calc(100vw-88px-240px)] h-full">
@@ -23,24 +22,36 @@ import { SafeUser } from '~/types'
export default { export default {
data() { data() {
return { return {
activeServer: storeToRefs(useGlobalStore()).activeServer, user: storeToRefs(useGlobalStore()).user,
user: storeToRefs(useGlobalStore()).user
} }
}, },
async setup() { async setup() {
const globalStore = useGlobalStore()
const userStore = useGlobalStore()
const sessionToken = useCookie('sessionToken') const sessionToken = useCookie('sessionToken')
if (userStore.user.id === undefined && sessionToken.value) { if (globalStore.user.id === undefined && sessionToken.value) {
const user: SafeUser = await $fetch('/api/getCurrentUser') const route = useRoute()
const headers = useRequestHeaders(['cookie']) as Record<string, string>
const [user, { dms, servers }] = await Promise.all([
$fetch('/api/getCurrentUser', { headers }) as unknown as SafeUser,
$fetch('/api/user/getServers', { headers })
])
if (!user) return; if (!user || !servers || !dms) return;
userStore.setUser(user) globalStore.setUser(user)
const { channels: dms, servers } = await $fetch('/api/user/getServers')
useGlobalStore().servers = servers globalStore.setServers(servers)
useGlobalStore().dms = dms globalStore.setDms(dms)
console.log('params', route.params.id)
if (route.params.id && typeof route.params.id === 'string') {
globalStore.setActive(route.path.includes('@me') ? 'dms' : 'servers', route.params.id)
}
const server = globalStore.activeServer
return {
server
}
} }
} }
} }

View File

@@ -36,4 +36,8 @@ export default {
}, },
], ],
], ],
typescript: {
strict: true
}
} }

View File

@@ -2,16 +2,6 @@
<MessagePane :server="server" /> <MessagePane :server="server" />
</template> </template>
<script async setup lang="ts">
const route = useRoute()
const server: IChannel = await $fetch(`/api/channels/${route.params.id}`)
if (server) {
useGlobalStore().addDM(server);
useGlobalStore().setActive('dms', server.id);
}
</script>
<script lang="ts"> <script lang="ts">
import { useGlobalStore } from '~/stores/store' import { useGlobalStore } from '~/stores/store'
import { IChannel } from '~/types' import { IChannel } from '~/types'
@@ -21,8 +11,24 @@ definePageMeta({
}) })
export default { export default {
async setup() {
const route = useRoute()
const headers = useRequestHeaders(['cookie']) as Record<string, string>
const server: IChannel = await $fetch(`/api/channels/${route.params.id}`, { headers })
if (!server) throw new Error('could not find the dm')
useGlobalStore().addDM(server);
if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?')
useGlobalStore().setActive('dms', route.params.id);
return {
server
}
},
async updated() { async updated() {
if (!useGlobalStore().activeServer == this.server) useGlobalStore().setActive('dms', this.server.id) const route = useRoute()
if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?')
if (useGlobalStore().activeServer !== this.server) useGlobalStore().setActive('dms', route.params.id)
}, },
} }
</script> </script>

View File

@@ -19,9 +19,14 @@ export default {
userId: '' userId: ''
} }
}, },
mounted() {
console.log('mounted')
useGlobalStore().setActive('dms', '@me')
},
methods: { methods: {
async startDM() { async startDM() {
const server: IChannel = await $fetch('/api/channels/createDM', { method: 'post', body: { partnerId: this.userId } }) const headers = useRequestHeaders(['cookie']) as Record<string, string>
const server: IChannel = await $fetch('/api/channels/createDM', { method: 'post', body: { partnerId: this.userId }, headers })
useGlobalStore().addDM(server) useGlobalStore().addDM(server)
useRouter().push({ path: '/channel/@me/' + server.id }) useRouter().push({ path: '/channel/@me/' + server.id })

View File

@@ -2,19 +2,6 @@
<MessagePane :server="server" /> <MessagePane :server="server" />
</template> </template>
<script async setup lang="ts">
const route = useRoute()
const server: IChannel = await $fetch(`/api/channels/${route.params.id}`)
const realServer = useGlobalStore().servers?.filter((e) => e.channels.some((el) => el.id == route.params.id))[0]
if (realServer) {
useGlobalStore().addServer(realServer);
useGlobalStore().setActive('servers', realServer.id)
}
</script>
<script lang="ts"> <script lang="ts">
import { useGlobalStore } from '~/stores/store' import { useGlobalStore } from '~/stores/store'
import { IChannel } from '~/types' import { IChannel } from '~/types'
@@ -24,12 +11,32 @@ definePageMeta({
}) })
export default { export default {
async setup() {
const route = useRoute()
const headers = useRequestHeaders(['cookie']) as Record<string, string>
const server: IChannel = await $fetch(`/api/channels/${route.params.id}`, { headers })
const realServer = useGlobalStore().servers?.find((e) => e.channels.some((el) => el.id == route.params.id))
if (!realServer) throw new Error('realServer not found, this means that the channel is serverless but not a dm????');
useGlobalStore().addServer(realServer);
if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumiably?')
useGlobalStore().setActive('servers', route.params.id)
return {
server
}
},
async updated() { async updated() {
const route = useRoute()
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
if (!this.server) return; if (!this.server) return;
this.server = await $fetch(`/api/channels/${route.params.id}`); this.server = await $fetch(`/api/channels/${route.params.id}`, { headers });
if (!useGlobalStore().activeServer == this.server.id) useGlobalStore().setActive('servers', this.server.id) if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumiably?')
if (useGlobalStore().activeServer.id !== this.server.id) useGlobalStore().setActive('servers', route.params.id)
} }
} }
</script> </script>

View File

@@ -37,12 +37,14 @@ export default {
}, },
methods: { methods: {
async signup() { async signup() {
const headers = useRequestHeaders(['cookie'])
if (!this.username || !this.password) return; if (!this.username || !this.password) return;
const user = await $fetch('/api/login', { const user = await $fetch('/api/login', {
method: 'post', body: { method: 'post', body: {
username: this.username, username: this.username,
password: this.password password: this.password
} },
headers
}) as { userId: string; token: string; user: SafeUser; } }) as { userId: string; token: string; user: SafeUser; }
const userId = useCookie('userId') const userId = useCookie('userId')

View File

@@ -48,13 +48,15 @@ export default {
}, },
methods: { methods: {
async signup() { async signup() {
const headers = useRequestHeaders(['cookie']) as Record<string, string>
if (!this.username || !this.password || !this.email) return; if (!this.username || !this.password || !this.email) return;
const user = await $fetch('/api/signup', { const user = await $fetch('/api/signup', {
method: 'post', body: { method: 'post', body: {
username: this.username, username: this.username,
email: this.email, email: this.email,
password: this.password password: this.password
} },
headers
}) as { userId: string; token: string; user: SafeUser; } }) as { userId: string; token: string; user: SafeUser; }
const userId = useCookie('userId') const userId = useCookie('userId')

View File

@@ -4,6 +4,7 @@ const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) { if (!event.context.user.authenticated) {
event.node.res.statusCode = 401;
return { return {
message: 'You must be logged in to view a channel.' message: 'You must be logged in to view a channel.'
} }

View File

@@ -4,7 +4,7 @@ const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) { if (!event.context.user.authenticated) {
// event.node.res.statusCode = 401; event.node.res.statusCode = 401;
return { return {
message: "Unauthenticated" message: "Unauthenticated"
} }

View File

@@ -4,6 +4,7 @@ const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) { if (!event.context.user.authenticated) {
event.node.res.statusCode = 401;
return { return {
message: 'You must be logged in to view a channel.' message: 'You must be logged in to view a channel.'
} }

View File

@@ -4,6 +4,7 @@ const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) { if (!event.context.user.authenticated) {
event.node.res.statusCode = 401;
return { return {
message: 'You must be logged in to view a channel.' message: 'You must be logged in to view a channel.'
} }

View File

@@ -3,8 +3,11 @@ import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient() const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) return { if (!event.context.user.authenticated) {
message: 'You must be logged in to view a channel.' event.node.res.statusCode = 401;
return {
message: 'You must be logged in to view a channel.'
}
} }
if (!event.context.params.id) { if (!event.context.params.id) {

View File

@@ -1,10 +1,13 @@
import { IServer } from '~/types' import { IInviteCode, IServer } from '~/types'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient() const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) return { if (!event.context.user.authenticated) {
message: 'You must be logged in to view a channel.' event.node.res.statusCode = 401;
return {
message: 'You must be logged in to view a channel.'
}
} }
const { inviteId } = await readBody(event); const { inviteId } = await readBody(event);
@@ -20,10 +23,24 @@ export default defineEventHandler(async (event) => {
where: { where: {
id: inviteId id: inviteId
}, },
include: { select: {
server: true id: true,
server: {
select: {
id: true,
name: true,
participants: {
select: {
id: true,
}
}
}
},
expires: true,
expiryDate: true,
maxUses: true
} }
}) }) as IInviteCode | null;
if (!invite) { if (!invite) {
event.node.res.statusCode = 404; event.node.res.statusCode = 404;
@@ -32,6 +49,15 @@ export default defineEventHandler(async (event) => {
} }
} }
const userInServer = invite.server.participants.find((e) => e.id === event.context.user.id);
if (userInServer) {
event.node.res.statusCode = 409;
return {
message: `You are already in that server.`
}
}
// TODO: check if invite is valid // TODO: check if invite is valid
const server = await prisma.server.update({ const server = await prisma.server.update({
@@ -50,7 +76,7 @@ export default defineEventHandler(async (event) => {
channels: true, channels: true,
roles: true roles: true
} }
}) as IServer }) as unknown as IServer
if (!server) { if (!server) {
event.node.res.statusCode = 404; event.node.res.statusCode = 404;

View File

@@ -1,65 +1,79 @@
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import { IServer, IUser } from '~/types' import { IChannel, IServer, IUser } from '~/types'
const prisma = new PrismaClient() const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) { if (!event.context.user.authenticated) {
// event.node.res.statusCode = 401; event.node.res.statusCode = 401;
return { return {
message: "Unauthenticated" message: "Unauthenticated"
} }
} }
const { servers, channels } = await prisma.user.findFirst({ const servers = await prisma.server.findMany({
where: { where: {
id: event.context.user.id participants: {
some: {
id: event.context.user.id
}
}
}, },
select: { select: {
id: true,
name: true,
channels: { channels: {
select: { select: {
id: true, id: true,
name: true,
messages: false,
DM: true, DM: true,
dmParticipants: true name: true
} }
}, },
servers: { participants: {
select: {
id: true,
username: true
}
},
roles: {
select: { select: {
id: true, id: true,
name: true, name: true,
channels: { administrator: true,
owner: true,
users: {
select: { select: {
id: true, id: true
DM: true,
name: true
}
},
participants: {
select: {
id: true,
username: true
}
},
roles: {
select: {
id: true,
name: true,
administrator: true,
owner: true,
users: {
select: {
id: true
}
}
} }
} }
}, }
}, }
} }
}) as IUser | null; }) as unknown as IServer[] | null;
const dms = await prisma.channel.findMany({
where: {
DM: true,
dmParticipants: {
some: {
id: event.context.user.id
}
}
},
select: {
id: true,
name: true,
messages: false,
DM: true,
dmParticipants: {
select: {
id: true,
username: true
}
}
}
}) as IChannel[] | null;
return { return {
servers, channels servers, dms
} }
}) })

View File

@@ -1,9 +1,9 @@
import { Ref } from "vue";
import { SafeUser, IServer, IChannel } from "../types"; import { SafeUser, IServer, IChannel } from "../types";
export const useGlobalStore = defineStore('global', { export const useGlobalStore = defineStore('global', {
state: () => ({ state: () => ({
activeServer: {} as IServer, activeServer: {} as IServer | IChannel,
activeServerType: '' as "dms" | "servers" | undefined,
user: {} as SafeUser, user: {} as SafeUser,
dms: [] as IChannel[], dms: [] as IChannel[],
servers: [] as IServer[] servers: [] as IServer[]
@@ -17,20 +17,38 @@ export const useGlobalStore = defineStore('global', {
this.servers.push(server) this.servers.push(server)
}, },
addDM(dmChannel: IChannel) { addDM(dmChannel: IChannel) {
if (!this.channels || this.channels.find((e) => e.id === dmChannel.id)) return; if (!this.dms || this.dms.find((e) => e.id === dmChannel.id)) return;
this.channels.push(dmChannel) this.dms.push(dmChannel)
}, },
setActive(type: string, serverId: string) { setServers(servers: Array<IServer>) {
if (serverId === '@me') { this.servers = servers
this.activeServer = {} as IServer },
setDms(dms: Array<IChannel>) {
this.dms = dms
},
setActive(type: "servers" | "dms", channelId: string) {
if (channelId === '@me') {
this.activeServer = {} as IServer | IChannel
this.activeServerType = 'dms'
return; return;
} }
console.log(this.activeServer)
this.activeServerType = type
const searchableArray: IChannel[] | IServer[] | undefined = this[type] const searchableArray: IChannel[] | IServer[] | undefined = this[type]
if (!searchableArray) return; if (!searchableArray) return;
this.activeServer = searchableArray.find((e: IServer | IChannel) => e.id === serverId) let activeServerIndex: number;
console.log(this.activeServer, searchableArray.find((e: IServer | IChannel) => e.id === serverId)) if (type === 'servers') {
activeServerIndex = searchableArray.findIndex((e) => {
return e.channels.some((channel: IChannel) => channel.id === channelId)
})
} else {
activeServerIndex = searchableArray.findIndex((e) => {
return e.id === channelId
})
}
this.activeServer = this.servers[activeServerIndex]
}, },
}, },
}) })

View File

@@ -1,30 +1,3 @@
{ {
"compilerOptions": { "extends": "./.nuxt/tsconfig.json"
"target": "ES2020", }
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"allowJs": true,
"sourceMap": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"jsx": "preserve",
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
],
"@/*": [
"./*"
]
},
},
"extends": "./.nuxt/tsconfig.json"
}

30
tsconfig.json.bak Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"allowJs": true,
"sourceMap": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"jsx": "preserve",
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
],
"@/*": [
"./*"
]
},
},
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -13,16 +13,16 @@ export type SafeUser = Omit<Omit<IUser, 'passwordhash'>, 'email'>
export interface IServer { export interface IServer {
id: string; id: string;
name: string; name: string;
channels?: Array<IChannel>; channels: Array<IChannel>;
participants: Array<SafeUser>; participants: Array<SafeUser>;
roles?: Array<IRole>; roles: Array<IRole>;
inviteCode?: Array<IInviteCode>; inviteCode?: Array<IInviteCode>;
} }
export interface IChannel { export interface IChannel {
id: string; id: string;
name: string; name: string;
server?: IServer; server: IServer;
messages?: Array<IMessage> messages?: Array<IMessage>
DM: boolean; DM: boolean;
dmParticipants?: Array<SafeUser>; dmParticipants?: Array<SafeUser>;