Move folder
This commit is contained in:
12
src/App.vue
Normal file
12
src/App.vue
Normal 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
3
src/assets/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
22
src/assets/qualitySelector.ts
Normal file
22
src/assets/qualitySelector.ts
Normal 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 ?? ''
|
||||
})
|
||||
}
|
32
src/components/NavbarView.vue
Normal file
32
src/components/NavbarView.vue
Normal 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>
|
72
src/components/TwitchChat.vue
Normal file
72
src/components/TwitchChat.vue
Normal 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>
|
37
src/components/VideoPlayer.vue
Normal file
37
src/components/VideoPlayer.vue
Normal 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
40
src/main.ts
Normal 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
32
src/router/index.ts
Normal 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
|
80
src/views/CategoryView.vue
Normal file
80
src/views/CategoryView.vue
Normal 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>
|
72
src/views/HomepageView.vue
Normal file
72
src/views/HomepageView.vue
Normal 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>
|
11
src/views/PageNotFound.vue
Normal file
11
src/views/PageNotFound.vue
Normal 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>
|
24
src/views/PrivacyPageView.vue
Normal file
24
src/views/PrivacyPageView.vue
Normal 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
191
src/views/UserView.vue
Normal 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>
|
Reference in New Issue
Block a user