stream day 2

This commit is contained in:
Zoe
2023-01-03 15:11:36 -06:00
parent 0bece5d0fc
commit 47afcbb8ec
23 changed files with 605 additions and 37 deletions

30
App.vue Normal file
View File

@@ -0,0 +1,30 @@
<template>
<div>
<NuxtLayout>
<div class="flex h-screen max-h-screen">
<Nav />
<Sidebar />
<NuxtPage />
</div>
</NuxtLayout>
</div>
</template>
<script lang="ts">
import { useUserStore } from '~/stores/user'
export default {
async setup() {
const userStore = useUserStore()
const sessionToken = useCookie('sessionToken')
console.log(sessionToken.value)
if (userStore.user.id === undefined && sessionToken.value) {
const user = await $fetch('/api/getCurrentUser')
if (!user) return;
userStore.setUser(user)
}
}
}
</script>

116
components/Nav.vue Normal file
View File

@@ -0,0 +1,116 @@
<template>
<nav class="p-4 bg-zinc-800 grid grid-cols-1 grid-rows-[56px_1fr_56px] h-screen text-white relative">
<div>
<div
@click="openServer('@me')"
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"
height="32"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
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>
</div>
</div>
<div class="overflow-y-scroll my-2 flex gap-2">
<div v-for="server in servers"
:key="server.id"
@click="openServer(server.id)"
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]">
<svg width="32"
height="32"
viewBox="0 0 256 154">
<defs>
<linearGradient id="svgIDa"
x1="-2.778%"
x2="100%"
y1="32%"
y2="67.556%">
<stop offset="0%"
stop-color="#2298BD" />
<stop offset="100%"
stop-color="#0ED7B5" />
</linearGradient>
</defs>
<path fill="url(#svgIDa)"
d="M128 0C93.867 0 72.533 17.067 64 51.2C76.8 34.133 91.733 27.733 108.8 32c9.737 2.434 16.697 9.499 24.401 17.318C145.751 62.057 160.275 76.8 192 76.8c34.133 0 55.467-17.067 64-51.2c-12.8 17.067-27.733 23.467-44.8 19.2c-9.737-2.434-16.697-9.499-24.401-17.318C174.249 14.743 159.725 0 128 0ZM64 76.8C29.867 76.8 8.533 93.867 0 128c12.8-17.067 27.733-23.467 44.8-19.2c9.737 2.434 16.697 9.499 24.401 17.318C81.751 138.857 96.275 153.6 128 153.6c34.133 0 55.467-17.067 64-51.2c-12.8 17.067-27.733 23.467-44.8 19.2c-9.737-2.434-16.697-9.499-24.401-17.318C110.249 91.543 95.725 76.8 64 76.8Z" />
</svg>
</div>
</div>
<div>
<div @click="createServerModelOpen = true"
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"
height="32"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m-7-7h14" />
</svg>
</div>
</div>
</nav>
<div v-if="createServerModelOpen"
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
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">
Create a server:
</h2>
<div>
<form @submit.prevent="createServer"
class="w-3/5">
<input v-model="serverName"
type="text"
class="py-2 px-3 rounded-md mb-2 bg-zinc-700 shadow-md border border-zinc-700/80"
placeholder="Server name" />
<input type="submit"
class="py-2 px-3 rounded-md bg-zinc-700 shadow-md border border-zinc-700/80" />
</form>
</div>
</div>
</div>
</template>
<script lang="ts">
import { useServerStore } from '~/stores/servers'
export default {
data() {
return {
servers: useServerStore().servers,
createServerModelOpen: false,
serverName: ''
}
},
methods: {
async createServer() {
const serverStore = useServerStore();
const { server } = await $fetch('/api/channel/create', { method: 'post', body: { serverName: this.serverName } })
this.createServerModelOpen = false;
this.serverName = '';
serverStore.addServer(server)
},
openServer(id: string): void {
const router = useRouter();
useServerStore().servers.find((e: unknown, i: number) => {
if (e.id === id) {
useServerStore().activeServer = i
}
})
router.push({ path: `/channel/${id}` });
}
}
}
</script>

64
components/Sidebar.vue Normal file
View File

@@ -0,0 +1,64 @@
<template>
<div class="flex bg-zinc-700 w-60 h-screen shadow-sm text-white select-none">
<div class="w-full"
v-if="server">
<div class="flex p-4 border-b border-zinc-600/80">
<h4 class="text-lg font-semibold w-fit ">
{{ server.name }}
</h4>
</div>
<div class="flex gap-y-1.5 px-1.5 mt-2 flex-col">
<div class="flex text-center hover:bg-zinc-600/70 px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm"
v-for="channel in server.channels"
:key="channel.id">
<svg width="24"
height="24"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16" />
</svg> {{ channel.name }}
</div>
<div class="flex text-center hover:bg-zinc-600/70 px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm"
v-for="channel in server.channels"
:key="channel.id">
<svg width="24"
height="24"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16" />
</svg> {{ channel.name }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { useUserStore } from '~/stores/user'
import { useServerStore } from '~/stores/servers'
export default {
data() {
return {
user: useUserStore().user,
server: useServerStore().servers[useServerStore().activeServer]
}
},
mounted() {
const route = useRoute()
if (route.path.includes('@me')) {
this.server = useServerStore().dms[useServerStore().activeServer]
}
}
}
</script>

View File

@@ -1,20 +0,0 @@
<template>
<div class="hover:scale-105 cursor-pointer duration-500 flex flex-col justify-center items-center text-center rounded shadow-xl border-2 border-gray-500 h-full w-full p-6">
<h2 class="text-lg text-gray-700">{{name}}</h2>
<p class="text-sm text-gray-600">{{description}}</p>
<a
class="text-sm text-violet-500 underline decoration-dotted underline-offset-2 cursor-pointer mt-3"
href={{documentation}}
target="_blank"
rel="noreferrer"
>
Documentation
</a>
</div>
</template>
<script lang="ts">
export default {
props: ['name', 'description', 'documentation']
}
</script>

View File

@@ -20,4 +20,18 @@ export default {
autoprefixer: {},
},
},
modules: [
[
'@pinia/nuxt',
{
autoImports: [
// automatically imports `defineStore`
'defineStore', // import { defineStore } from 'pinia'
// automatically imports `defineStore` as `definePiniaStore`
['defineStore', 'definePiniaStore'], // import { defineStore as definePiniaStore } from 'pinia'
],
},
],
],
}

90
package-lock.json generated
View File

@@ -5,9 +5,11 @@
"packages": {
"": {
"dependencies": {
"@pinia/nuxt": "^0.4.6",
"@prisma/client": "^4.8.0",
"bcryptjs": "^2.4.3",
"nuxt": "^3.0.0",
"pinia": "^2.0.28",
"uuid": "^9.0.0"
},
"devDependencies": {
@@ -831,6 +833,18 @@
"vue": "^3.2.45"
}
},
"node_modules/@pinia/nuxt": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.4.6.tgz",
"integrity": "sha512-HjrYEfLdFpmsjhicPJgL36jVhzHWukIQPFFHGTSF84Cplu+f2nY2XHKqe9ToHzE9rLee2RjLOwAzOnXa/I/u6A==",
"dependencies": {
"@nuxt/kit": "^3.0.0",
"pinia": ">=2.0.27"
},
"funding": {
"url": "https://github.com/sponsors/posva"
}
},
"node_modules/@prisma/client": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.8.0.tgz",
@@ -5065,6 +5079,56 @@
"node": ">=0.10.0"
}
},
"node_modules/pinia": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.28.tgz",
"integrity": "sha512-YClq9DkqCblq9rlyUual7ezMu/iICWdBtfJrDt4oWU9Zxpijyz7xB2xTwx57DaBQ96UGvvTMORzALr+iO5PVMw==",
"dependencies": {
"@vue/devtools-api": "^6.4.5",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/pkg-types": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz",
@@ -8037,6 +8101,15 @@
"vue-bundle-renderer": "^1.0.0"
}
},
"@pinia/nuxt": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.4.6.tgz",
"integrity": "sha512-HjrYEfLdFpmsjhicPJgL36jVhzHWukIQPFFHGTSF84Cplu+f2nY2XHKqe9ToHzE9rLee2RjLOwAzOnXa/I/u6A==",
"requires": {
"@nuxt/kit": "^3.0.0",
"pinia": ">=2.0.27"
}
},
"@prisma/client": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.8.0.tgz",
@@ -11029,6 +11102,23 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
},
"pinia": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.28.tgz",
"integrity": "sha512-YClq9DkqCblq9rlyUual7ezMu/iICWdBtfJrDt4oWU9Zxpijyz7xB2xTwx57DaBQ96UGvvTMORzALr+iO5PVMw==",
"requires": {
"@vue/devtools-api": "^6.4.5",
"vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
}
}
},
"pkg-types": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz",

View File

@@ -7,9 +7,11 @@
"prepare": "nuxi prepare"
},
"dependencies": {
"@pinia/nuxt": "^0.4.6",
"@prisma/client": "^4.8.0",
"bcryptjs": "^2.4.3",
"nuxt": "^3.0.0",
"pinia": "^2.0.28",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@@ -0,0 +1,16 @@
<template>
hello world
</template>
<script>
import { useServerStore } from '~/stores/servers'
export default {
async setup() {
const route = useRoute()
const { server } = await $fetch(`/api/channel/${route.params.dmId}`)
if (!useServerStore().dms.includes(server)) useServerStore().addDM(server);
}
}
</script>

View File

@@ -0,0 +1,26 @@
<template>
<form @submit.prevent="startDM">
<input v-model="userId" />
<input type="submit" />
</form>
</template>
<script>
import { useServerStore } from '~/stores/servers'
export default {
data() {
return {
userId: ''
}
},
methods: {
async startDM() {
const { server } = await $fetch('/api/channel/createDM', { method: 'post', body: { partnerId: this.userId } })
useServerStore().addDM(server)
useRouter().push({ path: '/channel/@me/' + server.id })
}
}
}
</script>

View File

@@ -2,6 +2,11 @@
hello world
</template>
<script setup>
$fetch('/api/getChannelById', { params: { test: 123 } })
<script setup async>
import { useServerStore } from '~/stores/servers'
const route = useRoute()
const { server } = await $fetch(`/api/channel/${route.params.id}`)
if (!useServerStore().servers.includes(server)) useServerStore().addServer(server);
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div>
Hello there
Hello there traveler
<nuxt-link to="/login">Login</nuxt-link>
<nuxt-link to="/signup">Signup</nuxt-link>
</div>

View File

@@ -15,6 +15,8 @@
</template>
<script>
import { useUserStore } from '~/stores/user'
export default {
data() {
return {
@@ -36,6 +38,8 @@ export default {
userId.value = user.userId
const token = useCookie('sessionToken')
token.value = user.token
useUserStore().setUser(user)
}
}
}

View File

@@ -19,6 +19,8 @@
</template>
<script>
import { useUserStore } from '~/stores/user'
export default {
data() {
return {
@@ -42,6 +44,8 @@ export default {
userId.value = user.userId
const token = useCookie('sessionToken')
token.value = user.token
useUserStore().setUser(user)
}
}
}

View File

@@ -22,18 +22,24 @@ model Server {
id String @id @default(cuid())
name String
participants User[]
channels Channel[]
}
model Room {
model Channel {
id String @id @default(cuid())
name String
Server Server? @relation(fields: [serverId], references: [id])
serverId String?
Message Message[]
}
model Message {
id String @id @default(cuid())
body String
channel Channel @relation(fields: [channelId], references: [id])
creator User @relation(fields: [userId], references: [id])
userId String
channelId String
}
model Session {

View File

@@ -2,11 +2,11 @@ import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (!event.context.authenticated) return {
if (!event.context.user.authenticated) return {
message: 'You must be logged in to view a channel.'
}
if (!event.context.params.channelId) {
if (!event.context.params.id) {
event.node.res.statusCode = 400;
return {
message: 'A channelId is required'
@@ -15,10 +15,21 @@ export default defineEventHandler(async (event) => {
const server = await prisma.server.findFirst({
where: {
id: event.context.params.channelId
id: event.context.params.id
},
include: {
participants: true,
channels: true
}
})
if (!server) {
event.node.res.statusCode = 404;
return {
message: `Channel with id "${event.context.params.id}" not found`
}
}
return {
server
}

View File

@@ -0,0 +1,55 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) {
event.node.res.statusCode = 401;
return {
message: 'You must be logged in to view a channel.'
}
}
const { serverName } = await readBody(event)
if (!serverName) {
event.node.res.statusCode = 400;
return {
message: 'channel name is required to create a channel.'
}
}
const preExistingServer = await prisma.server.findFirst({
where: {
name: serverName
}
})
if (preExistingServer) {
event.node.res.statusCode = 409;
return {
message: `Server with name ${serverName} already exists.`
}
}
const server = await prisma.server.create({
data: {
name: serverName,
participants: { connect: [{ id: event.context.user.id }] },
channels: {
create: [
{
name: 'general',
},
]
}
},
include: {
channels: true,
participants: true
}
})
return {
server
}
})

View File

@@ -0,0 +1,67 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) {
event.node.res.statusCode = 401;
return {
message: 'You must be logged in to view a channel.'
}
}
const { partnerId } = await readBody(event)
if (!partnerId) {
event.node.res.statusCode = 400;
return {
message: 'A friend is required to create a DM.'
}
}
const partner = await prisma.user.findFirst({
where: {
id: partnerId
}
})
const user = await prisma.user.findFirst({
where: {
id: event.context.user.id
}
})
const preExistingServer = await prisma.server.findFirst({
where: {
name: `${user.username} and ${partner.username}`
}
})
if (preExistingServer) {
event.node.res.statusCode = 409;
return {
message: `DM already exists.`
}
}
const server = await prisma.server.create({
data: {
name: `${user.username} and ${partner.username}`,
participants: { connect: [{ id: event.context.user.id }, { id: partner.id }] },
channels: {
create: [
{
name: 'default',
},
]
}
},
include: {
channels: true,
participants: true
}
})
return {
server
}
})

View File

@@ -0,0 +1,21 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (event.context.user === undefined || event.context.user.id === undefined) {
// event.node.res.statusCode = 401;
return {
message: "Unauthenticated"
}
}
const user = await prisma.user.findFirst({
where: {
id: event.context.user.id
}
})
user.passwordhash = undefined;
return user
})

View File

@@ -13,6 +13,26 @@ export default defineEventHandler(async (event) => {
}
}
const preExistingUser = await prisma.user.findFirst({
where: {
OR: [
{
username: body.username
},
{
email: body.email
}
]
}
})
if (preExistingUser) {
event.node.res.statusCode = 409;
return {
message: `User with username ${body.username} or email ${body.email} already exists`
}
}
const passwordhash = await bcryptjs.hash(body.password, 10)
const user = await prisma.user.create({

View File

@@ -4,7 +4,12 @@ const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
const cookies = parseCookies(event)
if (!cookies.sessionToken) return;
console.log(cookies.sessionToken)
if (!cookies.sessionToken) {
event.context.user = { authenticated: false }
return;
}
const session = await prisma.session.findFirst({
where: {
@@ -12,7 +17,10 @@ export default defineEventHandler(async (event) => {
}
})
if (!session) return;
if (!session) {
event.context.user = { authenticated: false }
return;
}
event.context.authenticated = true;
event.context.user = { authenticated: true, id: session.userId };
})

15
stores/servers.ts Normal file
View File

@@ -0,0 +1,15 @@
export const useServerStore = defineStore('server', {
state: () => ({
servers: [],
dms: [],
activeServer: undefined
}),
actions: {
addServer(server) {
this.servers.push(server)
},
addDM(server) {
this.dms.push(server)
}
},
})

10
stores/user.ts Normal file
View File

@@ -0,0 +1,10 @@
export const useUserStore = defineStore('user', {
state: () => ({
user: {}
}),
actions: {
setUser(user) {
this.user = user;
}
},
})

4
types/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface IUser {
id: string;
username: string;
}