feat: translations

This commit is contained in:
Jannat Patel
2023-12-07 11:38:12 +05:30
parent d00da31f84
commit 11079dae00
11 changed files with 535 additions and 454 deletions

View File

@@ -10,11 +10,12 @@
"dependencies": {
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.16",
"vue": "^3.2.25",
"vue-router": "^4.0.12",
"tailwindcss": "^3.2.7",
"lucide-vue-next": "^0.259.0",
"pinia": "^2.0.33"
"pinia": "^2.0.33",
"qalendar": "^3.6.1",
"tailwindcss": "^3.2.7",
"vue": "^3.2.25",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",

View File

@@ -1,67 +1,67 @@
<template>
<div class="flex flex-col border border-gray-200 h-full rounded-md shadow-sm" style="min-height: 320px;">
<div class="course-image" :class="{'default-image': !course.image}" :style="{ backgroundImage: 'url(' + encodeURI(course.image) + ')' }">
<div class="flex relative top-4 left-4">
<div class="course-card-pills rounded-md border border-gray-200" v-for="tag in course.tags">
{{ tag }}
</div>
</div>
<div v-if="!course.image" class="image-placeholder">{{ course.title[0] }}</div>
</div>
<div class="flex flex-col flex-auto p-4">
<div class="flex text-base items-center justify-between mb-2">
<div v-if="course.lesson_count" class="flex items-center space-x-1 py-1">
<BookOpen class="h-4 w-4 text-gray-700" />
<span> {{ course.lesson_count }} </span>
</div>
<div v-if="course.enrollment_count" class="flex items-center space-x-1 py-1">
<Users class="h-4 w-4 text-gray-700" />
<span> {{ course.enrollment_count }} </span>
</div>
<div v-if="course.avg_rating" class="flex items-center space-x-1 py-1">
<Star class="h-4 w-4 text-gray-700" />
<span> {{ course.avg_rating }} </span>
</div>
</div>
<div class="text-xl font-semibold">
{{ course.title }}
</div>
<div class="short-introduction text-base">
{{ course.short_introduction }}
</div>
<div v-if="user && course.membership" class="w-full bg-gray-200 rounded-full h-1 mb-2">
<div class="bg-gray-900 h-1 rounded-full" :style="{ width: Math.ceil(course.membership.progress) + '%' }"></div>
</div>
<div v-if="user && course.membership" class="text-sm mb-4">
{{ Math.ceil(course.membership.progress) }}% completed
</div>
<div class="flex items-center justify-between text-base mt-auto">
<div class="flex avatar-group overlap">
<div class="mr-1" :class="{'avatar-group overlap': course.instructors.length > 1}">
<UserAvatar v-for="instructor in course.instructors" :user="instructor"/>
<div class="flex flex-col border border-gray-200 h-full rounded-md shadow-sm text-base overflow-auto" style="min-height: 320px;">
<div class="course-image" :class="{ 'default-image': !course.image }" :style="{ backgroundImage: 'url(' + encodeURI(course.image) + ')' }">
<div class="flex relative top-4 left-4 w-fit">
<div class="course-card-pills rounded-md border border-gray-200" v-for="tag in course.tags">
{{ tag }}
</div>
<span v-if="course.instructors.length == 1">
{{ course.instructors[0].full_name }}
</span>
<span v-if="course.instructors.length == 2">
{{ course.instructors[0].first_name }} and {{ course.instructors[1].first_name }}
</span>
<span v-if="course.instructors.length > 2">
{{ course.instructors[0].first_name }} and {{ course.instructors.length - 1 }} others
</span>
</div>
<div v-if="!course.image" class="image-placeholder">{{ course.title[0] }}</div>
</div>
<div class="flex flex-col flex-auto p-4">
<div class="flex items-center justify-between mb-2">
<div v-if="course.lesson_count" class="flex items-center space-x-1 py-1">
<BookOpen class="h-4 w-4 text-gray-700" />
<span> {{ course.lesson_count }} </span>
</div>
<div v-if="course.enrollment_count" class="flex items-center space-x-1 py-1">
<Users class="h-4 w-4 text-gray-700" />
<span> {{ course.enrollment_count }} </span>
</div>
<div v-if="course.avg_rating" class="flex items-center space-x-1 py-1">
<Star class="h-4 w-4 text-gray-700" />
<span> {{ course.avg_rating }} </span>
</div>
</div>
<div class="text-xl font-semibold">
{{ course.title }}
</div>
<div class="text-base font-semibold">
{{ course.price }}
<div class="short-introduction">
{{ course.short_introduction }}
</div>
<div v-if="user && course.membership" class="w-full bg-gray-200 rounded-full h-1 mb-2">
<div class="bg-gray-900 h-1 rounded-full" :style="{ width: Math.ceil(course.membership.progress) + '%' }"></div>
</div>
<div v-if="user && course.membership" class="text-sm mb-4">
{{ Math.ceil(course.membership.progress) }}% completed
</div>
<div class="flex items-center justify-between mt-auto">
<div class="flex avatar-group overlap">
<div class="mr-1" :class="{ 'avatar-group overlap': course.instructors.length > 1 }">
<UserAvatar v-for="instructor in course.instructors" :user="instructor"/>
</div>
<span v-if="course.instructors.length == 1">
{{ course.instructors[0].full_name }}
</span>
<span v-if="course.instructors.length == 2">
{{ course.instructors[0].first_name }} and {{ course.instructors[1].first_name }}
</span>
<span v-if="course.instructors.length > 2">
{{ course.instructors[0].first_name }} and {{ course.instructors.length - 1 }} others
</span>
</div>
<div class="font-semibold">
{{ course.price }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
@@ -139,6 +139,7 @@ const props = defineProps({
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
margin-bottom: 1.25rem;
margin: 0.25rem 0 1.25rem;
line-height: 1.5;
}
</style>

View File

@@ -12,6 +12,7 @@ import {
frappeRequest,
resourcesPlugin,
} from 'frappe-ui'
import translationPlugin from './translation'
// create a pinia instance
let pinia = createPinia()
@@ -24,6 +25,7 @@ app.use(FrappeUI)
app.use(pinia)
app.use(router)
app.use(resourcesPlugin)
app.use(translationPlugin)
app.component('Button', Button)
app.mount('#app')

View File

@@ -0,0 +1,7 @@
<template>
<div>
Course Detail
</div>
</template>
<script setup>
</script>

View File

@@ -4,24 +4,31 @@
>
<Breadcrumbs
class="h-7"
:items="[{ label: 'All Courses', route: { name: 'Courses' } }]"
:items="[{ label: __('All Courses'), route: { name: 'Courses' } }]"
/>
<Button variant="solid">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
New Course
</Button>
<div class="flex">
<Select class="mr-2"
:options="orderOptions"
v-model="orderBy"
/>
<Button variant="solid">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __("New Course") }}
</Button>
</div>
</header>
<div class="mx-5 my-10">
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
<template #tab="{ tab, selected }">
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
<template #tab="{ tab, selected }">
<div>
<button
class="group -mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
:class="{ 'text-gray-900': selected }"
>
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
{{ tab.label }}
{{ __(tab.label) }}
<Badge
class="group-hover:bg-gray-900"
:class="[selected ? 'bg-gray-900' : 'bg-gray-600']"
@@ -32,28 +39,35 @@
{{ tab.count }}
</Badge>
</button>
</template>
<template #default="{ tab }">
<div class="grid grid-cols-3 gap-8 mt-5" v-if="tab.courses && tab.courses.value.length">
<div v-for="course in tab.courses.value">
<CourseCard :course="course" />
</div>
</div>
</template>
<template #default="{ tab }">
<div class="grid grid-cols-3 gap-8 mt-5" v-if="tab.courses && tab.courses.value.length && 0">
<router-link v-for="course in tab.courses.value" :to="{ name: 'CourseDetail', params: { course: course.name } }">
<CourseCard :course="course" />
</router-link>
</div>
<div v-else class="grid flex-1 place-items-center text-xl font-medium text-gray-500">
<div class="flex flex-col items-center justify-center mt-4">
<div>{{ __('No {0} courses found').format(tab.label.toLowerCase()) }}</div>
</div>
<div v-else class="grid flex-1 place-items-center text-xl font-medium text-gray-500">
<div class="flex flex-col items-center justify-center mt-4">
<div>No {{ tab.label.toLowerCase() }} courses found</div>
</div>
</div>
</template>
</Tabs>
</div>
</template>
</Tabs>
</div>
</template>
<script setup>
import { createListResource, Breadcrumbs, Tabs, Badge } from 'frappe-ui';
import { createListResource, Breadcrumbs, Tabs, Badge, Select } from 'frappe-ui';
import CourseCard from '@/components/CourseCard.vue';
import { Plus } from 'lucide-vue-next'
import { ref, computed, watchEffect } from 'vue'
import { ref, computed } from 'vue'
import { usersStore } from '@/stores/user'
import { sessionStore } from '@/stores/session'
const { isLoggedIn } = sessionStore()
const { getUser } = usersStore()
const user = computed(() => isLoggedIn && getUser())
const courses = createListResource({
type: 'list',
@@ -61,36 +75,75 @@ const courses = createListResource({
doctype: 'LMS Course',
url: "lms.lms.utils.get_courses",
auto: true,
})
});
const tabIndex = ref(0)
const tabs = [
{
label: 'Live',
courses: computed(() => {
return courses.data?.live || []
}),
courses: computed(() => courses.data?.live || []),
count: computed(() => courses.data?.live?.length),
show: true
},
{
label: 'Upcoming',
courses: computed(() => courses.data?.upcoming),
count: computed(() => courses.data?.upcoming?.length),
show: true
},
{
label: 'Enrolled',
courses: computed(() => courses.data?.enrolled),
count: computed(() => courses.data?.enrolled?.length),
show: user
},
{
label: 'Created',
courses: computed(() => courses.data?.created),
count: computed(() => courses.data?.created?.length),
show: computed(() => user && (user.roles.includes('Course Creator') || user.roles.includes('Moderator')))
},
{
label: 'Under Review',
courses: computed(() => courses.data?.under_review),
count: computed(() => courses.data?.under_review?.length),
show: computed(() => user && user.roles.includes('Moderator'))
},
]
];
const orderOptions = [
{
label: "Sort By",
disabled: 1
},
{
label: "Most Popular",
value: "enrollment"
},
{
label: "Highest Rated",
value: "rating"
},
{
label: "Newest",
value: "creation"
},
];
const orderBy = 'enrollment';
function sort_courses(order) {
const categories = ['live', 'upcoming', 'enrolled', 'created', 'under_review'];
categories.forEach(category => {
console.log(courses.data)
courses.data[category] = courses.data[category].sort((a, b) => {
if (order === 'enrollment') {
return b.enrollment_count - a.enrollment_count;
} else if (order === 'rating') {
return b.avg_rating - a.avg_rating;
} else if (order === 'newest') {
return new Date(b.creation).getTime() - new Date(a.creation).getTime();
}
});
});
}
</script>

View File

@@ -13,6 +13,12 @@ const routes = [
name: 'Courses',
component: () => import('@/pages/Courses.vue'),
},
{
path: '/courses/:course',
name: 'CourseDetail',
component: () => import('@/pages/CourseDetail.vue'),
props: true,
},
]
let router = createRouter({

View File

@@ -38,6 +38,7 @@ export const usersStore = defineStore('lms-users', () => {
email: email,
full_name: email.split('@')[0],
user_image: null,
roles: ['LMS Student'],
}
}
return usersByName[email]

View File

@@ -0,0 +1,53 @@
import { createResource } from 'frappe-ui'
export default function translationPlugin(app) {
app.config.globalProperties.__ = translate
// fetch translations
if (!window.translatedMessages)
fetchTranslations().then((translations) => {
window.translatedMessages = translations
})
}
async function translate(message) {
let lang = window.lang || 'hi'
let translatedMessage = /* window.translatedMessages[message] || */ message
const hasPlaceholders = /{\d+}/.test(message)
console.log(translatedMessage)
console.log(hasPlaceholders)
if (!hasPlaceholders) {
return translatedMessage
}
return {
format: function (...args) {
return translatedMessage.replace(
/{(\d+)}/g,
function (match, number) {
console.log(match, number)
console.log(args[number])
return typeof args[number] != 'undefined'
? args[number]
: match
}
)
},
}
}
async function fetchTranslations() {
let lang = window.lang || 'hi'
let translations = await createResource({
url: 'lms.lms.api.get_translations',
cache: 'translations',
auto: true,
})
let translatedMessages = {}
console.log(translations.data)
translations.forEach((translation) => {
translatedMessages[translation.source_text] =
translation.translated_text
})
window.translatedMessages = translatedMessages
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
"""
import frappe
from frappe.translate import get_all_translations
@frappe.whitelist()
@@ -143,7 +144,6 @@ def add_mentor_to_subgroup(subgroup, email):
@frappe.whitelist(allow_guest=True)
def get_user_info(user=None):
print(user)
if frappe.session.user == "Guest":
frappe.throw("Authentication failed", exc=frappe.AuthenticationError)
filters = {}
@@ -157,5 +157,16 @@ def get_user_info(user=None):
order_by="full_name asc",
distinct=True,
).run(as_dict=1)
print(users)
user["roles"] = frappe.get_roles(user.name)
return users
@frappe.whitelist()
def get_translations():
if frappe.session.user != "Guest":
language = frappe.db.get_value("User", frappe.session.user, "language")
else:
language = frappe.db.get_single_value("System Settings", "language")
print("language", language)
print(get_all_translations(language))
return get_all_translations(language)

View File

@@ -1166,7 +1166,6 @@ def get_courses():
"course_price",
"currency",
],
filters={"published": True},
)
courses = get_course_details(courses)
@@ -1212,16 +1211,21 @@ def get_categorized_courses(courses):
live, upcoming, enrolled, created, under_review = [], [], [], [], []
for course in courses:
if course.upcoming:
if course.status == "Under Review":
under_review.append(course)
elif course.published and course.upcoming:
upcoming.append(course)
elif course.published:
live.append(course)
elif course.membership:
if course.membership and course.published:
enrolled.append(course)
elif course.is_instructor:
created.append(course)
elif course.status == "Under Review":
under_review.append(course)
categories = [live, upcoming, enrolled, created, under_review]
for category in categories:
category.sort(key=lambda x: x.enrollment_count, reverse=True)
return {
"live": live,