fix: all empty states now come from a common component

This commit is contained in:
Jannat Patel
2025-05-12 17:46:23 +05:30
parent 9107d76522
commit e6945ac076
20 changed files with 512 additions and 6091 deletions

View File

@@ -47,6 +47,7 @@ declare module 'vue' {
Discussions: typeof import('./src/components/Discussions.vue')['default']
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
Evaluators: typeof import('./src/components/Evaluators.vue')['default']
Event: typeof import('./src/components/Modals/Event.vue')['default']

View File

@@ -2,6 +2,7 @@
"name": "frappe-ui-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"serve": "vite preview",
@@ -26,7 +27,7 @@
"codemirror-editor-vue3": "^2.8.0",
"dayjs": "^1.11.6",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.134",
"frappe-ui": "^0.1.141",
"highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0",

View File

@@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},

View File

@@ -55,7 +55,10 @@
</div>
</li>
</ComboboxOption>
<div v-if="attrs.onCreate" class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t">
<div
v-if="attrs.onCreate"
class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t"
>
<Button
variant="ghost"
class="w-full !justify-start"
@@ -75,13 +78,18 @@
</Combobox>
</div>
<div v-if="values.length" class="grid grid-cols-2 gap-2 mt-4">
<div v-for="value in values" class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2">
<div
v-for="value in values"
class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2"
>
<span class="break-all">
{{ value }}
</span>
<X class="size-4 stroke-1.5 cursor-pointer" @click="removeValue(value)" />
<X
class="size-4 stroke-1.5 cursor-pointer"
@click="removeValue(value)"
/>
</div>
</div>
<!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> -->
</div>

View File

@@ -0,0 +1,24 @@
<template>
<div 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-2.5">
{{ __('No {0}').format(type?.toLowerCase()) }}
</div>
<div
class="leading-5 text-base w-2/5 text-base text-center text-ink-gray-7"
>
{{
__(
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
).format(type?.toLowerCase())
}}
</div>
</div>
</template>
<script setup lang="ts">
import { BookOpen, GraduationCap } from 'lucide-vue-next'
const props = defineProps({
type: String,
})
</script>

View File

@@ -21,14 +21,28 @@
</header>
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
<div class="grid grid-cols-3 gap-5 mb-5">
<FormControl v-model="titleFilter" :placeholder="__('Search by title')" />
<FormControl
v-model="typeFilter"
type="select"
:options="assignmentTypes"
:placeholder="__('Type')"
/>
<div class="flex items-center justify-between mb-5">
<div
v-if="assignmentCount"
class="text-xl font-semibold text-ink-gray-7 mb-4"
>
{{ __('{0} Assignments').format(assignmentCount) }}
</div>
<div
v-if="assignments.data?.length || assigmentCount > 0"
class="grid grid-cols-2 gap-5"
>
<FormControl
v-model="titleFilter"
:placeholder="__('Search by title')"
/>
<FormControl
v-model="typeFilter"
type="select"
:options="assignmentTypes"
:placeholder="__('Type')"
/>
</div>
</div>
<ListView
v-if="assignments.data?.length"
@@ -46,22 +60,7 @@
}"
>
</ListView>
<div
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<Pencil class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No assignments found') }}
</div>
<div class="leading-5">
{{
__(
'You have not created any assignments yet. To create a new assignment, click on the "New" button above.'
)
}}
</div>
</div>
<EmptyState v-else type="Assignments" />
<div
v-if="assignments.data && assignments.hasNextPage"
class="flex justify-center my-5"
@@ -81,16 +80,18 @@
import {
Breadcrumbs,
Button,
call,
createListResource,
FormControl,
ListView,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue'
import { Plus, Pencil } from 'lucide-vue-next'
import { Plus } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session'
import AssignmentForm from '@/components/Modals/AssignmentForm.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user')
const dayjs = inject('$dayjs')
@@ -98,6 +99,7 @@ const titleFilter = ref('')
const typeFilter = ref('')
const showAssignmentForm = ref(false)
const assignmentID = ref('new')
const assignmentCount = ref(0)
const { brand } = sessionStore()
const router = useRouter()
const readOnlyMode = window.read_only_mode
@@ -106,7 +108,7 @@ onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
getAssignmentCount()
titleFilter.value = router.currentRoute.value.query.title
typeFilter.value = router.currentRoute.value.query.type
})
@@ -179,6 +181,14 @@ const assignmentColumns = computed(() => {
]
})
const getAssignmentCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Assignment',
}).then((data) => {
assignmentCount.value = data
})
}
const assignmentTypes = computed(() => {
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
return types.map((type) => {

View File

@@ -56,7 +56,6 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Settings') }}
@@ -80,7 +79,6 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Date and Time') }}
@@ -137,7 +135,6 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Configurations') }}
@@ -205,9 +202,7 @@
</Button>
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{
__(
'Appears when the batch URL is shared on socials'
)
__('Appears when the batch URL is shared on socials')
}}
</div>
</div>
@@ -239,7 +234,6 @@
</div>
</div>
<div class="px-20 pb-5 space-y-5">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Payment') }}
@@ -263,7 +257,6 @@
/>
</div>
</div>
</div>
</div>
</template>

View File

@@ -70,22 +70,8 @@
<BatchCard :batch="batch" />
</router-link>
</div>
<div
v-else-if="!batches.list.loading"
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No batches found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'There are no batches matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<EmptyState v-else-if="!batches.list.loading" type="Batches" />
<div
v-if="!batches.list.loading && batches.hasNextPage"
class="flex justify-center mt-5"
@@ -110,6 +96,7 @@ import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import BatchCard from '@/components/BatchCard.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user')
const dayjs = inject('$dayjs')

View File

@@ -101,22 +101,7 @@
</Button>
</div>
</div>
<div
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No certified members') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'No certified members found. Please check again later or get certified yourself.'
)
}}
</div>
</div>
<EmptyState v-else type="Certified Members" />
</template>
<script setup>
import {
@@ -130,8 +115,9 @@ import {
usePageMeta,
} from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, GraduationCap } from 'lucide-vue-next'
import { GraduationCap } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import EmptyState from '@/components/EmptyState.vue'
const currentCategory = ref('')
const filters = ref({})

View File

@@ -127,7 +127,9 @@
{{ __('Remove') }}
</Button>
<div class="mt-2 text-ink-gray-5 text-sm">
{{ __('Appears on the course card in the course list') }}
{{
__('Appears on the course card in the course list')
}}
</div>
</div>
</div>
@@ -136,10 +138,9 @@
</div>
</div>
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
{{ __("Settings") }}
{{ __('Settings') }}
</div>
<div class="grid grid-cols-2 gap-5">
<div class="flex flex-col space-y-5">
@@ -174,7 +175,6 @@
</div>
</div>
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
@@ -189,7 +189,7 @@
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>
<FormControl
v-model="course.video_link"
:label="__('Preview Video')"
@@ -200,8 +200,7 @@
"
/>
</div>
<div class="px-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5">
{{ __('Pricing and Certification') }}
@@ -225,13 +224,19 @@
</div>
<div class="grid grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl v-if="course.paid_course || course.paid_certificate" v-model="course.course_price" :label="__('Amount')" />
<FormControl
v-if="course.paid_course || course.paid_certificate"
v-model="course.course_price"
:label="__('Amount')"
/>
<Link
v-if="course.paid_certificate"
doctype="Course Evaluator"
v-model="course.evaluator"
:label="__('Evaluator')"
:onCreate="(value, close) => openSettings('Evaluators', close)"
:onCreate="
(value, close) => openSettings('Evaluators', close)
"
/>
</div>
<Link

View File

@@ -66,22 +66,7 @@
<CourseCard :course="course" />
</router-link>
</div>
<div
v-else-if="!courses.list.loading"
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 italic mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No courses found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'There are no courses matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<EmptyState v-else-if="!courses.list.loading" type="Courses" />
<div
v-if="!courses.list.loading && courses.hasNextPage"
class="flex justify-center mt-5"
@@ -108,6 +93,7 @@ import { BookOpen, Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import router from '../router'
const user = inject('$user')

View File

@@ -34,7 +34,11 @@
>
{{ __('{0} Open Jobs').format(jobCount) }}
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div
v-if="jobs.data?.length || jobCount > 0"
class="grid grid-cols-1 md:grid-cols-3 gap-2"
>
<FormControl
type="text"
:placeholder="__('Search')"
@@ -79,21 +83,7 @@
</router-link>
</div>
</div>
<div
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-56"
>
<Laptop class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No jobs found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{ __('There are no jobs available at the moment.') }}
</div>
<div class="leading-5 w-1/5 text-center">
{{ __('Post a new job or check again later.') }}
</div>
</div>
<EmptyState v-else type="Job Openings" />
</div>
</div>
</template>
@@ -106,11 +96,12 @@ import {
FormControl,
usePageMeta,
} from 'frappe-ui'
import { Laptop, Plus, Search } from 'lucide-vue-next'
import { Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted, watch } from 'vue'
import JobCard from '@/components/JobCard.vue'
import Link from '@/components/Controls/Link.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user')
const jobType = ref(null)

View File

@@ -82,22 +82,7 @@
</div>
</div>
</div>
<div
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No programs found') }}
</div>
<div class="leading-5">
{{
__(
'There are no programs available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<EmptyState v-else type="Programs" />
<Dialog
v-model="showDialog"
@@ -129,8 +114,9 @@ import {
usePageMeta,
} from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import { Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session'
import { showToast } from '@/utils'

View File

@@ -40,18 +40,7 @@
</Button>
</div>
</div>
<div
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No submissions') }}
</div>
<div class="leading-5">
{{ __('No quiz submissions found. Please check again later.') }}
</div>
</div>
<EmptyState v-else />
</template>
<script setup>
import {
@@ -65,10 +54,10 @@ import {
ListHeaderItem,
usePageMeta,
} from 'frappe-ui'
import { BookOpen } from 'lucide-vue-next'
import { computed, onMounted, inject } from 'vue'
import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore()
const router = useRouter()

View File

@@ -21,6 +21,9 @@
</router-link>
</header>
<div v-if="quizzes.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
<div v-if="quizCount" class="text-xl font-semibold text-ink-gray-7 mb-4">
{{ __('{0} Quizzes').format(quizCount) }}
</div>
<ListView
:columns="quizColumns"
:rows="quizzes.data"
@@ -53,27 +56,13 @@
</Button>
</div>
</div>
<div
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No quizzes found') }}
</div>
<div class="leading-5">
{{
__(
'You have not created any quizzes yet. To create a new quiz, click on the "New Quiz" button above.'
)
}}
</div>
</div>
<EmptyState v-else type="Quizzes" />
</template>
<script setup>
import {
Breadcrumbs,
Button,
call,
createListResource,
ListView,
ListRows,
@@ -83,19 +72,22 @@ import {
usePageMeta,
} from 'frappe-ui'
import { useRouter } from 'vue-router'
import { computed, inject, onMounted } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next'
import { computed, inject, onMounted, ref } from 'vue'
import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore()
const user = inject('$user')
const router = useRouter()
const quizCount = ref(0)
const readOnlyMode = window.read_only_mode
onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
getQuizCount()
})
const quizFilter = computed(() => {
@@ -114,6 +106,14 @@ const quizzes = createListResource({
orderBy: 'modified desc',
})
const getQuizCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Quiz',
}).then((data) => {
quizCount.value = data
})
}
const quizColumns = computed(() => {
return [
{

View File

@@ -1,5 +1,7 @@
module.exports = {
presets: [require('frappe-ui/src/tailwind/preset')],
import frappeUIPreset from 'frappe-ui/src/tailwind/preset'
export default {
presets: [frappeUIPreset],
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',

View File

@@ -25,7 +25,7 @@ export default defineConfig({
}),
],
server: {
allowedHosts: ['fs', 'persona'],
allowedHosts: ['fs', 'per2'],
},
resolve: {
alias: {

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
"frappe-ui",
"frontend"
],
"type": "module",
"scripts": {
"test-local": "cypress open --e2e --browser chrome",
"postinstall": "cd frontend && yarn install --check-files",

5553
yarn.lock

File diff suppressed because it is too large Load Diff