Move folder

This commit is contained in:
dragongoose
2023-03-24 07:55:10 -04:00
parent 6265fa182b
commit 1e9726a601
29 changed files with 86 additions and 121 deletions

12
src/App.vue Normal file
View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import NavbarItem from './components/NavbarView.vue'
</script>
<template>
<navbar-item></navbar-item>
<Suspense>
<RouterView />
</Suspense>
</template>

3
src/assets/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,22 @@
import 'videojs-contrib-quality-levels'
import type { QualityLevelList } from 'videojs-contrib-quality-levels'
export const createQualitySelector = (player: any) => {
const qualityLevels: QualityLevelList = player.qualityLevels()
const myButton = player.controlBar.addChild('button')
const myButtonDom = myButton.el()
myButtonDom.innerHTML = 'Hello'
myButtonDom.addEventListener('click', () => {})
qualityLevels.on('change', function () {
console.log('Quality Level changed!')
console.log('New level:', qualityLevels[qualityLevels.selectedIndex])
console.log(qualityLevels)
const qualityLabel = qualityLevels[qualityLevels.selectedIndex].height?.toString() + 'p'
myButtonDom.textContent = qualityLabel ?? ''
})
}

View File

@ -0,0 +1,32 @@
<script lang="ts">
export default {}
</script>
<template>
<div class="p-4 flex items-center justify-between bg-ctp-base text-white">
<h1 class="font-bold text-2xl">Naqvbar</h1>
<div>
<form class="relative">
<label for="searchBar" class="hidden">Search</label>
<v-icon
name="io-search-outline"
class="text-black absolute my-auto inset-y-0 left-2"
></v-icon>
<input
type="text"
id="searchBar"
name="searchBar"
placeholder="Search"
class="rounded-md p-1 pl-8 text-black"
/>
</form>
</div>
<ul class="inline-flex space-x-6 font-medium">
<router-link to="">Github</router-link>
<router-link to="/preferences">Preferences</router-link>
<router-link to="/about">About</router-link>
</ul>
</div>
</template>

View File

@ -0,0 +1,72 @@
<script lang="ts">
import { ref, type Ref } from 'vue'
export default {
props: {
isLive: {
type: Boolean,
default() {
return false
}
},
channelName: {
type: String
}
},
setup(props) {
let messages: Ref<
{ username: string; channel: string; message: string; messageType: string }[]
> = ref([])
let ws = new WebSocket('ws://localhost:7000')
return {
ws,
messages,
props
}
},
mounted() {
const chatList = this.$refs.chatList as Element
const chatStatusMessage = this.$refs.initConnectingStatus as Element
this.ws.onmessage = (message) => {
if (message.data == 'OK') {
chatStatusMessage.textContent = `Connected to ${this.channelName}`
} else {
this.messages.push(JSON.parse(message.data))
this.scrollToBottom(chatList)
}
}
this.ws.onopen = (data) => {
console.log(data)
this.ws.send('JOIN ' + this.props.channelName?.toLowerCase())
}
},
methods: {
getChat() {
return this.messages
},
scrollToBottom(el: Element) {
el.scrollTop = el.scrollHeight
}
}
}
</script>
<template>
<div v-if="isLive" class="p-3 bg-ctp-crust rounded-lg w-full max-w-[15.625rem] flex flex-col">
<ul class="overflow-y-scroll h-[46.875rem]" ref="chatList">
<li>
<p ref="initConnectingStatus" class="text-gray-500 text-sm italic"> Connecting to {{ channelName }}.</p>
</li>
<li v-for="message in getChat()" :key="messages.indexOf(message)">
<div class="text-white inline-flex">
<p class="text-sm">
<strong class="text-ctp-pink font-bold text-sm">{{ message.username }}</strong
>: {{ message.message }}
</p>
</div>
</li>
</ul>
</div>
</template>

View File

@ -0,0 +1,37 @@
<template>
<div>
<video id="video-player" class="video-js vjs-defaultskin"></video>
</div>
</template>
<script lang="ts">
// Importing video-js
import videojs from 'video.js'
import qualityLevels from 'videojs-contrib-quality-levels'
videojs.registerPlugin('qualityLevels', qualityLevels)
export default {
name: 'VideoJsPlayer',
props: {
options: {
type: Object,
default() {
return {}
}
}
},
data() {
let player: any
return {
player
}
},
// initializing the video player
// when the component is being mounted
mounted() {
this.player = videojs('video-player', this.options, () => {
this.player.hlsQualitySelector({ displayCurrentQuality: true })
})
}
}
</script>

40
src/main.ts Normal file
View File

@ -0,0 +1,40 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/index.css'
import 'video.js/dist/video-js.css'
const app = createApp(App)
import { OhVueIcon, addIcons } from 'oh-vue-icons'
import {
IoSearchOutline,
IoLink,
FaCircleNotch,
BiTwitter,
BiInstagram,
BiDiscord,
BiYoutube,
BiTiktok,
BiHeartFill,
IoPerson
} from 'oh-vue-icons/icons'
addIcons(
IoSearchOutline,
IoLink,
FaCircleNotch,
BiTwitter,
BiInstagram,
BiDiscord,
BiYoutube,
BiTiktok,
BiHeartFill,
IoPerson
)
app.component('v-icon', OhVueIcon)
app.use(router)
app.mount('#app')

32
src/router/index.ts Normal file
View File

@ -0,0 +1,32 @@
import { createRouter, createWebHistory } from 'vue-router'
import UserView from '../views/UserView.vue'
import PageNotFound from '../views/PageNotFound.vue'
import PrivacyPageView from '../views/PrivacyPageView.vue'
import HomepageView from '../views/HomepageView.vue'
import CategoryView from '../views/CategoryView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: HomepageView
},
{
path:'/game/:game',
component: CategoryView
},
{
path: '/privacy',
name: 'about',
component: PrivacyPageView
},
{
path: '/:username',
component: UserView
},
{ path: '/:pathMatch(.*)*', component: PageNotFound }
]
})
export default router

View File

@ -0,0 +1,80 @@
<script lang="ts">
import { useRoute } from 'vue-router'
export default {
async setup() {
const route = useRoute()
const game = route.params.game
const res = await fetch(`http://localhost:7000/api/discover/${game}`)
return {
data: await res.json()
}
},
methods: {
abbreviate(text: number) {
return Intl.NumberFormat('en-US', {
//@ts-ignore
notation: "compact",
maximumFractionDigits: 1
}).format(text)
}
}
}
</script>
<template>
<div class="flex flex-col max-w-5xl mx-auto">
<div class="flex space-x-4 p-3">
<img :src="data.cover" class="self-start rounded-md">
<div>
<h1 class="font-bold text-5xl text-white">{{ data.name }}</h1>
<div class="inline-flex my-1 space-x-3">
<p class="font-bold text-white text-lg">Followers: {{ abbreviate(data.followers) }}</p>
<p class="font-bold text-white text-lg">Viewers: {{ abbreviate(data.viewers) }}</p>
</div>
<ul class="mb-5">
<li v-for="tag in data.tags" :key="tag" class="inline-flex">
<span class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm">{{ tag }}</span>
</li>
</ul>
<p class="text-md text-gray-400 overflow-y-auto">{{ data.description }}</p>
</div>
</div>
<div class="max-w-[58rem] mx-auto">
<ul>
<li v-for="stream in data.streams" :key="stream" class="inline-flex m-2 hover:scale-105 transition-transform">
<div class="bg-ctp-crust rounded-lg">
<a :href="`http://localhost:5173/${stream.streamer.name}`">
<img :src="stream.preview" class="rounded-lg rounded-b-none">
</a>
<div class="text-white p-2 inline-flex space-x-2 w-full">
<div class="inline-flex w-full">
<div class="inline-flex">
<img :src="stream.streamer.pfp" class="rounded-full mr-2">
<div>
<p class="font-bold w-[22.9rem] truncate">{{ stream.title }}</p>
<div class="inline-flex w-full justify-between">
<p class="text-gray-300">{{ stream.streamer.name }}</p>
<p class="self-end float-right"> <v-icon name="io-person"></v-icon> {{ abbreviate(stream.viewers) }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
</template>

View File

@ -0,0 +1,72 @@
<script lang="ts">
export default {
async setup() {
const res = await fetch(`http://localhost:7000/api/discover`)
return {
data: await res.json()
}
},
methods: {
abbreviate(text: number) {
return Intl.NumberFormat('en-US', {
//@ts-ignore
notation: "compact",
maximumFractionDigits: 1
}).format(text)
}
}
}
</script>
<template>
<div class="max-w-5xl mx-auto">
<div class="p-2">
<h1 class="font-bold text-5xl text-white"> Discover </h1>
<p class="text-xl text-white"> Sort through popular categories</p>
<div class="pt-5 inline-flex text-white">
<p class="mr-2 font-bold text-white">Filter by tag</p>
<form class="relative">
<label for="searchBar" class="hidden">Search</label>
<v-icon
name="io-search-outline"
class="absolute my-auto inset-y-0 left-2"
></v-icon>
<input
type="text"
id="searchBar"
name="searchBar"
placeholder="Search"
class="rounded-md p-1 pl-8 text-black bg-neutral-500 placeholder:text-white"
/>
</form>
</div>
</div>
<ul class="">
<li v-for="category in data" :key="category" class="inline-flex m-2 hover:scale-105 transition-transform">
<div class="bg-ctp-crust max-w-[13.5rem] rounded-lg">
<a :href="`http://localhost:5173/game/${category.name}`">
<img :src="category.image" class="rounded-lg rounded-b-none">
</a>
<div class="p-2">
<div>
<p class="font-bold text-white text-xl"> {{ category.displayName }}</p>
<p class="text-sm text-white"> {{ abbreviate(category.viewers) }} viewers</p>
</div>
<ul class="h-8 overflow-hidden">
<li v-for="tag in category.tags" :key="tag" class="inline-flex">
<span class="p-2.5 py-1.5 bg-ctp-surface0 rounded-md m-0.5 text-xs font-bold text-white">{{ tag }}</span>
</li>
</ul>
</div>
</div>
</li>
</ul>
</div>
</template>

View File

@ -0,0 +1,11 @@
<script lang="ts">
export default {}
</script>
<template>
<div class="flex flex-col items-center pt-10 font-bold text-5xl text-white">
<h1>oops....</h1>
<h1>this page wasn't found()</h1>
<h2 class="text-4xl">maybe go <RouterLink to="/" class="text-gray-500">home</RouterLink>?</h2>
</div>
</template>

View File

@ -0,0 +1,24 @@
<script lang="ts">
export default {}
</script>
<template>
<article
class="prose prose-invert border-2 border-ctp-peach max-w-prose bg-ctp-crust rounded-lg mx-auto p-8 pt-10 text-white"
>
<h1>Privacy Policy</h1>
<p>
For the oficial instance, no logs are kept except for when an error is met that affects the
user is encounered. An example of this is when data retrieval fails when a user requests. No
identifying information is kept except for the time of request. below is an example of this
data
</p>
<code class="">
{ "endpoint":"/api/users/chibidoki", "level":"warn","message": "No element found for selector:
li.InjectLayout-sc-1i43xsx-0:nth-child(2) > a:nth-child(1) > div:nth-child(1) >
div:nth-child(1) > p:nth-child(1)", "origin":"http://localhost:5173",
"reqId":"fed6f1f6-403f-4d6a-9943-3d07ea7bf9bb", "timestamp":"2023-03-07T22:42:37.982Z" }
</code>
</article>
</template>

191
src/views/UserView.vue Normal file
View File

@ -0,0 +1,191 @@
<script lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import type { StreamerData } from '../../../server/types/scraping/Streamer'
import VideoPlayer from '../components/VideoPlayer.vue'
import TwitchChat from '../components/TwitchChat.vue'
export default {
async setup() {
const route = useRoute()
const username = route.params.username
const getUser = async () => {
const res = await fetch(`http://localhost:7000/api/users/${username}`)
if (res.status !== 200) {
const data = await res.json()
if (!data.code) {
return {
status: 'error',
code: 'error'
}
}
return {
...data
}
}
const data: StreamerData = await res.json()
data.pfp = `http://localhost:7000/proxy/img?imageUrl=${encodeURIComponent(data.pfp)}`
return data
}
const data = ref()
onMounted(async () => {
const fetchedUser = await getUser()
data.value = fetchedUser
})
return {
data,
videoOptions: {
autoplay: true,
controls: true,
sources: [
{
src: `http://localhost:7000/proxy/stream/${username}/hls.m3u8`,
type: 'application/vnd.apple.mpegurl'
}
],
fluid: true
}
}
},
components: {
VideoPlayer,
TwitchChat
},
methods: {
truncate(value: string, length: number) {
if (value.length > length) {
return value.substring(0, length) + '...'
} else {
return value
}
}
}
}
</script>
<template>
<div
v-if="!data"
id="loadingDiv"
class="flex mx-auto justify-center bg-ctp-crust rounded-lg w-2/3 p-2 text-white"
>
<div class="flex space-x-3">
<h1 class="text-4xl font-bold">Searching...</h1>
<v-icon name="fa-circle-notch" class="animate-spin w-10 h-10"></v-icon>
</div>
</div>
<div
v-else-if="data.status === 'error'"
class="flex flex-col max-w-prose justify-center text-center mx-auto p-6 bg-ctp-crust rounded-lg text-white"
>
<div class="mb-6">
<h1 class="font-bold text-5xl">oops...</h1>
<p class="font-bold text-3xl">this wasn't supposed to happen</p>
</div>
<p class="text-xl">
the server was encountered an error while retriving the data, and now we're here :3
</p>
<div class="mt-5">
<p class="text-xl">please contact the administrator with this code</p>
<p class="text-xl">error identifier: {{ data.code }}</p>
</div>
</div>
<div v-else class="w-full justify-center inline-flex space-x-4 p-4">
<div class="flex bg-ctp-crust flex-col p-6 rounded-lg max-w-prose min-w-[65ch] text-white">
<div v-if="data.isLive" class="w-full mx-auto rounded-lg mb-5">
<video-player :options="videoOptions"> </video-player>
</div>
<div class="w-full flex-wrap p-3">
<div class="inline-flex w-2/3">
<div class="w-20 h-20 relative">
<img
:src="data.pfp"
class="rounded-full border-4 p-0.5 w-auto h-20"
:style="`border-color: ${data.colorHex};`"
/>
<span
v-if="data.isLive"
class="absolute top-16 right-[1.2rem] bg-ctp-red font-bold text-sm p-1.5 py-0.5 rounded-md"
>LIVE</span
>
</div>
<div class="ml-3 content-between">
<h1 class="text-4xl font-bold">{{ data.username }}</h1>
<h1 v-if="!data.stream" class="font-bold text-md self-end">
{{ data.followersAbbv }} Followers
</h1>
<div v-else class="w-[17rem]">
<p class="text-sm font-bold text-gray-200 self-end">
{{ truncate(data.stream.title, 130) }}
</p>
</div>
</div>
</div>
<div class="inline-flex w-1/3 float-right h-full text-right">
<div v-if="!data.isLive" class="w-full">
<p
class="font-bold bg-ctp-mantle p-3 py-2 rounded-lg w-min float-right border-2 border-ctp-red"
>
OFFLINE
</p>
</div>
<div v-else class="w-full">
<ul class="text-xs font-bold text-right space-x-1 space-y-1 overflow-y-auto">
<li
v-for="tag in data.stream.tags"
:key="tag"
class="inline-flex bg-ctp-mantle p-1.5 px-2 rounded-md"
>
{{ tag }}
</li>
</ul>
</div>
</div>
<div class="pt-2 pl- inline-flex">
<button class="text-white text-sm font-bold p-2 py-1 rounded-md bg-purple-600">
<v-icon name="bi-heart-fill" scale="0.85"></v-icon>
Follow
</button>
<p class="align-baseline font-bold ml-3">{{ data.followersAbbv }} Followers</p>
</div>
</div>
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
<div class="inline-flex w-full">
<span class="pr-3 font-bold text-3xl">About</span>
</div>
<p class="mb-5">{{ data.about }}</p>
<hr class="my-auto w-full bg-gray-200 rounded-full opacity-40" />
<ul class="flex font-semibold text-md justify-start flex-wrap flex-row">
<li v-for="link in data.socials" :key="link">
<a :href="link.link" class="text-white hover:text-gray-400 mr-4">
<v-icon :name="`bi-${link.type}`" class="w-6 h-6 mr-1"></v-icon>
<span>{{ link.name }}</span>
</a>
</li>
</ul>
</div>
</div>
<twitch-chat :isLive="data.isLive" :channelName="data.username"></twitch-chat>
</div>
</template>