feat: fullscreen stream on admin home, fix card titles 2-line clamp
- Remove greeting for admin view, render stream fullscreen (no scroll) - AdminHome: stream fills full viewport height via flex layout - StreamEmbed: fills parent height instead of fixed aspect-ratio - Set Twitch channel to lofigirl - CourseCard: clamp title to 2 lines with fixed min-height Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,11 +66,11 @@
|
|||||||
|
|
||||||
<!-- Card body -->
|
<!-- Card body -->
|
||||||
<div class="flex flex-col flex-1 p-4">
|
<div class="flex flex-col flex-1 p-4">
|
||||||
<!-- Title (when image present) -->
|
<!-- Title (when image present): max 2 lines -->
|
||||||
<div
|
<div
|
||||||
v-if="course.image"
|
v-if="course.image"
|
||||||
class="font-semibold text-ink-gray-9 leading-snug mb-1"
|
class="font-semibold text-ink-gray-9 leading-snug mb-1 line-clamp-2 text-sm"
|
||||||
:class="course.title.length > 40 ? 'text-sm' : 'text-base'"
|
style="min-height: 2.5rem"
|
||||||
>
|
>
|
||||||
{{ course.title }}
|
{{ course.title }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-xl overflow-hidden border border-outline-gray-2 bg-surface-gray-9">
|
<div class="w-full h-full flex flex-col overflow-hidden bg-black">
|
||||||
<!-- Stream header -->
|
<!-- Stream header -->
|
||||||
<div class="flex items-center justify-between px-4 py-3 bg-surface-gray-9 border-b border-outline-gray-7">
|
<div class="flex items-center justify-between px-4 py-3 bg-surface-gray-9 border-b border-outline-gray-7">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Embed iframe -->
|
<!-- Embed iframe: fills remaining height -->
|
||||||
<div class="relative w-full" style="aspect-ratio: 16/9">
|
<div class="relative flex-1 min-h-0 w-full">
|
||||||
<iframe
|
<iframe
|
||||||
v-if="embedUrl"
|
v-if="embedUrl"
|
||||||
:src="embedUrl"
|
:src="embedUrl"
|
||||||
|
|||||||
@@ -1,258 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- Fullscreen stream: fills all available space, no scroll -->
|
||||||
<!-- Stream embed -->
|
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||||
<div class="mt-10">
|
<!-- Stream fills remaining height -->
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex-1 min-h-0 relative bg-black">
|
||||||
<span class="font-semibold text-lg text-ink-gray-9">{{ __('Live') }}</span>
|
|
||||||
</div>
|
|
||||||
<StreamEmbed
|
<StreamEmbed
|
||||||
:platform="streamConfig.platform"
|
:platform="streamConfig.platform"
|
||||||
:channel="streamConfig.channel"
|
:channel="streamConfig.channel"
|
||||||
:is-live="streamConfig.isLive"
|
:is-live="streamConfig.isLive"
|
||||||
|
class="h-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="createdBatches.data?.length" class="mt-10">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<span class="font-semibold text-lg text-ink-gray-9">
|
|
||||||
{{ __('Upcoming Batches') }}
|
|
||||||
</span>
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'Batches',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-1 text-ink-gray-5 text-xs">
|
|
||||||
<span>
|
|
||||||
{{ __('See all') }}
|
|
||||||
</span>
|
|
||||||
<MoveRight class="size-3 stroke-1.5" />
|
|
||||||
</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
|
||||||
<router-link
|
|
||||||
v-for="batch in createdBatches.data"
|
|
||||||
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
|
|
||||||
>
|
|
||||||
<BatchCard :batch="batch" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!createdCourses.data?.length && !createdBatches.data?.length"
|
|
||||||
class="flex flex-col items-center justify-center mt-60"
|
|
||||||
>
|
|
||||||
<GraduationCap class="size-10 mx-auto stroke-1 text-ink-gray-5" />
|
|
||||||
<div class="text-lg font-semibold text-ink-gray-7 mb-1.5">
|
|
||||||
{{ __('No courses created') }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="leading-5 text-base w-full md:w-2/5 text-base text-center text-ink-gray-7"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'There are no courses currently. Create your first course to get started!'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'Courses', query: { newCourse: '1' } }"
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
<Button>
|
|
||||||
<template #prefix>
|
|
||||||
<Plus class="size-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
{{ __('Create Course') }}
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-5 mt-10">
|
|
||||||
<div v-if="evals?.data?.length">
|
|
||||||
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
|
||||||
{{ __('Upcoming Evaluations') }}
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
|
||||||
<div
|
|
||||||
v-for="evaluation in evals?.data"
|
|
||||||
class="border hover:border-outline-gray-3 rounded-md p-3 flex flex-col h-full cursor-pointer"
|
|
||||||
@click="redirectToProfile()"
|
|
||||||
>
|
|
||||||
<div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
|
|
||||||
{{ evaluation.course_title }}
|
|
||||||
</div>
|
|
||||||
<div class="text-ink-gray-7 text-sm">
|
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<Calendar class="w-4 h-4 stroke-1.5" />
|
|
||||||
<span class="ml-2">
|
|
||||||
{{ dayjs(evaluation.date).format('DD MMMM YYYY') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<Clock class="w-4 h-4 stroke-1.5" />
|
|
||||||
<span class="ml-2">
|
|
||||||
{{ formatTime(evaluation.start_time) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<GraduationCap class="w-4 h-4 stroke-1.5" />
|
|
||||||
<span class="ml-2">
|
|
||||||
{{ evaluation.member_name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="liveClasses?.data?.length">
|
|
||||||
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
|
||||||
{{ __('Upcoming Live Classes') }}
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
|
||||||
<div
|
|
||||||
v-for="cls in liveClasses?.data"
|
|
||||||
class="border hover:border-outline-gray-3 rounded-md p-3"
|
|
||||||
>
|
|
||||||
<div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
|
|
||||||
{{ cls.title }}
|
|
||||||
</div>
|
|
||||||
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
|
|
||||||
{{ cls.description }}
|
|
||||||
</div>
|
|
||||||
<div class="mt-auto space-y-3 text-ink-gray-7 text-sm">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Calendar class="w-4 h-4 stroke-1.5" />
|
|
||||||
<span>
|
|
||||||
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Clock class="w-4 h-4 stroke-1.5" />
|
|
||||||
<span>
|
|
||||||
{{ formatTime(cls.time) }} -
|
|
||||||
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="canAccessClass(cls)"
|
|
||||||
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
v-if="user.data?.is_moderator || user.data?.is_evaluator"
|
|
||||||
:href="cls.start_url"
|
|
||||||
target="_blank"
|
|
||||||
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
|
|
||||||
:class="cls.join_url ? 'w-full' : 'w-1/2'"
|
|
||||||
>
|
|
||||||
<Monitor class="h-4 w-4 stroke-1.5" />
|
|
||||||
{{ __('Start') }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
:href="cls.join_url"
|
|
||||||
target="_blank"
|
|
||||||
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
|
|
||||||
>
|
|
||||||
<Video class="h-4 w-4 stroke-1.5" />
|
|
||||||
{{ __('Join') }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
v-else-if="hasClassEnded(cls)"
|
|
||||||
:text="__('This class has ended')"
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
|
|
||||||
<Info class="w-4 h-4 stroke-1.5" />
|
|
||||||
<span>
|
|
||||||
{{ __('Ended') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Button, createResource, Tooltip } from 'frappe-ui'
|
import { ref } from 'vue'
|
||||||
import { inject, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import {
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
GraduationCap,
|
|
||||||
Info,
|
|
||||||
Monitor,
|
|
||||||
MoveRight,
|
|
||||||
Plus,
|
|
||||||
Video,
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
import { formatTime } from '@/utils'
|
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
|
||||||
import BatchCard from '@/pages/Batches/components/BatchCard.vue'
|
|
||||||
import StreamEmbed from '@/components/StreamEmbed.vue'
|
import StreamEmbed from '@/components/StreamEmbed.vue'
|
||||||
|
|
||||||
// Настройки стрима — укажи platform и channel
|
|
||||||
const streamConfig = ref({
|
|
||||||
platform: 'twitch' as 'twitch' | 'youtube',
|
|
||||||
channel: '', // например: 'imo_online' для Twitch или video ID для YouTube
|
|
||||||
isLive: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const user = inject<any>('$user')
|
|
||||||
const dayjs = inject<any>('$dayjs')
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
liveClasses?: { data?: any[] }
|
liveClasses?: { data?: any[] }
|
||||||
evals?: { data?: any[] }
|
evals?: { data?: any[] }
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const createdCourses = createResource({
|
const streamConfig = ref({
|
||||||
url: 'lms.lms.api.get_created_courses',
|
platform: 'twitch' as 'twitch' | 'youtube',
|
||||||
auto: true,
|
channel: 'lofigirl',
|
||||||
|
isLive: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const createdBatches = createResource({
|
|
||||||
url: 'lms.lms.api.get_created_batches',
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getClassEnd = (cls: { date: string; time: string; duration: number }) => {
|
|
||||||
const classStart = new Date(`${cls.date}T${cls.time}`)
|
|
||||||
return new Date(classStart.getTime() + cls.duration * 60000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const canAccessClass = (cls: {
|
|
||||||
date: string
|
|
||||||
time: string
|
|
||||||
duration: number
|
|
||||||
}) => {
|
|
||||||
if (cls.date < dayjs().format('YYYY-MM-DD')) return false
|
|
||||||
if (cls.date > dayjs().format('YYYY-MM-DD')) return false
|
|
||||||
if (hasClassEnded(cls)) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasClassEnded = (cls: {
|
|
||||||
date: string
|
|
||||||
time: string
|
|
||||||
duration: number
|
|
||||||
}) => {
|
|
||||||
const classEnd = getClassEnd(cls)
|
|
||||||
const now = new Date()
|
|
||||||
return now > classEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirectToProfile = () => {
|
|
||||||
router.push({
|
|
||||||
name: 'ProfileEvaluationSchedule',
|
|
||||||
params: { username: user.data?.username },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-5 pt-5 pb-10">
|
<!-- Admin view: fullscreen stream, no header, no scroll -->
|
||||||
<div class="space-y-2">
|
<div
|
||||||
|
v-if="isAdmin && currentTab === 'instructor'"
|
||||||
|
class="w-full h-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<AdminHome
|
||||||
|
:liveClasses="adminLiveClasses"
|
||||||
|
:evals="adminEvals"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Student view: greeting + content -->
|
||||||
|
<div v-else class="w-full px-5 pt-5 pb-10">
|
||||||
|
<div class="space-y-2 mb-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-xl font-bold text-ink-gray-9">
|
<div class="text-xl font-bold text-ink-gray-9">
|
||||||
{{ __('Hey') }}, {{ user.data?.full_name }} 👋
|
{{ __('Hey') }}, {{ user.data?.full_name }} 👋
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
<div
|
@click="showStreakModal = true"
|
||||||
v-if="!isAdmin"
|
class="bg-surface-amber-2 px-2 py-1 rounded-md cursor-pointer"
|
||||||
@click="showStreakModal = true"
|
>
|
||||||
class="bg-surface-amber-2 px-2 py-1 rounded-md cursor-pointer"
|
<span> 🔥 </span>
|
||||||
>
|
<span class="text-ink-gray-9">
|
||||||
<span> 🔥 </span>
|
{{ streakInfo.data?.current_streak }}
|
||||||
<span class="text-ink-gray-9">
|
</span>
|
||||||
{{ streakInfo.data?.current_streak }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-base text-ink-gray-6 leading-6">
|
||||||
<div class="text-lg text-ink-gray-6 leading-6">
|
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AdminHome
|
<StudentHome :myLiveClasses="myLiveClasses" />
|
||||||
v-if="isAdmin && currentTab === 'instructor'"
|
|
||||||
:liveClasses="adminLiveClasses"
|
|
||||||
:evals="adminEvals"
|
|
||||||
/>
|
|
||||||
<StudentHome
|
|
||||||
v-else-if="currentTab === 'student'"
|
|
||||||
:myLiveClasses="myLiveClasses"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Streak v-model="showStreakModal" :streakInfo="streakInfo" />
|
<Streak v-model="showStreakModal" :streakInfo="streakInfo" />
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
Reference in New Issue
Block a user