Files
enlight-lms/frontend/src/components/CourseCard.vue
joylessorchid a26f02065a fix: increase card image height, fix title overflow, fix Twitch error 2000
- CourseCard: raise image from h-40 to h-52 (less cropping)
- CourseCard: use -webkit-line-clamp inline style to hard-clip title overflow
- CourseCard: reduce footer bottom padding (pb-1)
- StreamEmbed: pass both current hostname and localhost as Twitch parent
  params to fix error 2000 in local/dev environments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 06:59:34 +03:00

200 lines
6.1 KiB
Vue

<template>
<div
v-if="course.title"
class="flex flex-col h-full rounded-xl overflow-hidden border border-outline-gray-2 bg-surface-white transition-all duration-200 hover:shadow-md hover:-translate-y-0.5 group"
style="min-height: 320px"
>
<!-- Cover image / gradient -->
<div
class="relative w-full h-52 bg-cover bg-center bg-no-repeat flex-shrink-0"
:style="
course.image
? { backgroundImage: `url('${encodeURI(course.image)}')` }
: { backgroundImage: getGradientColor() }
"
>
<!-- Top badges row -->
<div class="absolute top-2.5 left-2.5 flex flex-wrap gap-1">
<span
v-if="course.upcoming"
class="inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full bg-surface-amber-2 text-ink-amber-3 border border-outline-amber-1"
>
<Clock class="h-2.5 w-2.5" />
{{ __('Upcoming') }}
</span>
<span
v-else-if="isNew"
class="inline-flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full bg-surface-green-1 text-ink-green-3 border border-outline-green-1"
>
<Sparkles class="h-2.5 w-2.5" />
{{ __('New') }}
</span>
</div>
<div class="absolute top-2.5 right-2.5 flex items-center gap-1">
<Tooltip v-if="course.featured" :text="__('Featured')">
<span class="inline-flex items-center gap-1 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-surface-amber-2 text-ink-amber-3 border border-outline-amber-1">
<Star class="h-2.5 w-2.5 stroke-2" />
</span>
</Tooltip>
</div>
<!-- Title fallback when no image -->
<div
v-if="!course.image"
class="flex items-end h-full px-4 pb-3"
>
<h3
class="text-white font-bold leading-tight drop-shadow"
:class="course.title.length > 40 ? 'text-base' : course.title.length > 24 ? 'text-lg' : 'text-xl'"
>
{{ course.title }}
</h3>
</div>
<!-- Progress overlay -->
<div
v-if="user && course.membership"
class="absolute bottom-0 left-0 right-0 h-1 bg-black/20"
>
<div
class="h-full bg-ink-green-3 transition-all duration-500"
:style="{ width: Math.ceil(course.membership.progress) + '%' }"
/>
</div>
</div>
<!-- Card body -->
<div class="flex flex-col flex-1 p-4">
<!-- Title (when image present): max 2 lines -->
<div
v-if="course.image"
class="font-semibold text-ink-gray-9 leading-snug mb-1 text-sm"
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.5rem"
>
{{ course.title }}
</div>
<!-- Short intro: clamp to 2 lines, fixed height -->
<p class="text-xs text-ink-gray-5 leading-relaxed line-clamp-2 mt-0.5" style="height: 2.5rem; overflow: hidden">
{{ course.short_introduction }}
</p>
<!-- Spacer: pushes stats + footer to the bottom -->
<div class="flex-1" />
<!-- Stats row -->
<div class="flex items-center gap-3 text-xs text-ink-gray-5 mb-3 flex-wrap">
<span v-if="course.lessons" class="flex items-center gap-1">
<BookOpen class="h-3.5 w-3.5" />
{{ course.lessons }} {{ __('lessons') }}
</span>
<span v-if="course.enrollments" class="flex items-center gap-1">
<Users class="h-3.5 w-3.5" />
{{ formatAmount(course.enrollments) }}
</span>
<span v-if="course.rating" class="flex items-center gap-1 text-ink-amber-3">
<Star class="h-3.5 w-3.5 fill-current" />
{{ course.rating }}
</span>
</div>
<!-- Footer -->
<div class="flex items-center justify-between pt-2 pb-1 border-t border-outline-gray-1">
<!-- Instructors -->
<div class="flex items-center gap-1.5">
<div class="flex -space-x-1.5">
<UserAvatar
v-for="instructor in course.instructors.slice(0, 3)"
:key="instructor.name"
:user="instructor"
class="ring-1 ring-white"
/>
</div>
<CourseInstructors
v-if="course.instructors.length === 1"
:instructors="course.instructors"
class="text-xs text-ink-gray-5"
/>
</div>
<!-- Right side: progress % / price / cert -->
<div class="flex items-center gap-2">
<span
v-if="user && course.membership"
class="text-xs font-medium text-ink-green-3"
>
{{ Math.ceil(course.membership.progress) }}%
</span>
<span v-else-if="course.paid_course" class="text-sm font-semibold text-ink-gray-9">
{{ course.price }}
</span>
<Tooltip
v-if="course.paid_certificate || course.enable_certification"
:text="__('Get Certified')"
>
<span class="inline-flex items-center gap-0.5 text-[10px] font-medium text-ink-blue-3 bg-surface-blue-1 px-1.5 py-0.5 rounded border border-outline-blue-1">
<GraduationCap class="h-3 w-3" />
</span>
</Tooltip>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import {
Award,
BookOpen,
Clock,
GraduationCap,
Sparkles,
Star,
Users,
} from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { Tooltip } from 'frappe-ui'
import { formatAmount } from '@/utils'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import colors from '@/utils/frappe-ui-colors.json'
import dayjs from '@/utils/dayjs'
const { user } = sessionStore()
const props = defineProps({
course: {
type: Object,
default: null,
},
})
// Course is "new" if published in the last 60 days
const isNew = computed(() => {
if (!props.course.published_on) return false
return dayjs().diff(dayjs(props.course.published_on), 'day') <= 60
})
const getGradientColor = () => {
let theme = localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
let color = props.course.card_gradient?.toLowerCase() || 'blue'
let colorMap = colors[theme][color]
return `linear-gradient(to bottom right, #111827, ${colorMap[500]})`
}
</script>
<style>
.avatar-group {
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
}
</style>