feat: course card gradient
This commit is contained in:
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -40,6 +40,7 @@ declare module 'vue' {
|
||||
Code: typeof import('./src/components/Controls/Code.vue')['default']
|
||||
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
|
||||
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
|
||||
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
|
||||
CourseCard: typeof import('./src/components/CourseCard.vue')['default']
|
||||
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
|
||||
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
|
||||
|
||||
97
frontend/src/components/Controls/ColorSwatches.vue
Normal file
97
frontend/src/components/Controls/ColorSwatches.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-1">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<Popover placement="bottom">
|
||||
<template #target="{ togglePopover, isOpen }">
|
||||
<FormControl
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="[&>div>input]:pl-8"
|
||||
:placeholder="__('Set Color')"
|
||||
@focus="togglePopover"
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="(val: string) => emit('update:modelValue', val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="size-4 rounded-full"
|
||||
:style="
|
||||
modelValue
|
||||
? {
|
||||
backgroundColor:
|
||||
theme.backgroundColor[modelValue.toLowerCase()][400],
|
||||
}
|
||||
: {}
|
||||
"
|
||||
></div>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<Button variant="ghost">
|
||||
<X
|
||||
class="size-3 text-ink-gray-5"
|
||||
@click="emit('update:modelValue', null)"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</FormControl>
|
||||
</template>
|
||||
<template #body="{ close }">
|
||||
<div class="rounded-lg bg-surface-white p-3 border w-fit mt-2">
|
||||
<div class="text-xs text-ink-gray-5 mb-1.5">
|
||||
{{ __('Swatches') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
class="size-5 rounded-full cursor-pointer"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
theme.backgroundColor[color.toLowerCase()][400],
|
||||
}"
|
||||
@click="
|
||||
(e) => {
|
||||
emit('update:modelValue', color)
|
||||
close()
|
||||
emit('change', color)
|
||||
}
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Button, FormControl, Popover } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { theme } from '@/utils/theme'
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
label: string
|
||||
}>()
|
||||
|
||||
const colors = computed(() => {
|
||||
return [
|
||||
'Red',
|
||||
'Blue',
|
||||
'Green',
|
||||
'Amber',
|
||||
'Purple',
|
||||
'Cyan',
|
||||
'Orange',
|
||||
'Violet',
|
||||
'Pink',
|
||||
'Teal',
|
||||
'Gray',
|
||||
'Yellow',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -1,41 +1,51 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="course.title"
|
||||
class="flex flex-col h-full rounded-md border-2 overflow-auto"
|
||||
class="flex flex-col h-full rounded-md border-2 overflow-auto hover:border hover:border-outline-gray-3 text-ink-gray-9"
|
||||
style="min-height: 350px"
|
||||
>
|
||||
<div
|
||||
class="course-image"
|
||||
:class="{ 'default-image': !course.image }"
|
||||
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
|
||||
class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat rounded-t-md"
|
||||
:style="
|
||||
course.image
|
||||
? { backgroundImage: `url('${encodeURI(course.image)}')` }
|
||||
: {
|
||||
backgroundImage: getGradientColor(),
|
||||
backgroundBlendMode: 'screen',
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
|
||||
<Badge
|
||||
<div
|
||||
v-if="course.featured"
|
||||
variant="subtle"
|
||||
theme="green"
|
||||
size="md"
|
||||
class="mb-1 mr-1"
|
||||
class="flex items-center space-x-1 text-xs text-ink-amber-3 bg-surface-white px-2 py-0.5 rounded-md mr-1 mb-1"
|
||||
>
|
||||
{{ __('Featured') }}
|
||||
</Badge>
|
||||
<Star class="size-3 stroke-2" />
|
||||
<span>
|
||||
{{ __('Featured') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="course.tags"
|
||||
v-for="tag in course.tags?.split(', ')"
|
||||
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md mb-1 mr-1"
|
||||
class="text-xs bg-surface-white text-ink-gray-9 px-2 py-0.5 rounded-md mb-1 mr-1"
|
||||
>
|
||||
{{ tag }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!course.image" class="image-placeholder">
|
||||
{{ course.title[0] }}
|
||||
<div
|
||||
v-if="!course.image"
|
||||
class="flex items-center justify-center text-white flex-1 font-extrabold text-2xl my-auto"
|
||||
:class="course.tags ? 'h-[80%]' : 'h-full'"
|
||||
>
|
||||
{{ course.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div v-if="course.lessons">
|
||||
<Tooltip :text="__('Lessons')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<span class="flex items-center">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.lessons }}
|
||||
</span>
|
||||
@@ -44,8 +54,8 @@
|
||||
|
||||
<div v-if="course.enrollments">
|
||||
<Tooltip :text="__('Enrolled Students')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<Users class="h-4 w-4 stroke-1. mr-1" />
|
||||
<span class="flex items-center">
|
||||
<Users class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.enrollments }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -53,14 +63,14 @@
|
||||
|
||||
<div v-if="course.rating">
|
||||
<Tooltip :text="__('Average Rating')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<span class="flex items-center">
|
||||
<Star class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.rating }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="course.status != 'Approved'">
|
||||
<!-- <div v-if="course.status != 'Approved'">
|
||||
<Badge
|
||||
variant="subtle"
|
||||
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
|
||||
@@ -68,14 +78,14 @@
|
||||
>
|
||||
{{ course.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="text-xl font-semibold leading-6 text-ink-gray-9">
|
||||
<div v-if="course.image" class="text-xl font-semibold leading-6">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
<div class="short-introduction text-ink-gray-7 text-sm">
|
||||
<div class="short-introduction text-sm">
|
||||
{{ course.short_introduction }}
|
||||
</div>
|
||||
|
||||
@@ -84,11 +94,8 @@
|
||||
:progress="course.membership.progress"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="user && course.membership"
|
||||
class="text-sm text-ink-gray-7 mt-2 mb-4"
|
||||
>
|
||||
{{ Math.ceil(course.membership.progress) }}% completed
|
||||
<div v-if="user && course.membership" class="text-sm mt-2 mb-4">
|
||||
{{ Math.ceil(course.membership.progress) }}% {{ __('completed') }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-auto">
|
||||
@@ -108,21 +115,23 @@
|
||||
<div v-if="course.paid_course" class="font-semibold">
|
||||
{{ course.price }}
|
||||
</div>
|
||||
<div
|
||||
|
||||
<Tooltip
|
||||
v-if="course.paid_certificate || course.enable_certification"
|
||||
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
|
||||
:text="__('Get Certified')"
|
||||
>
|
||||
{{ __('Certification') }}
|
||||
</div>
|
||||
<GraduationCap class="size-5 stroke-1.5" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||
import { BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Badge, Tooltip } from 'frappe-ui'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { theme } from '@/utils/theme'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
|
||||
@@ -134,16 +143,24 @@ const props = defineProps({
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const getGradientColor = () => {
|
||||
let color = props.course.card_gradient.toLowerCase()
|
||||
let colorMap = theme.backgroundColor[color]
|
||||
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
||||
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
|
||||
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
|
||||
/* return `radial-gradient(ellipse at 80% 20%, black 20%, ${colorMap[500]} 100%)` */
|
||||
/* return `radial-gradient(ellipse at 30% 70%, black 50%, ${colorMap[500]} 100%)` */
|
||||
/* return `radial-gradient(ellipse at 80% 20%, ${colorMap[100]} 0%, ${colorMap[300]} 50%, ${colorMap[500]} 100%)` */
|
||||
/* return `conic-gradient(from 180deg at 50% 50%, ${colorMap[100]} 0%, ${colorMap[200]} 50%, ${colorMap[400]} 100%)` */
|
||||
/* return `linear-gradient(135deg, ${colorMap[100]}, ${colorMap[300]}), linear-gradient(120deg, rgba(255,255,255,0.4) 0%, transparent 60%) ` */
|
||||
/* return `radial-gradient(circle at 20% 30%, ${colorMap[100]} 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 40%, ${colorMap[200]} 0%, transparent 50%),
|
||||
linear-gradient(135deg, ${colorMap[300]} 0%, ${colorMap[400]} 100%);` */
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.course-image {
|
||||
height: 168px;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.course-card-pills {
|
||||
background: #ffffff;
|
||||
margin-left: 0;
|
||||
@@ -157,14 +174,6 @@ const props = defineProps({
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.default-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: theme('colors.green.100');
|
||||
color: theme('colors.green.600');
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -173,14 +182,7 @@ const props = defineProps({
|
||||
.avatar-group .avatar {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
}
|
||||
.image-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
font-size: 5rem;
|
||||
color: theme('colors.gray.700');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.avatar-group.overlap .avatar + .avatar {
|
||||
margin-left: calc(-8px);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="text-ink-gray-7">
|
||||
<div class="">
|
||||
<span v-if="instructors?.length == 1">
|
||||
<router-link
|
||||
:to="{
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
{{ instructors[0].first_name }}
|
||||
</router-link>
|
||||
and
|
||||
{{ __('and') }}
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Profile',
|
||||
@@ -38,7 +38,7 @@
|
||||
>
|
||||
{{ instructors[0].first_name }}
|
||||
</router-link>
|
||||
and {{ instructors?.length - 1 }} others
|
||||
{{ __('and') }} {{ instructors?.length - 1 }} {{ __('others') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -47,9 +47,16 @@
|
||||
:required="true"
|
||||
/>
|
||||
<div>
|
||||
<div class="mb-1.5 text-xs text-ink-gray-5">
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ __('Tags') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
:class="['w-full', 'flex-1', 'my-1']"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
<div>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<div
|
||||
@@ -64,37 +71,13 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
:class="[
|
||||
'w-full',
|
||||
'flex-1',
|
||||
{ 'mt-2': course.tags?.length },
|
||||
]"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-ink-gray-5 mb-2">
|
||||
{{ __('Course Image') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!course.course_image"
|
||||
@@ -144,6 +127,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ColorSwatches
|
||||
v-model="course.card_gradient"
|
||||
:label="__('Card Gradient')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,6 +173,21 @@
|
||||
</div>
|
||||
|
||||
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('About the Course') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Course Description') }}
|
||||
@@ -342,6 +345,7 @@ import {
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const newTag = ref('')
|
||||
@@ -365,6 +369,7 @@ const course = reactive({
|
||||
description: '',
|
||||
video_link: '',
|
||||
course_image: null,
|
||||
card_gradient: '',
|
||||
tags: '',
|
||||
category: '',
|
||||
published: false,
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="courses.data?.length"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-5"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8"
|
||||
>
|
||||
<router-link
|
||||
v-for="course in courses.data"
|
||||
|
||||
Reference in New Issue
Block a user