feat: Embedding Cloudflare Stream

This commit is contained in:
safe user
2025-04-25 09:28:44 +00:00
12 changed files with 262 additions and 126 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col border-2 hover:bg-surface-gray-2 rounded-md p-4 h-full"
class="flex flex-col border hover:border-outline-gray-4 rounded-md p-4 h-full"
style="min-height: 150px"
>
<div class="text-lg leading-5 font-semibold mb-2 text-ink-gray-9">

View File

@@ -1,22 +1,35 @@
<template>
<div class="flex flex-col border rounded-md p-4 h-full">
<div class="flex space-x-4 mb-2">
<img :src="job.company_logo" class="size-8 rounded-full object-contain" />
<div class="flex flex-col space-y-1 flex-1">
<div class="flex items-center justify-between">
<span class="text-lg font-semibold text-ink-gray-9">
{{ job.job_title }}
</span>
</div>
<div class="text-xs text-ink-gray-5">
<div
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-4"
>
<div class="flex space-x-4 mb-4">
<div class="flex flex-col space-y-2 flex-1">
<div class="text-lg font-semibold text-ink-gray-9">
{{ job.company_name }}
</div>
<span class="font-medium text-ink-gray-7 leading-5">
{{ job.job_title }}
</span>
<div class="flex items-center space-x-1 text-sm text-ink-gray-7">
<MapPin class="size-3" />
<span>
{{ job.location }}{{ job.country ? `, ${job.country}` : '' }}
</span>
</div>
<div
v-if="job.applicants"
class="flex items-center space-x-1 text-sm text-ink-gray-7"
>
<User class="size-3" />
<span>
{{ job.applicants }}
{{ job.applicants > 1 ? __('applicants') : __('applicant') }}
</span>
</div>
</div>
<!-- <img :src="job.company_logo" alt="Company Logo" class="size-8 rounded-full object-contain bg-white" /> -->
</div>
<div class="space-x-4 mt-auto">
<Badge>
{{ job.location }}
</Badge>
<div class="space-x-2 mt-auto">
<Badge>
{{ job.type }}
</Badge>
@@ -24,11 +37,16 @@
{{ dayjs(job.creation).fromNow() }}
</Badge>
</div>
<!-- <div
class="description text-ink-gray-9 text-sm"
v-html="job.description"
></div> -->
</div>
</template>
<script setup>
import { inject } from 'vue'
import { Badge } from 'frappe-ui'
import { MapPin, User } from 'lucide-vue-next'
const dayjs = inject('$dayjs')
const props = defineProps({
@@ -38,3 +56,15 @@ const props = defineProps({
},
})
</script>
<style>
.description {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
margin-top: auto;
line-height: 1.5;
}
</style>

View File

@@ -16,11 +16,11 @@
},
]"
/>
<div v-if="user.data?.name" class="flex space-x-2">
<div v-if="user.data?.name" class="flex items-center space-x-2">
<router-link
v-if="user.data.name == job.data?.owner"
:to="{
name: 'JobCreation',
name: 'JobForm',
params: { jobName: job.data?.name },
}"
>
@@ -47,6 +47,12 @@
</template>
{{ __('Apply') }}
</Button>
<Badge v-else variant="subtle" theme="green" size="lg">
<template #prefix>
<Check class="h-4 w-4" />
</template>
{{ __('You have applied') }}
</Badge>
</div>
<div v-else>
<Button @click="redirectToLogin(job.data?.name)">
@@ -56,13 +62,13 @@
</Button>
</div>
</header>
<div v-if="job.data" class="max-w-3xl mx-auto">
<div v-if="job.data" class="max-w-3xl mx-auto pt-5">
<div class="p-4">
<div class="space-y-5 mb-10">
<div class="flex items-center">
<img
:src="job.data.company_logo"
class="w-16 h-16 rounded-lg object-contain cursor-pointer mr-4"
class="size-10 rounded-lg object-contain cursor-pointer mr-4"
:alt="job.data.company_name"
@click="redirectToWebsite(job.data.company_website)"
/>
@@ -75,7 +81,7 @@
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-10 gap-y-5 md:gap-y-5"
>
<div class="flex items-center space-x-4">
<Building2 class="h-4 w-4 text-ink-green-2" />
<Building2 class="size-4 stroke-1.5 text-ink-gray-7" />
<div class="flex flex-col space-y-1 text-ink-gray-7">
<span class="text-xs text-ink-gray-5 font-medium uppercase">
{{ __('Organisation') }}
@@ -86,20 +92,20 @@
</div>
</div>
<div class="flex items-center space-x-4">
<MapPin class="size-4 text-ink-red-3" />
<MapPin class="size-4 stroke-1.5 text-ink-gray-7" />
<div class="flex flex-col space-y-1 text-ink-gray-7">
<span class="text-xs font-medium uppercase">
<span class="text-xs text-ink-gray-5 font-medium uppercase">
{{ __('Location') }}
</span>
<span class="text-sm font-semibold">
{{ job.data.location }}
{{ job.data.location }}, {{ job.data.country }}
</span>
</div>
</div>
<div class="flex items-center space-x-4">
<ClipboardType class="h-4 w-4 text-yellow-500" />
<ClipboardType class="size-4 stroke-1.5 text-ink-gray-7" />
<div class="flex flex-col space-y-1 text-ink-gray-7">
<span class="text-xs font-medium uppercase">
<span class="text-xs text-ink-gray-5 font-medium uppercase">
{{ __('Category') }}
</span>
<span class="text-sm font-semibold">
@@ -108,9 +114,9 @@
</div>
</div>
<div class="flex items-center space-x-4">
<CalendarDays class="h-4 w-4 text-ink-blue-2" />
<CalendarDays class="size-4 stroke-1.5 text-ink-gray-7" />
<div class="flex flex-col space-y-1 text-ink-gray-7">
<span class="text-xs font-medium uppercase">
<span class="text-xs text-ink-gray-5 font-medium uppercase">
{{ __('Posted on') }}
</span>
<span class="text-sm font-semibold">
@@ -122,9 +128,9 @@
v-if="applicationCount.data"
class="flex items-center space-x-4"
>
<SquareUserRound class="h-4 w-4 text-purple-500" />
<SquareUserRound class="size-4 stroke-1.5 text-ink-gray-7" />
<div class="flex flex-col space-y-1 text-ink-gray-7">
<span class="text-xs font-medium uppercase">
<span class="text-xs text-ink-gray-5 font-medium uppercase">
{{ __('Applications Received') }}
</span>
<span class="text-sm font-semibold">
@@ -149,12 +155,19 @@
</div>
</template>
<script setup>
import { Button, Breadcrumbs, createResource, usePageMeta } from 'frappe-ui'
import {
Badge,
Button,
Breadcrumbs,
createResource,
usePageMeta,
} from 'frappe-ui'
import { inject, ref } from 'vue'
import { sessionStore } from '../stores/session'
import JobApplicationModal from '@/components/Modals/JobApplicationModal.vue'
import {
MapPin,
Check,
SendHorizonal,
Pencil,
Building2,

View File

@@ -13,17 +13,22 @@
<div class="text-lg font-semibold mb-4">
{{ __('Job Details') }}
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="grid grid-cols-2 gap-5">
<div class="space-y-4">
<FormControl
v-model="job.job_title"
:label="__('Title')"
class="mb-4"
:required="true"
/>
<FormControl
v-model="job.location"
:label="__('Location')"
:label="__('City')"
:required="true"
/>
<Link
v-model="job.country"
doctype="Country"
:label="__('Country')"
:required="true"
/>
</div>
@@ -45,25 +50,12 @@
/>
</div>
</div>
<div class="mt-4">
<label class="block text-ink-gray-5 text-xs mb-1">
{{ __('Description') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="job.description"
@change="(val) => (job.description = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
</div>
<div class="container mb-4 pb-4">
<div class="container border-b mb-4 pb-4">
<div class="text-lg font-semibold mb-4">
{{ __('Company Details') }}
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-5">
<div>
<FormControl
v-model="job.company_name"
@@ -128,6 +120,19 @@
</div>
</div>
</div>
<div class="container mt-4">
<label class="block text-ink-gray-5 text-xs mb-1">
{{ __('Description') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="job.description"
@change="(val) => (job.description = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
</div>
</div>
</template>
@@ -217,6 +222,7 @@ const imageResource = createResource({
const job = reactive({
job_title: '',
location: '',
country: '',
type: 'Full Time',
status: 'Open',
company_name: '',
@@ -317,7 +323,7 @@ const breadcrumbs = computed(() => {
},
{
label: props.jobName == 'new' ? 'New Job' : 'Edit Job',
route: { name: 'JobCreation' },
route: { name: 'JobForm' },
},
]
return crumbs

View File

@@ -10,7 +10,7 @@
<router-link
v-if="user.data?.name"
:to="{
name: 'JobCreation',
name: 'JobForm',
params: {
jobName: 'new',
},
@@ -25,40 +25,48 @@
</router-link>
</header>
<div>
<div v-if="jobs.data?.length" class="p-5">
<div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5"
>
<div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
v-if="jobCount"
class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0"
>
<div class="text-xl text-ink-gray-9 font-semibold">
{{ __('Find the perfect job for you') }}
</div>
<div class="grid grid-cols-2 gap-2">
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
:placeholder="__('Type')"
@change="updateJobs"
/>
</div>
{{ __('{0} Open Jobs').format(jobCount) }}
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<Link
doctype="Country"
v-model="country"
:placeholder="__('Country')"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
/>
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
:placeholder="__('Type')"
@change="updateJobs"
/>
</div>
</div>
<div v-if="jobs.data?.length" class="w-full md:w-4/5 mx-auto p-5 pt-0">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<router-link
v-for="job in jobs.data"
:to="{
@@ -73,18 +81,17 @@
</div>
<div
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
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. Open a job opportunity or check here again later.'
)
}}
{{ __('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>
</div>
@@ -94,21 +101,25 @@
import {
Button,
Breadcrumbs,
call,
createResource,
FormControl,
usePageMeta,
} from 'frappe-ui'
import { Laptop, Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted } from 'vue'
import { inject, computed, ref, onMounted, watch } from 'vue'
import JobCard from '@/components/JobCard.vue'
import Link from '@/components/Controls/Link.vue'
const user = inject('$user')
const jobType = ref(null)
const { brand } = sessionStore()
const searchQuery = ref('')
const country = ref(null)
const filters = ref({})
const orFilters = ref({})
const jobCount = ref(0)
onMounted(() => {
let queries = new URLSearchParams(location.search)
@@ -116,6 +127,7 @@ onMounted(() => {
jobType.value = queries.get('type')
}
updateJobs()
getJobCount()
})
const jobs = createResource({
@@ -153,8 +165,30 @@ const updateFilters = () => {
} else {
orFilters.value = {}
}
if (country.value) {
filters.value.country = country.value
} else {
delete filters.value.country
}
}
const getJobCount = () => {
call('frappe.client.get_count', {
doctype: 'Job Opportunity',
filters: {
status: 'Open',
disabled: 0,
},
}).then((data) => {
jobCount.value = data
})
}
watch(country, (val) => {
updateJobs()
})
const jobTypes = computed(() => {
return [
'',

View File

@@ -134,8 +134,8 @@ const routes = [
},
{
path: '/job-opening/:jobName/edit',
name: 'JobCreation',
component: () => import('@/pages/JobCreation.vue'),
name: 'JobForm',
component: () => import('@/pages/JobForm.vue'),
props: true,
},
{

View File

@@ -9,18 +9,19 @@
"field_order": [
"job_title",
"location",
"disabled",
"country",
"column_break_5",
"type",
"status",
"disabled",
"section_break_6",
"description",
"company_details_section",
"company_name",
"company_website",
"column_break_11",
"column_break_phkm",
"company_logo",
"company_email_address"
"company_email_address",
"company_details_section",
"description"
],
"fields": [
{
@@ -36,7 +37,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Location",
"label": "City",
"reqd": 1
},
{
@@ -62,7 +63,8 @@
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Company Details"
},
{
"fieldname": "description",
@@ -72,8 +74,7 @@
},
{
"fieldname": "company_details_section",
"fieldtype": "Section Break",
"label": "Company Details"
"fieldtype": "Section Break"
},
{
"fieldname": "company_name",
@@ -89,10 +90,6 @@
"label": "Company Website",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "company_logo",
"fieldtype": "Attach Image",
@@ -111,13 +108,30 @@
"label": "Company Email Address",
"options": "Email",
"reqd": 1
},
{
"fieldname": "column_break_phkm",
"fieldtype": "Column Break"
},
{
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
"options": "Country",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"links": [
{
"link_doctype": "LMS Job Application",
"link_fieldname": "job"
}
],
"make_attachments_public": 1,
"modified": "2025-01-17 12:38:57.134919",
"modified_by": "Administrator",
"modified": "2025-04-24 14:34:35.920242",
"modified_by": "sayali@frappe.io",
"module": "Job",
"name": "Job Opportunity",
"owner": "Administrator",
@@ -157,8 +171,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "job_title"
}
}

View File

@@ -281,6 +281,7 @@ def get_job_details(job):
[
"job_title",
"location",
"country",
"type",
"company_name",
"company_logo",
@@ -306,14 +307,20 @@ def get_job_opportunities(filters=None, orFilters=None):
fields=[
"job_title",
"location",
"country",
"type",
"company_name",
"company_logo",
"name",
"creation",
"description",
],
order_by="creation desc",
)
for job in jobs:
job.description = frappe.utils.strip_html_tags(job.description)
job.applicants = frappe.db.count("LMS Job Application", {"job": job.name})
return jobs

View File

@@ -91,7 +91,7 @@
"fetch_from": "member.username",
"fieldname": "member_username",
"fieldtype": "Data",
"label": "Memeber Username",
"label": "Member Username",
"read_only": 1
},
{
@@ -145,10 +145,11 @@
"options": "LMS Certificate"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-02-21 17:11:37.986157",
"modified_by": "Administrator",
"modified": "2025-04-25 10:06:25.824119",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Enrollment",
"owner": "Administrator",
@@ -192,10 +193,11 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "member_name",
"track_changes": 1
}
}

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-04-18 16:04+0000\n"
"PO-Revision-Date: 2025-04-21 06:38\n"
"PO-Revision-Date: 2025-04-24 08:07\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Chinese Simplified\n"
"MIME-Version: 1.0\n"
@@ -465,7 +465,7 @@ msgstr "作业标题"
#: frontend/src/components/Modals/AssignmentForm.vue:112
msgid "Assignment created successfully"
msgstr ""
msgstr "作业创建成功"
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.py:24
msgid "Assignment for Lesson {0} by {1} already exists."
@@ -473,7 +473,7 @@ msgstr "学员{1}的课时{0}作业已存在。"
#: frontend/src/components/Modals/AssignmentForm.vue:129
msgid "Assignment updated successfully"
msgstr ""
msgstr "作业更新成功"
#. Description of the 'Question' (Small Text) field in DocType 'Course Lesson'
#: lms/lms/doctype/course_lesson/course_lesson.json
@@ -831,7 +831,7 @@ msgstr "已认证"
#: frontend/src/pages/CertifiedParticipants.vue:196
#: frontend/src/pages/CertifiedParticipants.vue:203
msgid "Certified Members"
msgstr ""
msgstr "认证成员"
#. Label of the certified_participants (Check) field in DocType 'LMS Settings'
#: lms/lms/doctype/lms_settings/lms_settings.json lms/www/lms.py:290
@@ -1458,7 +1458,7 @@ msgstr "新建试题"
#: frontend/src/components/Modals/AssignmentForm.vue:7
msgid "Create an Assignment"
msgstr ""
msgstr "创建作业"
#: frontend/src/components/AppSidebar.vue:460
msgid "Create your first batch"
@@ -1754,7 +1754,7 @@ msgstr "编辑"
#: frontend/src/components/Modals/AssignmentForm.vue:8
msgid "Edit Assignment"
msgstr ""
msgstr "编辑作业"
#: frontend/src/components/CourseOutline.vue:52
#: frontend/src/components/Modals/ChapterModal.vue:5
@@ -3584,11 +3584,11 @@ msgstr "无证书"
#: frontend/src/pages/CertifiedParticipants.vue:110
msgid "No certified members"
msgstr ""
msgstr "无认证成员"
#: frontend/src/pages/CertifiedParticipants.vue:114
msgid "No certified members found. Please check again later or get certified yourself."
msgstr ""
msgstr "未找到认证成员,请稍后再试或自行申请认证。"
#: frontend/src/components/BatchCourses.vue:67
msgid "No courses added"
@@ -4077,11 +4077,11 @@ msgstr "请输入答案"
#: lms/lms/doctype/lms_batch/lms_batch.py:58
msgid "Please install the Payments App to create a paid batch. Refer to the documentation for more details. {0}"
msgstr ""
msgstr "请安装支付应用以创建付费班级,详情请参阅文档{0}"
#: lms/lms/doctype/lms_course/lms_course.py:55
msgid "Please install the Payments App to create a paid course. Refer to the documentation for more details. {0}"
msgstr ""
msgstr "请安装支付应用以创建付费课程,详情请参阅文档{0}"
#: frontend/src/pages/Billing.vue:254
msgid "Please let us know where you heard about us from."
@@ -4184,7 +4184,7 @@ msgstr "发布于"
#: frontend/src/components/AppSidebar.vue:92
msgid "Powered by Learning"
msgstr ""
msgstr "技术支持:学习平台"
#. Name of a DocType
#: lms/lms/doctype/preferred_function/preferred_function.json
@@ -5135,7 +5135,7 @@ msgstr "提交列表"
#: frontend/src/components/Modals/AssignmentForm.vue:30
msgid "Submission Type"
msgstr ""
msgstr "提交类型"
#: frontend/src/components/Assignment.vue:13
#: frontend/src/components/Assignment.vue:16
@@ -6139,7 +6139,7 @@ msgstr "证书"
#: frontend/src/pages/CertifiedParticipants.vue:21
msgid "certified members"
msgstr ""
msgstr "认证成员"
#: frontend/src/pages/Lesson.vue:178
msgid "completed"

View File

@@ -101,4 +101,5 @@ lms.patches.v2_0.allow_guest_access #05-02-2025
lms.patches.v2_0.migrate_batch_student_data #10-02-2025
lms.patches.v2_0.delete_old_enrollment_doctypes
lms.patches.v2_0.delete_unused_custom_fields
lms.patches.v2_0.update_certificate_request_status
lms.patches.v2_0.update_certificate_request_status
lms.patches.v2_0.update_job_city_and_country

View File

@@ -0,0 +1,28 @@
import frappe
def execute():
jobs = frappe.get_all("Job Opportunity", fields=["name", "location"])
for job in jobs:
if "," in job.location:
city, country = job.location.split(",", 1)
city = city.strip()
country = country.strip()
save_country(country, job)
frappe.db.set_value("Job Opportunity", job.name, "location", city)
else:
save_country(job.location, job)
def save_country(country, job):
if frappe.db.exists("Country", country):
frappe.db.set_value("Job Opportunity", job.name, "country", country)
else:
country_mapping = {
"US": "United States",
"USA": "United States",
"UAE": "United Arab Emirates",
}
country = country_mapping.get(country, country)
frappe.db.set_value("Job Opportunity", job.name, "country", country)