mirror of
https://codeberg.org/SafeTwitch/safetwitch.git
synced 2024-12-22 13:22:58 -05:00
Add following page and chat badges
This commit is contained in:
parent
620a394211
commit
354ae5a959
7 changed files with 180 additions and 39 deletions
|
@ -1,16 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
interface Badge {
|
interface Badge {
|
||||||
id: string,
|
id: string
|
||||||
title: string,
|
title: string
|
||||||
setId: string,
|
setId: string
|
||||||
version: string,
|
version: string
|
||||||
images: { [k:string]: string }
|
images: { [k: string]: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
badgeInfo: {
|
badgeInfo: {
|
||||||
type: Object,
|
type: Object
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -25,5 +25,5 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img :src="src" class="w-3.5 h-3.5 rounded-sm">
|
<img :src="src" class="w-3.5 h-3.5 rounded-sm" />
|
||||||
</template>
|
</template>
|
62
src/components/FollowButton.vue
Normal file
62
src/components/FollowButton.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
isFollowing: ref(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
followStreamer() {
|
||||||
|
const username = this.$props.username
|
||||||
|
const follows = localStorage.getItem('following')
|
||||||
|
let parsedFollows: string[] = []
|
||||||
|
|
||||||
|
if (this.isFollowing && follows) {
|
||||||
|
const index = JSON.parse(follows).indexOf(username)
|
||||||
|
if (index === -1) return
|
||||||
|
parsedFollows = parsedFollows.splice(index, 1)
|
||||||
|
this.isFollowing = false
|
||||||
|
} else {
|
||||||
|
if (follows) parsedFollows = JSON.parse(follows)
|
||||||
|
parsedFollows.push(username)
|
||||||
|
this.isFollowing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('following', JSON.stringify(parsedFollows))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let followerData = localStorage.getItem('following')
|
||||||
|
if (!followerData) return
|
||||||
|
|
||||||
|
let following: string[] = JSON.parse(followerData)
|
||||||
|
const isFollower = following.includes(this.$props.username)
|
||||||
|
|
||||||
|
if (isFollower) {
|
||||||
|
this.isFollowing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
ref="followButton"
|
||||||
|
@click="followStreamer"
|
||||||
|
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>
|
||||||
|
<span v-if="isFollowing"> Unfollow </span>
|
||||||
|
<span v-else> Follow </span>
|
||||||
|
</button>
|
||||||
|
</template>
|
76
src/components/StreamPreview.vue
Normal file
76
src/components/StreamPreview.vue
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export interface Stream {
|
||||||
|
tags: string[]
|
||||||
|
title: string
|
||||||
|
topic: string
|
||||||
|
startedAt: number
|
||||||
|
viewers: number
|
||||||
|
preview: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
stream: {},
|
||||||
|
name: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setup(props) {
|
||||||
|
let streamData: Stream
|
||||||
|
if (!props.stream && props.name) {
|
||||||
|
const streamDataFetch = await fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/api/users/${props.name}`
|
||||||
|
)
|
||||||
|
const data = await streamDataFetch.json()
|
||||||
|
|
||||||
|
if (!data.stream) return
|
||||||
|
|
||||||
|
data.stream.streamer = { name: props.name, pfp: data.pfp }
|
||||||
|
streamData = data.stream
|
||||||
|
} else {
|
||||||
|
streamData = props.stream as Stream
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontend_url = import.meta.env.VITE_INSTANCE_URL
|
||||||
|
|
||||||
|
return {
|
||||||
|
frontend_url,
|
||||||
|
streamData
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
abbreviate(text: number) {
|
||||||
|
return Intl.NumberFormat('en-US', {
|
||||||
|
//@ts-ignore
|
||||||
|
notation: 'compact',
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
}).format(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-ctp-crust rounded-lg w-[27rem]">
|
||||||
|
<a :href="`${this.frontend_url}/${streamData.streamer.name}`">
|
||||||
|
<img :src="streamData.preview" class="rounded-lg rounded-b-none" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="text-white p-2 inline-flex space-x-2 w-full h-16">
|
||||||
|
<div class="inline-flex">
|
||||||
|
<div class="inline-flex">
|
||||||
|
<img :src="streamData.streamer.pfp" class="rounded-full mr-2" />
|
||||||
|
<div class="w-full">
|
||||||
|
<p class="font-bold w-[22.9rem] truncate">{{ streamData.title }}</p>
|
||||||
|
<div class="inline-flex w-full justify-between">
|
||||||
|
<p class="text-gray-300">{{ streamData.streamer.name }}</p>
|
||||||
|
<p class="self-end float-right">
|
||||||
|
<v-icon name="io-person"></v-icon> {{ abbreviate(streamData.viewers) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ref, type Ref } from 'vue'
|
import { ref, type Ref } from 'vue'
|
||||||
import BadgeVue from './Badge.vue'
|
import BadgeVue from './ChatBadge.vue'
|
||||||
|
|
||||||
interface Badge {
|
interface Badge {
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -58,7 +58,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onopen = (data) => {
|
this.ws.onopen = () => {
|
||||||
this.ws.send('JOIN ' + this.props.channelName?.toLowerCase())
|
this.ws.send('JOIN ' + this.props.channelName?.toLowerCase())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -120,7 +120,7 @@ export default {
|
||||||
<p class="text-sm items-center">
|
<p class="text-sm items-center">
|
||||||
|
|
||||||
<ul class="inline-flex space-x-1 pr-1">
|
<ul class="inline-flex space-x-1 pr-1">
|
||||||
<li v-for="badge in getBadges(message)">
|
<li v-for="badge in getBadges(message)" :key="badge.id">
|
||||||
<badge-vue :badge-info="badge"></badge-vue>
|
<badge-vue :badge-info="badge"></badge-vue>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import StreamPreviewVue from '@/components/StreamPreview.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async setup() {
|
async setup() {
|
||||||
|
@ -22,6 +23,9 @@ export default {
|
||||||
maximumFractionDigits: 1
|
maximumFractionDigits: 1
|
||||||
}).format(text)
|
}).format(text)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
StreamPreviewVue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -57,28 +61,7 @@ export default {
|
||||||
:key="stream"
|
:key="stream"
|
||||||
class="inline-flex m-2 hover:scale-105 transition-transform"
|
class="inline-flex m-2 hover:scale-105 transition-transform"
|
||||||
>
|
>
|
||||||
<div class="bg-ctp-crust rounded-lg">
|
<StreamPreviewVue :stream="stream"></StreamPreviewVue>
|
||||||
<a :href="`${frontend_url}/${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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ref, type Ref } from 'vue'
|
import { ref, type Ref } from 'vue'
|
||||||
|
import StreamPreviewVue from '@/components/StreamPreview.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async setup() {
|
async setup() {
|
||||||
|
@ -11,7 +12,8 @@ export default {
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
frontend_url,
|
frontend_url,
|
||||||
filterTags: ''
|
filterTags: '',
|
||||||
|
following: ref([])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -73,12 +75,32 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getNextCategory()
|
this.getNextCategory()
|
||||||
|
|
||||||
|
let following = localStorage.getItem('following')
|
||||||
|
if (following) {
|
||||||
|
this.following = JSON.parse(following)
|
||||||
|
} else {
|
||||||
|
this.following = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
StreamPreviewVue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="max-w-5xl mx-auto">
|
<div class="max-w-5xl mx-auto">
|
||||||
|
<div v-if="following.length > 0" class="p-2 text-white">
|
||||||
|
<h1 class="font-bold text-5xl">Following</h1>
|
||||||
|
<p class="text-xl">Streamers you follow</p>
|
||||||
|
<ul class="flex overflow-x-scroll flex-nowrap h-80 space-x-1">
|
||||||
|
<li v-for="streamer in following" :key="streamer" class="inline-block">
|
||||||
|
<stream-preview-vue :name="streamer"></stream-preview-vue>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<h1 class="font-bold text-5xl text-white">Discover</h1>
|
<h1 class="font-bold text-5xl text-white">Discover</h1>
|
||||||
<p class="text-xl text-white">Sort through popular categories</p>
|
<p class="text-xl text-white">Sort through popular categories</p>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'
|
||||||
import VideoPlayer from '@/components/VideoPlayer.vue'
|
import VideoPlayer from '@/components/VideoPlayer.vue'
|
||||||
import TwitchChat from '@/components/TwitchChat.vue'
|
import TwitchChat from '@/components/TwitchChat.vue'
|
||||||
import ErrorMessage from '@/components/ErrorMessage.vue'
|
import ErrorMessage from '@/components/ErrorMessage.vue'
|
||||||
|
import FollowButton from '@/components/FollowButton.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async setup() {
|
async setup() {
|
||||||
|
@ -56,7 +57,8 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
VideoPlayer,
|
VideoPlayer,
|
||||||
TwitchChat,
|
TwitchChat,
|
||||||
ErrorMessage
|
ErrorMessage,
|
||||||
|
FollowButton
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
truncate(value: string, length: number) {
|
truncate(value: string, length: number) {
|
||||||
|
@ -140,11 +142,7 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-2 pl- inline-flex">
|
<div class="pt-2 pl- inline-flex">
|
||||||
<button class="text-white text-sm font-bold p-2 py-1 rounded-md bg-purple-600">
|
<follow-button :username="data.username"></follow-button>
|
||||||
<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>
|
<p class="align-baseline font-bold ml-3">{{ data.followersAbbv }} Followers</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue