import { call, toast } from 'frappe-ui' import { useTimeAgo } from '@vueuse/core' import colorsJSON from '@/utils/frappe-ui-colors.json' import { Quiz } from '@/utils/quiz' import { Program } from '@/utils/program' import { Assignment } from '@/utils/assignment' import { Upload } from '@/utils/upload' import { Markdown } from '@/utils/markdownParser' import { useSettings } from '@/stores/settings' import { usersStore } from '@/stores/user' import Header from '@editorjs/header' import Paragraph from '@editorjs/paragraph' import { CodeBox } from '@/utils/code' import NestedList from '@editorjs/nested-list' import InlineCode from '@editorjs/inline-code' import dayjs from '@/utils/dayjs' import Embed from '@editorjs/embed' import SimpleImage from '@editorjs/simple-image' import Table from '@editorjs/table' import Plyr from 'plyr' import 'plyr/dist/plyr.css' import DOMPurify from 'dompurify' const readOnlyMode = window.read_only_mode export function timeAgo(date) { return useTimeAgo(date).value } export function formatTime(timeString) { if (!timeString) return '' const [hour, minute] = timeString.split(':').map(Number) const dummyDate = new Date(0, 0, 0, hour, minute) const formattedTime = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: 'numeric', hour12: true, }).format(dummyDate) return formattedTime } export const formatSeconds = (time) => { const minutes = Math.floor(time / 60) const seconds = Math.floor(time % 60) return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}` } export function formatNumber(number) { return number.toLocaleString('en-IN', { maximumFractionDigits: 0, }) } export function formatNumberIntoCurrency(number, currency) { if (number) { return number.toLocaleString('en-IN', { maximumFractionDigits: 0, style: 'currency', currency: currency, }) } return '' } // create a function that formats numbers in thousands to k export function formatAmount(amount) { if (amount > 999) { return (amount / 1000).toFixed(1) + 'k' } return amount } export function convertToTitleCase(str) { if (!str) { return '' } return str .toLowerCase() .split(' ') .map(function (word) { return word.charAt(0).toUpperCase().concat(word.substr(1)) }) .join(' ') } export function getFileSize(file_size) { let value = parseInt(file_size) if (value > 1048576) { return (value / 1048576).toFixed(2) + 'M' } else if (value > 1024) { return (value / 1024).toFixed(2) + 'K' } return value } export function getImgDimensions(imgSrc) { return new Promise((resolve) => { let img = new Image() img.onload = function () { let { width, height } = img resolve({ width, height, ratio: width / height }) } img.src = imgSrc }) } export function htmlToText(html) { const div = document.createElement('div') div.innerHTML = html return div.textContent || div.innerText || '' } export function getEditorTools() { return { header: { class: Header, config: { placeholder: 'Header', }, }, list: { class: NestedList, inlineToolbar: true, config: { defaultStyle: 'ordered', }, }, table: { class: Table, inlineToolbar: true, }, quiz: Quiz, assignment: Assignment, program: Program, upload: Upload, markdown: { class: Markdown, inlineToolbar: true, }, image: SimpleImage, paragraph: { class: Paragraph, inlineToolbar: true, config: { preserveBlank: true, }, }, codeBox: { class: CodeBox, config: { useDefaultTheme: 'dark', }, }, inlineCode: { class: InlineCode, shortcut: 'CMD+SHIFT+M', }, embed: { class: Embed, inlineToolbar: false, config: { services: { youtube: { regex: /^(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)$/, embedUrl: '<%= remote_id %>', /* 'https://www.youtube.com/embed/<%= remote_id %>?origin=https://plyr.io&iv_load_policy=3&modestbranding=1&playsinline=1&showinfo=0&rel=0&enablejsapi=1' */ html: `
`, id: ([id]) => id, }, vimeo: { regex: /^(?:http[s]?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)(?:\/([a-zA-Z0-9]+))?(?:\?[^\s]*)?$/, embedUrl: 'https://player.vimeo.com/video/<%= remote_id %>', html: ``, id: ([id, hash]) => (hash ? `${id}?h=${hash}` : id), }, cloudflareStream: { regex: /^https:\/\/customer-[a-z0-9]+\.cloudflarestream\.com\/([a-f0-9]{32})\/watch$/, embedUrl: 'https://iframe.videodelivery.net/<%= remote_id %>', html: ``, }, bunnyStream: { regex: /^https:\/\/(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)\/play\/([a-zA-Z0-9]+\/[a-zA-Z0-9-]+)$/, embedUrl: 'https://iframe.mediadelivery.net/embed/<%= remote_id %>', html: ``, }, codepen: true, aparat: { regex: /^(?:http[s]?:\/\/)?(?:www.)?aparat\.com\/v\/([^\/\?\&]+)\/?$/, embedUrl: 'https://www.aparat.com/video/video/embed/videohash/<%= remote_id %>/vt/frame', html: ``, }, github: true, slides: { regex: /^https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub$/, embedUrl: 'https://docs.google.com/presentation/d/<%= remote_id %>/embed', html: ``, }, drive: { regex: /^https:\/\/drive\.google\.com\/file\/d\/([A-Za-z0-9_-]+)\/view(\?.+)?$/, embedUrl: 'https://drive.google.com/file/d/<%= remote_id %>/preview', html: ``, }, docsPublic: { regex: /^https:\/\/docs\.google\.com\/document\/d\/([A-Za-z0-9_-]+)\/edit(\?.+)?$/, embedUrl: 'https://docs.google.com/document/d/<%= remote_id %>/preview', html: "", }, sheetsPublic: { regex: /^https:\/\/docs\.google\.com\/spreadsheets\/d\/([A-Za-z0-9_-]+)\/edit(\?.+)?$/, embedUrl: 'https://docs.google.com/spreadsheets/d/<%= remote_id %>/preview', html: "", }, slidesPublic: { regex: /^https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/edit(\?.+)?$/, embedUrl: 'https://docs.google.com/presentation/d/<%= remote_id %>/embed', html: "", }, codesandbox: { regex: /^https:\/\/codesandbox\.io\/(?:(?:p\/(?:sandbox|devbox)\/)|(?:embed\/)|(?:s\/))?([A-Za-z0-9_-]+)(?:[\/\?].*)?$/, embedUrl: 'https://codesandbox.io/embed/<%= remote_id %>?view=editor+%2B+preview&module=%2Findex.html', html: "", }, }, }, }, } } export function getTimezones() { return [ 'Pacific/Midway', 'Pacific/Pago_Pago', 'Pacific/Honolulu', 'America/Anchorage', 'America/Vancouver', 'America/Los_Angeles', 'America/Tijuana', 'America/Edmonton', 'America/Denver', 'America/Phoenix', 'America/Mazatlan', 'America/Winnipeg', 'America/Regina', 'America/Chicago', 'America/Mexico_City', 'America/Guatemala', 'America/El_Salvador', 'America/Managua', 'America/Costa_Rica', 'America/Montreal', 'America/New_York', 'America/Indianapolis', 'America/Panama', 'America/Bogota', 'America/Lima', 'America/Halifax', 'America/Puerto_Rico', 'America/Caracas', 'America/Santiago', 'America/St_Johns', 'America/Montevideo', 'America/Araguaina', 'America/Argentina/Buenos_Aires', 'America/Godthab', 'America/Sao_Paulo', 'Atlantic/Azores', 'Canada/Atlantic', 'Atlantic/Cape_Verde', 'UTC', 'Etc/Greenwich', 'Europe/Belgrade', 'CET', 'Atlantic/Reykjavik', 'Europe/Dublin', 'Europe/London', 'Europe/Lisbon', 'Africa/Casablanca', 'Africa/Nouakchott', 'Europe/Oslo', 'Europe/Copenhagen', 'Europe/Brussels', 'Europe/Berlin', 'Europe/Helsinki', 'Europe/Amsterdam', 'Europe/Rome', 'Europe/Stockholm', 'Europe/Vienna', 'Europe/Luxembourg', 'Europe/Paris', 'Europe/Zurich', 'Europe/Madrid', 'Africa/Bangui', 'Africa/Algiers', 'Africa/Tunis', 'Africa/Harare', 'Africa/Nairobi', 'Europe/Warsaw', 'Europe/Prague', 'Europe/Budapest', 'Europe/Sofia', 'Europe/Istanbul', 'Europe/Athens', 'Europe/Bucharest', 'Asia/Nicosia', 'Asia/Beirut', 'Asia/Damascus', 'Asia/Jerusalem', 'Asia/Amman', 'Africa/Tripoli', 'Africa/Cairo', 'Africa/Johannesburg', 'Europe/Moscow', 'Asia/Baghdad', 'Asia/Kuwait', 'Asia/Riyadh', 'Asia/Bahrain', 'Asia/Qatar', 'Asia/Aden', 'Asia/Tehran', 'Africa/Khartoum', 'Africa/Djibouti', 'Africa/Mogadishu', 'Asia/Dubai', 'Asia/Muscat', 'Asia/Baku', 'Asia/Kabul', 'Asia/Yekaterinburg', 'Asia/Tashkent', 'Asia/Calcutta', 'Asia/Kathmandu', 'Asia/Novosibirsk', 'Asia/Almaty', 'Asia/Dacca', 'Asia/Krasnoyarsk', 'Asia/Dhaka', 'Asia/Bangkok', 'Asia/Saigon', 'Asia/Jakarta', 'Asia/Irkutsk', 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Taipei', 'Asia/Kuala_Lumpur', 'Asia/Singapore', 'Australia/Perth', 'Asia/Yakutsk', 'Asia/Seoul', 'Asia/Tokyo', 'Australia/Darwin', 'Australia/Adelaide', 'Asia/Vladivostok', 'Pacific/Port_Moresby', 'Australia/Brisbane', 'Australia/Sydney', 'Australia/Hobart', 'Asia/Magadan', 'SST', 'Pacific/Noumea', 'Asia/Kamchatka', 'Pacific/Fiji', 'Pacific/Auckland', 'Asia/Kolkata', 'Europe/Kiev', 'America/Tegucigalpa', 'Pacific/Apia', ] } export function getUserTimezone() { try { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone const supportedTimezones = getTimezones() if (supportedTimezones.includes(timezone)) { return timezone // e.g., 'Asia/Calcutta', 'America/New_York', etc. } else { throw Error('unsupported timezone') } } catch (error) { console.error('Error getting timezone:', error) return null } } export function getSidebarLinks() { let links = getSidebarItems() links.forEach((link) => { link.items = link.items.filter((item) => { return item.condition ? item.condition() : true }) }) links = links.filter((link) => { return link.items.length > 0 }) return links } const getSidebarItems = () => { const { userResource } = usersStore() const { settings } = useSettings() return [ { label: 'General', hideLabel: true, items: [ { label: 'Home', icon: 'Home', to: 'Home', condition: () => { return userResource?.data }, }, { label: 'Search', icon: 'Search', to: 'Search', condition: () => { return userResource?.data }, }, { label: 'Notifications', icon: 'Bell', to: 'Notifications', condition: () => { return userResource?.data }, }, ], }, { label: 'Learning', hideLabel: true, items: [ { label: 'Courses', icon: 'BookOpen', to: 'Courses', activeFor: [ 'Courses', 'CourseDetail', 'Lesson', 'LessonForm', ], }, { label: 'Programs', icon: 'Route', to: 'Programs', activeFor: ['Programs', 'ProgramDetail'], await: true, condition: () => { return checkIfCanAddProgram() }, }, { label: 'Batches', icon: 'Users', to: 'Batches', activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'], }, { label: 'Certifications', icon: 'GraduationCap', to: 'CertifiedParticipants', activeFor: ['CertifiedParticipants'], condition: () => { return userResource?.data }, }, { label: 'Jobs', icon: 'Briefcase', to: 'Jobs', activeFor: ['Jobs', 'JobDetail'], }, { label: 'Statistics', icon: 'TrendingUp', to: 'Statistics', activeFor: ['Statistics'], }, { label: 'Contact Us', icon: settings.data?.contact_us_url ? 'Headset' : 'Mail', to: settings.data?.contact_us_url ? settings.data?.contact_us_url : settings.data?.contact_us_email, condition: () => { return ( (settings?.data?.contact_us_email && userResource?.data) || settings?.data?.contact_us_url ) }, }, ], }, { label: 'Assessments', hideLabel: true, items: [ { label: 'Quizzes', icon: 'CircleHelp', to: 'Quizzes', condition: () => { return isAdmin() }, activeFor: [ 'Quizzes', 'QuizForm', 'QuizPage', 'QuizSubmissionList', 'QuizSubmission', ], }, { label: 'Assignments', icon: 'Pencil', to: 'Assignments', condition: () => { return isAdmin() }, activeFor: [ 'Assignments', 'AssignmentSubmissionList', 'AssignmentSubmission', ], }, { label: 'Programming Exercises', icon: 'Code', to: 'ProgrammingExercises', condition: () => { return isAdmin() }, activeFor: [ 'ProgrammingExercises', 'ProgrammingExerciseSubmissions', 'ProgrammingExerciseSubmission', ], }, ], }, ] } const isAdmin = () => { const { userResource } = usersStore() return ( userResource?.data?.is_instructor || userResource?.data?.is_moderator || userResource.data?.is_evaluator ) } const checkIfCanAddProgram = () => { const { userResource } = usersStore() const { programs } = useSettings() if (!userResource.data) return false if (userResource?.data?.is_moderator || userResource?.data?.is_instructor) { return true } return ( programs.data?.enrolled.length > 0 || programs.data?.published.length > 0 ) } export function getFormattedDateRange( startDate, endDate, format = 'DD MMM YYYY' ) { if (startDate === endDate) { return dayjs(startDate).format(format) } return `${dayjs(startDate).format(format)} - ${dayjs(endDate).format( format )}` } export function getLineStartPosition(string, position) { const charLength = 1 let char = '' while (char !== '\n' && position > 0) { position = position - charLength char = string.substr(position, charLength) } if (char === '\n') { position += 1 } return position } export function singularize(word) { const endings = { ves: 'fe', ies: 'y', i: 'us', zes: 'ze', ses: 's', es: 'e', s: '', } return word.replace( new RegExp(`(${Object.keys(endings).join('|')})$`), (r) => endings[r] ) } export const validateFile = async ( file, showToast = true, fileType = 'image' ) => { const error = (msg) => { if (showToast) toast.error(msg) console.error(msg) return msg } if (!file.type.startsWith(`${fileType}/`)) { return error(__('Only {0} file is allowed.').format(fileType)) } if (file.type === 'image/svg+xml') { const text = await file.text() const blacklist = [ /