SimpleLMS

This commit is contained in:
l.gabrysiak 2024-08-21 15:01:45 +02:00
parent 6823c35667
commit 2250f540f5
49 changed files with 11917 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/Plugins/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/Plugins/Misc.SimpleLMS/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,34 @@
@model CourseModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@inject INopHtmlHelper NopHtml
@{
//page title
ViewBag.PageTitle = T("SimpleLMS.Add").Text + " " + T("SimpleLMS.Course");
//active menu item (system name)
NopHtml.SetActiveMenuItemSystemName("SimpleLMS.Courses");
}
<form asp-controller="Course" asp-action="Create" method="post" id="product-form">
<div class="content-header clearfix">
<h1 class="float-left">
@T("SimpleLMS.Add") @T("SimpleLMS.Course")
<small>
<i class="fas fa-arrow-circle-left"></i>
<a asp-action="List">@T("SimpleLMS.BackToList")</a>
</small>
</h1>
<div class="float-right">
<button type="submit" name="save" class="btn btn-primary">
<i class="far fa-save"></i>
@T("Admin.Common.Save")
</button>
<button type="submit" name="save-continue" class="btn btn-primary">
<i class="far fa-save"></i>
@T("Admin.Common.SaveContinue")
</button>
</div>
</div>
@await Html.PartialAsync("_CreateOrUpdate.cshtml", Model)
</form>

View File

@ -0,0 +1,51 @@
@model CourseModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@inject INopHtmlHelper NopHtml
@{
//page title
ViewBag.PageTitle = T("SimpleLMS.Edit").Text + " " + T("SimpleLMS.Course");
//active menu item (system name)
NopHtml.SetActiveMenuItemSystemName("SimpleLMS.Courses");
}
@*<link href="@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/css/jquery-ui-1.10.4.custom.min.css")" rel="stylesheet" type="text/css" />*@
<link href="@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/css/simplelms.css")" type="text/css" rel="stylesheet" />
@*<script type="text/javascript" src="@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/js/jquery-ui-1.10.4.custom.min.js")"></script>*@
<form asp-controller="Course" asp-action="Edit" method="post" id="product-form">
<input type="hidden" id="Id" name="Id" value="@Model.Id" />
<div class="content-header clearfix">
<h1 class="float-left">
@T("SimpleLMS.Edit") @T("SimpleLMS.Course") - @Model.Name
<small>
<i class="fas fa-arrow-circle-left"></i>
<a asp-action="List">@T("SimpleLMS.BackToList")</a>
</small>
</h1>
<div class="float-right">
<button type="submit" name="save" class="btn btn-primary">
<i class="far fa-save"></i>
@T("Admin.Common.Save")
</button>
<button type="submit" name="save-continue" class="btn btn-primary">
<i class="far fa-save"></i>
@T("Admin.Common.SaveContinue")
</button>
<span id="course-delete" class="btn btn-danger">
<i class="far fa-trash-alt"></i>
@T("Admin.Common.Delete")
</span>
</div>
</div>
@await Html.PartialAsync("_CreateOrUpdate.cshtml", Model)
</form>
<nop-delete-confirmation asp-model-id="@Model.Id" asp-button-id="course-delete" />
<script src="@Url.Content("~/js/public.common.js")"></script>

View File

@ -0,0 +1,183 @@
@model CourseSearchModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@inject INopHtmlHelper NopHtml
@{
//page title
ViewBag.PageTitle = T("SimpleLMS.CourseList").Text;
//active menu item (system name)
NopHtml.SetActiveMenuItemSystemName("SimpleLMS.Courses");
}
@{
const string hideSearchBlockAttributeName = "CourseListPage.HideSearchBlock";
var hideSearchBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideSearchBlockAttributeName);
}
<form asp-controller="Course" asp-action="List" method="post">
<div class="content-header clearfix">
<h1 class="float-left">
@T("SimpleLMS.CourseList")
</h1>
<div class="float-right">
<a asp-action="Create" class="btn btn-primary">
<i class="fas fa-plus-square"></i>
@T("Admin.Common.AddNew")
</a>
@*<button type="button" id="delete-selected" class="btn btn-danger">
<i class="far fa-trash-alt"></i>
@T("Admin.Common.Delete.Selected")
</button>*@
@*<nop-action-confirmation asp-button-id="delete-selected" />*@
</div>
</div>
<section class="content">
<div class="container-fluid">
<div class="form-horizontal">
<div class="cards-group">
<div class="card card-default card-search">
<div class="card-body">
<div class="row search-row @(!hideSearchBlock ? "opened" : "")" data-hideAttribute="@hideSearchBlockAttributeName">
<div class="search-text">@T("Admin.Common.Search")</div>
<div class="icon-search"><i class="fas fa-search" aria-hidden="true"></i></div>
<div class="icon-collapse"><i class="far fa-angle-@(!hideSearchBlock ? "up" : "down")" aria-hidden="true"></i></div>
</div>
<div class="search-body @(hideSearchBlock ? "closed" : "")">
<div class="row">
<div class="col-md-5">
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="SearchCourseName" />
</div>
<div class="col-md-8">
<nop-editor asp-for="SearchCourseName" />
</div>
</div>
</div>
</div>
<div class="row">
<div class="text-center col-12">
<button type="button" id="search-courses" class="btn btn-primary btn-search">
<i class="fas fa-search"></i>
@T("Admin.Common.Search")
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card card-default">
<div class="card-body">
@await Html.PartialAsync("Table", new DataTablesModel
{
Name = "courses-grid",
UrlRead = new DataUrl("CourseList", "Course", null),
SearchButtonId = "search-courses",
Length = Model.PageSize,
LengthMenu = Model.AvailablePageSizes,
Filters = new List<FilterParameter>
{
new FilterParameter(nameof(Model.SearchCourseName))
},
ColumnCollection = new List<ColumnProperty>
{
new ColumnProperty(nameof(CourseModel.Id))
{
IsMasterCheckBox = true,
Render = new RenderCheckBox("checkbox_courses"),
ClassName = NopColumnClassDefaults.CenterAll,
Width = "50"
},
new ColumnProperty(nameof(CourseModel.Name))
{
Title = T("SimpleLMS.Name").Text
},
new ColumnProperty(nameof(CourseModel.Instructor))
{
Title = T("SimpleLMS.Instructor").Text
},
new ColumnProperty(nameof(CourseModel.Category))
{
Title = T("SimpleLMS.Category").Text
},
new ColumnProperty(nameof(CourseModel.SectionsTotal))
{
Title = T("SimpleLMS.Sections").Text
},
new ColumnProperty(nameof(CourseModel.EnrolledStudents))
{
Title = T("SimpleLMS.EnrolledStudents").Text
},
new ColumnProperty(nameof(CourseModel.Status))
{
Title = T("SimpleLMS.Status").Text
},
new ColumnProperty(nameof(CourseModel.Price))
{
Title = T("SimpleLMS.Price").Text
},
new ColumnProperty(nameof(CourseModel.Id))
{
Title = T("Admin.Common.Edit").Text,
Width = "80",
ClassName = NopColumnClassDefaults.Button,
Render = new RenderButtonEdit(new DataUrl("Edit"))
}
}
})
<script>
$(document).ready(function () {
$('#delete-selected-action-confirmation-submit-button').bind('click', function () {
var postData = {
selectedIds: selectedIds
};
addAntiForgeryToken(postData);
$.ajax({
cache: false,
type: "POST",
url: "@(Url.Action("DeleteSelected", "Course"))",
data: postData,
error: function (jqXHR, textStatus, errorThrown) {
$('#deleteSelectedFailed-info').text(errorThrown);
$('#deleteSelectedFailed').click();
},
complete: function (jqXHR, textStatus) {
updateTable('#courses-grid');
}
});
$('#delete-selected-action-confirmation').modal('toggle');
return false;
});
});
</script>
<nop-alert asp-alert-id="deleteSelectedFailed" />
</div>
</div>
</div>
</div>
</div>
</section>
</form>

View File

@ -0,0 +1,29 @@
@model AttachmentModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card-body">
<div class="card bg-light mb-3">
<div class="card-header"> @Html.DisplayText(Model.Name)</div>
<div class="card-body">
<h5 class="card-title">
</h5>
<p class="card-text"></p>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
});
</script>

View File

@ -0,0 +1,297 @@
@model CourseModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card-body">
@if (Model.Id > 0)
{
<div class="text-center">
<button type="button" class="btn btn-primary btn-sm" id="add-section">@T("SimpleLMS.Add") @T("SimpleLMS.Section")</button>
<button type="button" class="btn btn-secondary btn-sm" id="sort-sections" onclick="sortSections(@Model.Id)">@T("SimpleLMS.Sort") @T("SimpleLMS.Sections")</button>
</div>
<div class="card-body" id="sections">
@* @await Html.PartialAsync("_CreateOrUpdate.Sections.cshtml", Model.Sections);*@
</div>
<script type="text/javascript">
$(document).ready(function () {
refreshSections();
$('#add-section').on('click',
function (e) {
e.preventDefault();
@*displayPopupContentFromUrl('@Url.Action("CreateSection", "Course")?courseId=@Model.Id',
'@T("SimpleLMS.CreateNewSection")');*@
$('<div id="create-or-edit-section-modal" class="form-horizontal"></div>').load('@Url.Action("CreateSection", "Course")?courseId=@Model.Id')
.dialog({
modal: true,
width: 800,
position: {
of: window, my: "top+100", at: "top"
},
maxHeight: $(window).height() - 20,
title: '@T("SimpleLMS.Create") @T("SimpleLMS.Section")',
close: function (event, ui) {
$(this).dialog('destroy').remove();
}
});
});
$('#delete-anything-close-dialog').click(function () {
closeConfirmDialog();
});
});
function closeConfirmDialog() {
$('#delete-anything').dialog("close");
}
function editSection(sectionId)
{
$('<div id="create-or-edit-section-modal" class="form-horizontal"></div>').load('@Url.Action("EditSection", "Course")?sectionId=' + sectionId)
.dialog({
modal: true,
width: 800,
position: {
of: window, my: "top+100", at: "top"
},
maxHeight: $(window).height() - 20,
title: '@T("SimpleLMS.Edit") @T("SimpleLMS.Section")',
close: function (event, ui) {
$(this).dialog('destroy').remove();
}
});
}
function refreshSections() {
$("#sections").load('@Url.Action("Sections", "Course")' + '?courseId=@Model.Id');
}
function addLesson(sectionId,courseId)
{
$('<div id="create-or-edit-lesson-modal" class="form-horizontal"></div>').load('@Url.Action("CreateLesson", "Course")?sectionId=' + sectionId + '&courseId=' + courseId)
.dialog({
modal: true,
width: 800,
position: {
of: window, my: "top+100", at: "top"
},
maxHeight: $(window).height() - 20,
title: '@T("SimpleLMS.Create") @T("SimpleLMS.Lesson")',
close: function (event, ui) {
$(this).dialog('destroy').remove();
removeTinymce();
}
});
}
function editLesson(lessonId,sectionId,courseId)
{
$('<div id="create-or-edit-lesson-modal" class="form-horizontal"></div>').load('@Url.Action("EditLesson", "Course")?lessonId=' + lessonId + '&sectionId=' + sectionId + '&courseId=' + courseId)
.dialog({
modal: true,
width: 800,
position: {
of: window, my: "top+100", at: "top"
},
maxHeight: $(window).height() - 20,
title: '@T("SimpleLMS.Edit") @T("SimpleLMS.Lesson")',
close: function (event, ui) {
$(this).dialog('destroy').remove();
removeTinymce();
}
});
}
function sortLessons(sectionId, courseId)
{
$('<div id="sort-modal" class="form-horizontal"></div>').load('@Url.Action("SortLessons", "Course")?sectionId=' + sectionId)
.dialog({
modal: true,
width: 800,
position: {
of: window, my: "top+100", at: "top"
},
maxHeight: $(window).height() - 20,
title: '@T("SimpleLMS.Sort") @T("SimpleLMS.Lessons")',
close: function (event, ui) {
$(this).dialog('destroy').remove();
removeTinymce();
}
});
}
function sortSections(courseId)
{
$('<div id="sort-modal" class="form-horizontal"></div>').load('@Url.Action("SortSections", "Course")?courseId=' + courseId)
.dialog({
modal: true,
width: 800,
position: {
of: window, my: "top+100", at: "top"
},
maxHeight: $(window).height() - 20,
title: '@T("SimpleLMS.Sort") @T("SimpleLMS.Sections")',
close: function (event, ui) {
$(this).dialog('destroy').remove();
removeTinymce();
}
});
}
function deleteLesson(lessonId,sectionId,courseId)
{
openDialog();
$('#delete-anything-submit-button').off();
$('#delete-anything-submit-button').on('click',
function (e) {
$.ajax({
type: "POST",
url: '@Url.Action("DeleteLesson", "Course")',
//data: $('#create-or-edit-section').serialize(),
data: {
lessonId: lessonId,
sectionId: sectionId,
courseId: courseId
},
headers: {
"RequestVerificationToken": $('input:hidden[name="__RequestVerificationToken"]').val()
},
error: function (jqXHR, textStatus, errorThrown) {
closeConfirmDialog();
$('#deleteSelectedFailed-info').text(jqXHR.responseText + ' ' + errorThrown);
$('#deleteSelectedFailed').click();
},
complete: function (jqXHR, textStatus) {
closeConfirmDialog();
refreshSections();
}
})
});
}
function deleteSection(sectionId)
{
openDialog();
$('#delete-anything-submit-button').off();
$('#delete-anything-submit-button').on('click',
function(e) {
$.ajax({
type: "POST",
url: '@Url.Action("DeleteSection", "Course")',
//data: $('#create-or-edit-section').serialize(),
data: {
sectionId: sectionId
},
headers: {
"RequestVerificationToken": $('input:hidden[name="__RequestVerificationToken"]').val()
},
error: function (jqXHR, textStatus, errorThrown) {
closeConfirmDialog();
$('#deleteSelectedFailed-info').text(jqXHR.responseText + ' ' + errorThrown);
$('#deleteSelectedFailed').click();
},
complete: function (jqXHR, textStatus) {
closeConfirmDialog();
refreshSections();
}
});
});
}
function deleteCourse(courseId)
{
openDialog();
$('#delete-anything-submit-button').off();
$('#delete-anything-submit-button').on('click',
function(e) {
$.ajax({
type: "POST",
url: '@Url.Action("Delete", "Course")',
//data: $('#create-or-edit-section').serialize(),
data: {
courseId: courseId
},
headers: {
"RequestVerificationToken": $('input:hidden[name="__RequestVerificationToken"]').val()
},
error: function (jqXHR, textStatus, errorThrown) {
closeConfirmDialog();
$('#deleteSelectedFailed-info').text(jqXHR.responseText + ' ' + errorThrown);
$('#deleteSelectedFailed').click();
},
complete: function (jqXHR, textStatus) {
closeConfirmDialog();
refreshSections();
}
});
});
}
function openDialog() {
$('#delete-anything').dialog({
resizable: false,
height: "auto",
width: 400,
modal: true,
title: '@T("Admin.Common.AreYouSure")'
});
}
function removeTinymce() {
while (tinymce.editors.length > 0) {
tinymce.remove(tinymce.editors[0]);
}
}
</script>
<nop-alert asp-alert-id="deleteSelectedFailed" />
}
else
{
<div class="alert alert-warning alert-dismissible fade show" role="alert">
@T("SimpleLMS.SaveCourseToAddContentMessage")
</div>
}
</div>
<div id="delete-anything" style="display: none;">
<div class="modal-body">
@T("Admin.Common.DeleteConfirmation")
</div>
<div class="modal-footer">
<button type="submit" id="delete-anything-submit-button" class="btn btn-primary float-right">
@T("Admin.Common.Yes")
</button> <span class="btn btn-default float-right margin-r-5" data-dismiss="modal" id="delete-anything-close-dialog">
@T("Admin.Common.NoCancel")
</span>
</div>
</div>

View File

@ -0,0 +1,35 @@
@model CourseModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="ProductId" />
</div>
<div class="col-md-9">
<nop-select asp-for="ProductId" asp-items="Model.AvailableProducts" asp-required="true" />
<span asp-validation-for="ProductId"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="Name"/>
</div>
<div class="col-md-9">
<nop-editor asp-for="Name" asp-required="true" />
<span asp-validation-for="Name"></span>
</div>
</div>
</div>

View File

@ -0,0 +1,46 @@
@model LessonModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card bg-white ml-3" >
<div class="card-header border-info">
@if (Model.LessonType == Nop.Plugin.Misc.SimpleLMS.Domains.LessonType.Video)
{
<img src='@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/images/video-player.png")' height="16" class="mr-3" />
}
else if (Model.LessonType == Nop.Plugin.Misc.SimpleLMS.Domains.LessonType.Text)
{
<img src='@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/images/text.png")' height="16" class="mr-3" />
}
@Model.DisplayOrder. Lesson: <strong>@Model.Name</strong>
@if (Model.IsFreeLesson)
{
<span class='badge badge-success ml-2'>Free</span>
}
<div class="float-right">
<button type="button" class="btn btn-primary btn-sm" onclick="editLesson(@Model.Id,@Model.SectionId,@Model.CourseId)" id="edit-lesson-@Model.Id">@T("SimpleLMS.Edit") @T("SimpleLMS.Lesson")</button>&nbsp;
<button type="button" class="btn btn-danger btn-sm" id="delete-lesson-@Model.Id" onclick="deleteLesson(@Model.Id,@Model.SectionId,@Model.CourseId)">@T("SimpleLMS.Delete") @T("SimpleLMS.Lesson")</button>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
});
</script>

View File

@ -0,0 +1,265 @@
@model LessonModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<form asp-controller="Course" asp-action="CreateLesson" method="post" id="lesson-form">
<div class="card-body" id="create-or-edit-lesson">
<input type="hidden" id="Id" name="Id" value="@Model.Id" />
<input type="hidden" id="CourseId" name="CourseId" value="@Model.CourseId" />
<input type="hidden" id="SectionId" name="SectionId" value="@Model.SectionId" />
<input type="hidden" id="DisplayOrder" name="DisplayOrder" value="@Model.DisplayOrder" />
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="Name" />
</div>
<div class="col-md-9">
<nop-editor asp-for="Name" asp-required="true" />
<span asp-validation-for="Name"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="IsFreeLesson" />
</div>
<div class="col-md-9">
<nop-editor asp-for="IsFreeLesson" asp-required="false" />
<span asp-validation-for="IsFreeLesson"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="LessonType" />
</div>
<div class="col-md-9">
<nop-select asp-for="LessonType" asp-items="Model.AvailableLessonTypes" asp-required="true" />
<span asp-validation-for="LessonType"></span>
</div>
</div>
<div class="form-group row" id="lesson-contents">
<div class="col-md-3">
<nop-label asp-for="LessonContents" />
</div>
<div class="col-md-9">
<nop-editor asp-for="LessonContents" asp-template="RichEditor" />
<span asp-validation-for="LessonContents"></span>
</div>
</div>
<div class="form-group row" id="video-type">
<div class="col-md-3">
<nop-label asp-for="VideoType" />
</div>
<div class="col-md-9">
<nop-select asp-for="VideoType" asp-items="Model.AvailableVideoTypes" asp-required="true" />
<span asp-validation-for="VideoType"></span>
</div>
</div>
<div class="form-group row" id="video-id-from-provider">
<div class="col-md-3">
<nop-label asp-for="VideoIdFromProvider" />
</div>
<div class="col-md-9">
<nop-editor asp-for="VideoIdFromProvider" asp-required="true" />
<span asp-validation-for="VideoIdFromProvider"></span>
</div>
</div>
<div class="form-group row" id="video-embed-code">
<div class="col-md-3">
<nop-label asp-for="VideoEmbedCode" />
</div>
<div class="col-md-9">
<nop-editor asp-for="VideoEmbedCode" asp-required="true" />
<span asp-validation-for="VideoEmbedCode"></span>
</div>
</div>
<div class="form-group row" id="video-url">
<div class="col-md-3">
<nop-label asp-for="VideoUrl" />
</div>
<div class="col-md-9">
<nop-editor asp-for="VideoUrl" asp-required="true" />
<span asp-validation-for="VideoUrl"></span>
</div>
</div>
<div class="form-group row" id="video-duration">
<div class="col-md-3">
<nop-label asp-for="Duration" />
</div>
<div class="col-md-9">
<nop-editor asp-for="Duration" asp-required="true" />
<span asp-validation-for="Duration"></span>
</div>
</div>
<div class="form-group row" id="attachment-type">
<div class="col-md-3">
<nop-label asp-for="AttachmentType" />
</div>
<div class="col-md-9">
<nop-select asp-for="AttachmentType" asp-items="Model.AvailableAttachmentTypes" asp-required="true" />
<span asp-validation-for="AttachmentType"></span>
</div>
</div>
<div class="form-group row" id="submit-button">
<div class="col-md-3">
</div>
<div class="col-md-9">
<button type="button" class="btn btn-primary btn-sm" id="add-lesson-submit">
@(Model.Id>0 ? T("SimpleLMS.Update") + " " + @T("SimpleLMS.Lesson") : T("SimpleLMS.Add") + " " + @T("SimpleLMS.Lesson"))
</button>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
$('#add-lesson-submit').off();
$('#add-lesson-submit').on('click',
function (e) {
e.preventDefault();
var _form = $(this).closest("form");
_form.removeData('validator');
_form.removeData('unobtrusiveValidation');
$.validator.unobtrusive.parse(_form);
var isValid = $(_form).validate().form();
if (!isValid) {
return false;
}
//alert();
//alert($('#create-or-edit-section').serialize());
$(this).prop('disabled', true);
$.ajax({
type: "POST",
url: '@Url.Action((Model.Id>0? "EditLesson": "CreateLesson"), "Course")',
//data: $('#create-or-edit-section').serialize(),
data: {
id: $("#create-or-edit-lesson").find("#Id").val(),
courseId: $("#create-or-edit-lesson").find("#CourseId").val(),
sectionId: $("#create-or-edit-lesson").find("#SectionId").val(),
displayOrder: $("#create-or-edit-lesson").find("#DisplayOrder").val(),
name: $("#create-or-edit-lesson").find("#Name").val(),
isFreeLesson: $("#create-or-edit-lesson").find("#IsFreeLesson").is(":checked"),
lessonType: $("#create-or-edit-lesson").find("#LessonType").val(),
videoType: $("#create-or-edit-lesson").find("#VideoType").val(),
videoIdFromProvider: $("#create-or-edit-lesson").find("#VideoIdFromProvider").val(),
duration: $("#create-or-edit-lesson").find("#Duration").val(),
lessonContents: tinymce.get("LessonContents").getContent({ format: "html" })
},
headers: {
"RequestVerificationToken":
$('input:hidden[name="__RequestVerificationToken"]').val()
},
success: function (response) {
$("form").removeData("unobtrusiveValidation");
$.validator.unobtrusive.parse("form");
$('#create-or-edit-lesson-modal').dialog('destroy').remove();
removeTinymce();
refreshSections();
},
failure: function (response) {
$('#add-lesson-submit').prop('disabled', false);
alert(JSON.stringify(response));
},
error: function (response) {
let obj = JSON.parse(response.responseText);
var errorArray = {};
$.each(obj, function (key, value) {
errorArray[key] = value;
});
try {
$('#lesson-form').data('validator').showErrors(errorArray);
}
catch (e) {
}
$('#add-lesson-submit').prop('disabled', false);
}
});
});
$("#LessonType").change(function () {
lessonTypeChange();
});
hidelAll();
lessonTypeChange();
});
function lessonTypeChange() {
hidelAll();
var lessonTypeText = $('#LessonType').find(":selected").text();
if (lessonTypeText == "Video") {
$("#video-type").show();
$("#video-id-from-provider").show();
$("#video-duration").show();
$("#lesson-contents").show();
}
else if (lessonTypeText == "Text") {
$("#lesson-contents").show();
}
else if (lessonTypeText == "Document") {
$("#attachment-type").show();
}
$("#submit-button").show();
}
function hidelAll() {
$("#lesson-contents").hide();
$("#video-type").hide();
$("#video-id-from-provider").hide();
$("#video-embed-code").hide();
$("#video-url").hide();
$("#video-duration").hide();
$("#attachment-type").hide();
$("#submit-button").hide();
}
</script>
</form>

View File

@ -0,0 +1,17 @@
@model IList<LessonModel>
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
@foreach (var lesson in Model)
{
@await Html.PartialAsync("_CreateOrUpdate.Lesson.cshtml", lesson)
}

View File

@ -0,0 +1,43 @@
@model SectionModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card bg-light mb-3">
<div class="card-header">
@Model.DisplayOrder. @T("SimpleLMS.Section"): <strong>@Model.Title</strong>
@if (Model.IsFree)
{
<span class='badge badge-success ml-2'>Free</span>
}
<div class="float-right">
<button type="button" class="btn btn-primary btn-sm" onclick="editSection(@Model.Id)" id="edit-section-@Model.Id">@T("SimpleLMS.Edit") @T("SimpleLMS.Section")</button>&nbsp;
<button type="button" class="btn btn-primary btn-sm" onclick="addLesson(@Model.Id,@Model.CourseId)" id="add-lesson-@Model.Id">@T("SimpleLMS.Add") @T("SimpleLMS.Lesson")</button>&nbsp;
<button type="button" class="btn btn-secondary btn-sm" id="sort-lessons-@Model.Id" onclick="sortLessons(@Model.Id,@Model.CourseId)">@T("SimpleLMS.Sort") @T("SimpleLMS.Lessons")</button>&nbsp;
<button type="button" class="btn btn-danger btn-sm" id="delete-section-@Model.Id" onclick="deleteSection(@Model.Id)">@T("SimpleLMS.Delete") @T("SimpleLMS.Section")</button>
</div>
</div>
<div class="card-body">
@if (Model.Lessons != null && Model.Lessons.Count > 0)
{
@await Html.PartialAsync("_CreateOrUpdate.Lessons.cshtml", Model.Lessons)
}
else
{
<h5 class="card-title">
<strong> @T("SimpleLMS.NoLessonsAvailable")</strong>
</h5>
}
</div>
</div>

View File

@ -0,0 +1,112 @@
@model SectionModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<form asp-controller="Course" asp-action="CreateSection" method="post" id="section-form">
<div class="card-body" id="create-or-edit-section">
<input type="hidden" id="Id" name="Id" value="@Model.Id" />
<input type="hidden" id="CourseId" name="CourseId" value="@Model.CourseId" />
<input type="hidden" id="DisplayOrder" name="DisplayOrder" value="@Model.DisplayOrder" />
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="Title" />
</div>
<div class="col-md-9">
<nop-editor asp-for="Title" asp-required="true" />
<span asp-validation-for="Title"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="IsFree" />
</div>
<div class="col-md-9">
<nop-editor asp-for="IsFree" asp-required="false" />
<span asp-validation-for="IsFree"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
</div>
<div class="col-md-9">
<button type="button" class="btn btn-primary btn-sm" id="add-section-submit">
@(Model.Id>0 ? T("SimpleLMS.Update") + " " + @T("SimpleLMS.Section") : T("SimpleLMS.Add") + " " + @T("SimpleLMS.Section"))
</button>
</div>
</div>
</div>
<script type="text/javascript">
$('#add-lesson-submit').off();
$('#add-section-submit').on('click',
function (e) {
e.preventDefault();
var _form = $(this).closest("form");
_form.removeData('validator');
_form.removeData('unobtrusiveValidation');
$.validator.unobtrusive.parse(_form);
var isValid = $(_form).validate().form();
if (!isValid) {
return false;
}
//alert($('#create-or-edit-section').serialize());
$(this).prop('disabled', true);
$.ajax({
type: "POST",
url: '@Url.Action((Model.Id > 0 ? "EditSection" : "CreateSection"), "Course")',
//data: $('#create-or-edit-section').serialize(),
data: {
id: $("#create-or-edit-section").find("#Id").val(),
courseId: $("#create-or-edit-section").find("#CourseId").val(),
displayOrder: $("#create-or-edit-section").find("#DisplayOrder").val(),
title: $("#create-or-edit-section").find("#Title").val(),
isFree: $("#create-or-edit-section").find("#IsFree").is(":checked")
},
headers: {
"RequestVerificationToken":
$('input:hidden[name="__RequestVerificationToken"]').val()
},
success: function (response) {
$('#create-or-edit-section-modal').dialog('destroy').remove();
refreshSections();
},
failure: function (response) {
alert(JSON.stringify(response));
$(this).prop('enabled', false);
},
error: function (response) {
alert(JSON.stringify(response));
$(this).prop('enabled', false);
}
});
});
</script>
</form>

View File

@ -0,0 +1,16 @@
@model IList<SectionModel>
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
@foreach (var section in Model)
{
@await Html.PartialAsync("_CreateOrUpdate.Section.cshtml", section)
}

View File

@ -0,0 +1,154 @@
@model SortableEntity
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Domains;
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<form method="post" id="sort-form">
<div class="card-body" id="sort">
<input type="hidden" id="ParentId" name="ParentId" value="@Model.ParentId" />
<input type="hidden" id="SortRecordType" name="SortRecordType" value="@Model.SortRecordType" />
<ul id="sort-records" class="list-group bg-white">
@if (Model.SortRecords != null && Model.SortRecords.Count > 0)
{
for (int i = 0; i < Model.SortRecords.Count; i++)
{
<li class="ui-state-default list-group-item mb-1 all-scroll border-0" id="@Model.SortRecords[i].Id">
<span class="border-1 border-info">
<strong> @Model.SortRecords[i].DisplayText</strong>
</span>
</li>
}
}
</ul>
<div class="form-group row mt-3" id="submit-button">
<div class="col-md-12">
@if (Model.SortRecords != null && Model.SortRecords.Count > 0)
{
<button type="button" class="btn btn-primary btn-sm float-right" id="sort-submit">
@T("SimpleLMS.Update")
</button>
}
else if (@Model.SortRecordType == SortRecordType.Section)
{
<span>
@T("SimpleLMS.NoSectionsAvailable")
</span>
}
else if (@Model.SortRecordType == SortRecordType.Lesson)
{
<span>
@T("SimpleLMS.NoLessonsAvailable")
</span>
}
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
$("#sort-records").sortable();
$("#sort-records").disableSelection();
$('#sort-submit').off();
$('#sort-submit').on('click',
function (e) {
e.preventDefault();
var idsInOrder = $("#sort-records").sortable("toArray");
var _form = $(this).closest("form");
_form.removeData('validator');
_form.removeData('unobtrusiveValidation');
$.validator.unobtrusive.parse(_form);
var isValid = $(_form).validate().form();
if (!isValid) {
return false;
}
$(this).prop('disabled', true);
$.ajax({
type: "POST",
url: '@Url.Action((Model.SortRecordType == SortRecordType.Lesson ? "SortLessons" : "SortSections"), "Course")',
//data: $('#create-or-edit-section').serialize(),
data: {
parentId: $("#sort").find("#ParentId").val(),
sortRecordType: $("#sort").find("#SortRecordType").val(),
newSortOrderValues: JSON.stringify(idsInOrder),
},
headers: {
"RequestVerificationToken":
$('input:hidden[name="__RequestVerificationToken"]').val()
},
success: function (response) {
$("form").removeData("unobtrusiveValidation");
$.validator.unobtrusive.parse("form");
$('#sort-modal').dialog('destroy').remove();
refreshSections();
},
failure: function (response) {
$('#sort-submit').prop('disabled', false);
alert(JSON.stringify(response));
},
error: function (response) {
try {
let obj = JSON.parse(response.responseText);
var errorArray = {};
$.each(obj, function (key, value) {
errorArray[key] = value;
});
$('#sort-form').data('validator').showErrors(errorArray);
}
catch (e) {
alert(response.responseText);
}
$('#sort-submit').prop('disabled', false);
}
});
});
});
</script>
<style type="text/css">
.all-scroll {
cursor: all-scroll;
}
</style>
</form>

View File

@ -0,0 +1,29 @@
@model VideoModel
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card-body">
<div class="card bg-light mb-3">
<div class="card-header"> @Model.Duration</div>
<div class="card-body">
<h5 class="card-title">
</h5>
<p class="card-text"></p>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
});
</script>

View File

@ -0,0 +1,46 @@
@model CourseModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@inject INopHtmlHelper NopHtml
@{
const string hideInfoBlockAttributeName = "CoursePage.HideInfoBlock";
var hideInfoBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideInfoBlockAttributeName);
const string hideCourseContentBlockAttributeName = "CoursePage.HideCourseContentBlock";
var hideCourseContentBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideCourseContentBlockAttributeName);
NopHtml.AddScriptParts(ResourceLocation.Footer, "~/lib_npm/tinymce/tinymce.min.js", excludeFromBundle: true);
}
<div asp-validation-summary="All"></div>
<input asp-for="Id" type="hidden" />
<section class="content">
<div class="container-fluid">
<div class="form-horizontal">
<nop-cards id="course-cards">
<nop-card asp-name="course-info" asp-icon="fas fa-info" asp-title="@T("SimpleLMS.Course") @T("SimpleLMS.Info")"
asp-hide-block-attribute-name="@hideInfoBlockAttributeName" asp-hide="@hideInfoBlock" asp-advanced="false">
@await Html.PartialAsync("_CreateOrUpdate.Info.cshtml", Model)
</nop-card>
<nop-card asp-name="course-content" asp-icon="fas fa-info" asp-title="@T("SimpleLMS.CourseContent")"
asp-hide-block-attribute-name="@hideCourseContentBlockAttributeName" asp-hide="@hideCourseContentBlock" asp-advanced="false">
@await Html.PartialAsync("_CreateOrUpdate.CourseContent.cshtml", Model)
</nop-card>
</nop-cards>
</div>
</div>
</section>

View File

@ -0,0 +1,66 @@
@model ConfigurationModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@inject INopHtmlHelper NopHtml
@{
const string hideYouTubeBlockAttributeName = "Configuration.HideYouTubeBlock";
var hideYouTubeBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideYouTubeBlockAttributeName);
const string hideVimeoBlockAttributeName = "Configuration.HideVimeoBlock";
var hideVimeoBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideVimeoBlockAttributeName);
const string hideVdoCipherBlockAttributeName = "Configuration.HideVdoCipherBlock";
var hideVdoCipherBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideVdoCipherBlockAttributeName);
//active menu item (system name)
NopHtml.SetActiveMenuItemSystemName("SimpleLMS.configuration");
}
@*<link href="@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/css/jquery-ui-1.10.4.custom.min.css")" rel="stylesheet" type="text/css" />*@
@*<link href="@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/css/simplelms.css")" type="text/css" rel="stylesheet" />*@
@*<script type="text/javascript" src="@Url.Content("~/Plugins/Misc.SimpleLMS/Content/Admin/js/jquery-ui-1.10.4.custom.min.js")"></script>*@
<form asp-controller="Settings" asp-action="Configure" method="post">
<section class="content">
<div class="container-fluid">
<div class="cards-group">
<div class="card card-default">
<div class="card-header">
@T("Plugins.SimpleLMS.Configuration.PageTitle")
</div>
<div class="card-body form-horizontal">
<nop-cards id="course-cards">
@*<nop-card asp-name="YouTube" asp-icon="fas fa-info" asp-title="@T("SimpleLMS.Configuration.Youtube")" asp-hide-block-attribute-name="@hideYouTubeBlockAttributeName" asp-hide="@hideYouTubeBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Youtube.cshtml", Model)</nop-card>*@
<nop-card asp-name="Vimeo" asp-icon="fas fa-info" asp-title="@T("SimpleLMS.Configuration.Vimeo")" asp-hide-block-attribute-name="@hideVimeoBlockAttributeName" asp-hide="@hideVimeoBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Vimeo.cshtml", Model)</nop-card>
@*<nop-card asp-name="VdoCipher" asp-icon="fas fa-info" asp-title="@T("SimpleLMS.Configuration.VdoCipher")" asp-hide-block-attribute-name="@hideVdoCipherBlockAttributeName" asp-hide="@hideVdoCipherBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.VdoCipher.cshtml", Model)</nop-card>*@
</nop-cards>
</div>
</div>
<div class="card card-default">
<div class="card-body">
<div class="form-group row">
<div class="col-md-9 offset-md-3">
<button type="submit" name="save" class="btn btn-primary">@T("Admin.Common.Save")</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</form>

View File

@ -0,0 +1,26 @@

@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@model ConfigurationModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="VdoCipherKey" />
</div>
<div class="col-md-9">
<nop-editor asp-for="VdoCipherKey" asp-required="true" />
<span asp-validation-for="VdoCipherKey"></span>
</div>
</div>
</div>

View File

@ -0,0 +1,44 @@

@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@model ConfigurationModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@{
}
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="VimeoClient" />
</div>
<div class="col-md-9">
<nop-editor asp-for="VimeoClient" asp-required="true" />
<span asp-validation-for="VimeoClient"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="VimeoSecret" />
</div>
<div class="col-md-9">
<nop-editor asp-for="VimeoSecret" asp-required="true" />
<span asp-validation-for="VimeoSecret"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="VimeoAccess" />
</div>
<div class="col-md-9">
<nop-editor asp-for="VimeoAccess" asp-required="true" />
<span asp-validation-for="VimeoAccess"></span>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
@model ConfigurationModel
@using Nop.Plugin.Misc.SimpleLMS.Areas.Admin.Models
@using Nop.Core.Domain.Catalog;
@using Nop.Services
@using Nop.Services.Stores
@{
}
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="YouTubeApiKey" />
</div>
<div class="col-md-9">
<nop-editor asp-for="YouTubeApiKey" asp-required="true" />
<span asp-validation-for="YouTubeApiKey"></span>
</div>
</div>
</div>

View File

@ -0,0 +1,62 @@
@inherits Nop.Web.Framework.Mvc.Razor.NopRazorPage<TModel>
@inject IGenericAttributeService genericAttributeService
@inject IWorkContext workContext
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Nop.Web.Framework
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Mvc.ViewFeatures
@using Microsoft.AspNetCore.Routing
@using Microsoft.Extensions.Primitives
@using Nop.Core
@using Nop.Core.Domain.Common
@using Nop.Core.Events
@using Nop.Core.Infrastructure
@using Nop.Services.Common
@using static Nop.Services.Common.NopLinksDefaults
@using Nop.Web.Areas.Admin.Models.Affiliates
@using Nop.Web.Areas.Admin.Models.Blogs
@using Nop.Web.Areas.Admin.Models.Catalog
@using Nop.Web.Areas.Admin.Models.Cms
@using Nop.Web.Areas.Admin.Models.Common
@using Nop.Web.Areas.Admin.Models.Customers
@using Nop.Web.Areas.Admin.Models.Directory
@using Nop.Web.Areas.Admin.Models.Discounts
@using Nop.Web.Areas.Admin.Models.ExternalAuthentication
@using Nop.Web.Areas.Admin.Models.Forums
@using Nop.Web.Areas.Admin.Models.Home
@using Nop.Web.Areas.Admin.Models.Localization
@using Nop.Web.Areas.Admin.Models.Logging
@using Nop.Web.Areas.Admin.Models.Messages
@using Nop.Web.Areas.Admin.Models.MultiFactorAuthentication
@using Nop.Web.Areas.Admin.Models.News
@using Nop.Web.Areas.Admin.Models.Orders
@using Nop.Web.Areas.Admin.Models.Payments
@using Nop.Web.Areas.Admin.Models.Plugins
@using Nop.Web.Areas.Admin.Models.Plugins.Marketplace
@using Nop.Web.Areas.Admin.Models.Polls
@using Nop.Web.Areas.Admin.Models.Reports
@using Nop.Web.Areas.Admin.Models.Security
@using Nop.Web.Areas.Admin.Models.Settings
@using Nop.Web.Areas.Admin.Models.Shipping
@using Nop.Web.Areas.Admin.Models.ShoppingCart
@using Nop.Web.Areas.Admin.Models.Stores
@using Nop.Web.Areas.Admin.Models.Tasks
@using Nop.Web.Areas.Admin.Models.Tax
@using Nop.Web.Areas.Admin.Models.Templates
@using Nop.Web.Areas.Admin.Models.Topics
@using Nop.Web.Areas.Admin.Models.Vendors
@using Nop.Web.Extensions
@using Nop.Web.Framework
@using Nop.Web.Framework.Menu
@using Nop.Web.Framework.Models
@using Nop.Web.Framework.Events
@using Nop.Web.Framework.Extensions
@using Nop.Web.Framework.Infrastructure
@using Nop.Web.Framework.Models.DataTables
@using Nop.Web.Framework.Security.Captcha
@using Nop.Web.Framework.Security.Honeypot
@using Nop.Web.Framework.Themes
@using Nop.Web.Framework.UI

View File

@ -0,0 +1,3 @@
@{
Layout = "~/Areas/Admin/Views/Shared/_AdminLayout.cshtml";
}

Binary file not shown.

View File

@ -0,0 +1,134 @@

.ajax-loading-block-window {
position: fixed;
top: 50%;
left: 50%;
z-index: 999;
width: 32px;
height: 32px;
margin: -16px 0 0 -16px;
background: url('../images/loading.gif') center no-repeat;
}
.please-wait {
background: url('../images/ajax-loader-small.gif') no-repeat;
padding-left: 20px;
font-size: 14px;
}
.ui-dialog {
max-width: 90%;
border: 1px solid #ddd;
box-shadow: 0 0 2px rgba(0,0,0,0.15);
overflow: hidden;
background-color: #fff;
/*override jQuery UI styles, do not delete doubled properties*/
border-radius: 0;
padding: 0;
font: normal 14px Arial, Helvetica, sans-serif;
}
.ui-dialog:before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.ui-dialog-titlebar {
border-bottom: 1px solid #ddd;
overflow: hidden;
background-color: #eee;
padding: 10px 15px;
/*override jQuery UI styles, do not delete doubled properties*/
border-width: 0 0 1px;
border-radius: 0;
background-image: none;
padding: 10px 15px !important;
font-weight: normal;
cursor: auto !important;
}
.ui-dialog-titlebar > span {
float: left;
font-size: 18px;
color: #444;
/*override jQuery UI styles, do not delete doubled properties*/
margin: 0 !important;
}
.ui-dialog-titlebar button {
position: absolute;
top: 0;
right: 0;
width: 42px;
height: 42px;
border: none;
overflow: hidden;
background: url('../images/close.png') center no-repeat;
font-size: 0;
/*override jQuery UI styles, do not delete doubled properties*/
top: 0 !important;
right: 0 !important;
width: 42px !important;
height: 42px !important;
margin: 0 !important;
border: none !important;
border-radius: 0;
background: url('../images/close.png') center no-repeat !important;
padding: 0 !important;
}
.ui-dialog-titlebar button span {
display: none !important;
}
.ui-dialog-content {
padding: 15px;
line-height: 20px;
/*override jQuery UI styles, do not delete doubled properties*/
background-color: #fff !important;
padding: 15px 15px 20px 15px !important;
color: #777;
}
.ui-dialog-content .page {
min-height: 0;
}
.ui-dialog-content .page-title {
min-height: 0;
margin: 0 0 15px;
padding: 0px 10px 10px 10px;
text-align: center;
}
.ui-dialog-content .page-title h1 {
font-size: 24px;
line-height: 30px;
}
.ui-dialog-content .back-in-stock-subscription-page {
text-align: center;
}
.ui-dialog-content .back-in-stock-subscription-page .tooltip {
margin-bottom: 10px;
}
.ui-dialog-content .back-in-stock-subscription-page .button-1 {
border: none;
background-color: #4ab2f1;
padding: 10px 15px;
font-size: 15px;
color: #fff;
text-transform: uppercase;
}
.ui-dialog-content .back-in-stock-subscription-page .button-1:hover,
.ui-dialog-content .back-in-stock-subscription-page .button-1:focus {
background-color: #248ece;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,208 @@
.hide-overflow-x {
overflow-x: hidden;
}
@media (min-width: 992px) {
.cstm-nav .nav-link {
padding-right: 1rem !important;
padding-left: 1rem !important;
}
}
.course-page .about-course h5 {
margin-bottom: 20px;
}
.course-page .list-group.side-menu {
font-size: 0.85rem;
border-radius: 0;
}
.course-page .side-menu .list-group-item {
padding: 0;
font-size: 0.85rem;
}
.course-page .duration {
line-height: 1.4;
font-size: 12px;
color: #6c757d
}
.side-menu a, .dropdown-btn {
text-decoration: none;
color: #222;
display: block;
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
outline: none;
padding: 8px;
transition: 0.3s;
}
.side-menu a, .dropdown-btn:focus {
outline: 0;
}
.side-menu .list-group-item.side-menu-title {
padding: 10px;
}
.side-menu a:hover, .dropdown-btn:hover {
color: #818181;
}
.active {
color: #818181;
}
.dropdown-container {
display: none;
padding: 8px 0;
transition: 0.3s ease-out;
}
.dropdown-container a:hover, .dropdown-container a:hover.active {
background-color: #D1D7DC;
}
.fa-caret-down {
float: right;
padding-right: 8px;
}
[data-toggle="collapse"] .fa:before {
content: "\f0d7";
}
[data-toggle="collapse"].collapsed .fa:before {
content: "\f0da";
}
.vertical-align {
vertical-align: text-top;
}
.course-lesson {
}
.course-lesson:hover {
background: #eee;
}
.course-lesson-active {
background: #d1d7dc;
}
@keyframes growProgressBar {
0%, 33% {
--pgPercentage: 0;
}
100% {
--pgPercentage: var(--value);
}
}
@property --pgPercentage {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
div[role="progressbar"] {
--size: 3.5rem;
--fg: #369;
--bg: #def;
--pgPercentage: var(--value);
animation: growProgressBar 3s 1 forwards;
width: var(--size);
height: var(--size);
border-radius: 50%;
display: inline-grid;
place-items: center;
background: radial-gradient(closest-side, white 80%, transparent 0 99.9%, white 0), conic-gradient(var(--fg) calc(var(--pgPercentage) * 1%), var(--bg) 0);
font-family: Helvetica, Arial, sans-serif;
font-size: calc(var(--size) / 5);
color: var(--fg);
}
div[role="progressbar"]::before {
counter-reset: percentage var(--value);
content: counter(percentage) '%';
}
#loader {
position: fixed;
width: 200px;
padding: 10px;
left: 45%;
top: 0;
display: none;
background-color: #fff;
z-index: 99999;
text-align: center;
}
.video-wrapper {
width: 100%;
}
.video-container {
overflow: hidden;
position: relative;
width: 100%;
}
.video-container::after {
padding-top: 56.25%;
display: block;
content: '';
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
@media (max-width: 768px) {
#loaddiv {
height: 60vh;
overflow-y: scroll;
}
body {
overflow: hidden;
}
}
@media (max-width: 648px) {
#loaddiv {
height: 50vh;
overflow-y: scroll;
}
body {
overflow: hidden;
}
}
@media (max-width: 480px) {
#loaddiv {
height: 40vh;
overflow-y: scroll;
}
body {
overflow: hidden;
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Mvc.Razor;
using System.Collections.Generic;
using System.Linq;
namespace Nop.Plugin.Misc.SimpleLMS.Infrastructure
{
public class ViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
if (context.AreaName == "Admin")
{
viewLocations = new[] { $"/Plugins/Misc.SimpleLMS/Areas/Admin/Views/{context.ControllerName}/{context.ViewName}.cshtml" }.Concat(viewLocations);
}
//else if (context.AreaName == null && context.ViewName == "Components/CustomerNavigation/Default")
//{
// viewLocations = new[] { $"/Plugins/Misc.SimpleLMS/Views/Shared/Components/CustomCustomerNavigation/Default.cshtml" }.Concat(viewLocations);
//}
else
{
viewLocations = new[] { $"/Plugins/Misc.SimpleLMS/Views/{context.ControllerName}/{context.ViewName}.cshtml"
}.Concat(viewLocations);
}
return viewLocations;
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,220 @@
@model CourseDetail
@using Nop.Core
@using Nop.Core.Domain.Catalog
@inject CatalogSettings catalogSettings
@inject IWorkContext workContext
@{
}
@{
var sectionCount = 1;
var lessonCount = 1;
Layout = "_CourseDetailRoot";
NopHtml.AddTitleParts(Model.Name);
NopHtml.AddCssFileParts("~/lib_npm/bootstrap/css/bootstrap.min.css");
NopHtml.AddCssFileParts("~/lib_npm/@fortawesome/fontawesome-free/css/all.min.css");
NopHtml.AddCssFileParts("~/Plugins/Misc.SimpleLMS/Content/Public/Productstyle.css");
//<link rel="stylesheet" href="~/Plugins/Misc.SimpleLMS/Content/Public/Productstyle.css" />
NopHtml.AddScriptParts(ResourceLocation.Footer, "~/lib_npm/bootstrap/js/bootstrap.min.js");
}
<nav class="navbar navbar-expand-lg navbar-dark cstm-nav bg-dark">
@await Component.InvokeAsync("Logo")
@*<a class="navbar-brand" href="@Url.Action("mycourses","customer")">@T("SimpleLMS.MyCourses")</a>*@
<span class="course-title text-light pl-2"> @Model.Name</span>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown">
<a class="nav-link">
<div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="--value:@(Model.Progress);height:30px;width:30px;"></div>
@Model.CompletedLessons @T("SimpleLMS.Of") @Model.TotalLessons @T("SimpleLMS.Completed")
</a>
</li>
</ul>
</div>
</nav>
<div class="page-wrapper">
<section class="course-page">
<div class="hide-overflow-x">
<div class="row">
<div class="col-md-9 pr-0">
@if (Model.Sections.Count == 0)
{
<div class="m-3 p-3">
<p class="text-danger"> @T("SimpleLMS.NoLessonsAvailable")</p>
</div>
}
<div id="loaddiv">
</div>
</div>
<div class="col-md-3 pl-md-0">
<div class="accordion vh-100 overflow-auto" id="accordion@(Model.Id)">
@foreach (var section in Model.Sections)
{
<div class="card">
<div class="card-header p-0" id="heading@(section.Id)">
<h2 class="p-2 m-0">
<button class="btn btn-link btn-block text-left text-decoration-none text-dark" type="button" data-toggle="collapse" data-target="#collapse@(section.Id)" aria-expanded="true"
aria-controls="collapse@(section.Id)">
<strong> @T("SimpleLMS.Section") @(sectionCount++): @(section.Title)</strong>
<i class="fa fa-caret-down"></i>
<div class="duration">
@(section.CompletedLessons) / @(section.TotalLessons)
@(section.Duration>0? "| " + section.Duration + " " +T("SimpleLMS.Minutes"):"")
</div>
</button>
</h2>
</div>
<div id="collapse@(section.Id)" class="collapse show" aria-labelledby="heading@(section.Id)">
<div class="card-body p-0">
@foreach (var lesson in section.Lessons)
{<div class="course-lesson p-2 pl-3" id="lesson_@lesson.Id">
<div class="form-check">
<label class="vertical-align">
<input type="checkbox" data-course="@(Model.Id)" data-section="@(section.Id)" @(lesson.IsCompleted ? "checked" : "")
data-lesson="@(lesson.Id)" class="form-check-input is-complete" data-iscompleted="@lesson.IsCompleted" />
</label>
<span>
<a class="showCource text-secondary text-decoration-none p-1" style="max-width:100%;" href="javascript:void(0)" data-lesson="@lesson.Id" data-course="@lesson.CourseId">
@(lessonCount++). @lesson.Name
</a>
</span>
<div class="duration ml-2">
<small class="text-muted">
@if (lesson.LessonType == Nop.Plugin.Misc.SimpleLMS.Domains.LessonType.Video)
{
<i class="fa fa-play-circle pr-1" aria-hidden="true"></i>
}
@if (lesson.LessonType == Nop.Plugin.Misc.SimpleLMS.Domains.LessonType.Text)
{
<i class="fa fa-sticky-note"></i>
}
@(lesson.Duration>0? lesson.Duration+ " " + @T("SimpleLMS.Minutes"):"")
</small>
</div>
</div>
</div>
}
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
</section>
</div>
<script type="text/javascript">
$(document).ready(function () {
$(function () {
$(document).ajaxStart(function () {
$("#loader").show();
});
$(document).ajaxStop(function () {
$("#loader").hide();
});
$(document).ajaxError(function () {
$("#loader").hide();
});
});
$(".showCource").on("click", function () {
let lessonId = $(this).data("lesson");
let courseId = $(this).data("course");
loadLesson(lessonId, courseId);
});
$(".is-complete").change(function () {
let isChecked = $(this).is(":checked");
let lessonId = $(this).data("lesson");
let courseId = $(this).data("course");
let sectionId = $(this).data("section");
console.log($(this).data("lesson"));
$.ajax({
type: "POST",
url: '/customer/updatelessonstatus',
data: {
courseId: courseId,
sectionId: sectionId,
lessonId: lessonId,
isCompleted: isChecked
},
headers: {
"RequestVerificationToken":
$('input:hidden[name="__RequestVerificationToken"]').val()
},
success: function (response) {
},
failure: function (response) {
alert(JSON.stringify(response));
$(this).prop("checked", !isChecked);
},
error: function (response) {
alert(JSON.stringify(response));
$(this).prop("checked", !isChecked);
}
});
});
$('.collapse').collapse({
toggle: false
});
loadLesson('@Model.CurrentLesson','@Model.Id');
});
function loadLesson(lessonId, courseId) {
$.get('/Customer/LessonContent?lessonId=' + lessonId + '&courseId=' + courseId, function (data) {
$("#loaddiv").html("");
$("#loaddiv").html(data);
});
$('.course-lesson').removeClass("course-lesson-active");
$('#lesson_' + lessonId).addClass("course-lesson-active");
}
</script>
<script src="https://player.vimeo.com/api/player.js"></script>

View File

@ -0,0 +1,188 @@
@model CourseSearchModel
@{
Layout = "_ColumnsTwo";
NopHtml.AddTitleParts(T("SimpleLMS.MyCourses").Text);
}
@section left
{
@await Component.InvokeAsync("CustomerNavigation", new { selectedTabId = SimpleLMSDefaults.CustomerMyCoursesMenuTab })
}
<div class="page account-page customer-info-page">
<div class="page-title course-title-box flex-container">
<h1 class="d-inline">@T("SimpleLMS.MyCourses")</h1>
<div class="search-box store-search-box course-search-box push" id="course-search-div">
<form id="search-courses-form">
<input type="text" class="search-box-text" id="search-course-name" autocomplete="off" name="SearchCourseName" placeholder="@T("SimpleLMS.SearchCourses")"
aria-label="@T("Search.SearchBox.Text.Label")" />
<button type="submit" id="course-search" class="button-1 search-box-button">@T("Search.Button")</button>
</form>
</div>
</div>
<div class="page-body">
<!--Search-->
<div class="products-container">
<div class="ajax-products-busy"></div>
<div class="products-wrapper" id="course-list-parent">
@*@await Html.PartialAsync("_MyCourseList", Model.CourseOverviewList)*@
</div>
</div>
</div>
</div>
<style type="text/css">
.course-title-box {
display: flex;
}
.course-title-box .push {
margin-left: auto;
}
.course-search-box input.search-box-text {
width: 150px !important;
}
</style>
<script asp-location="Footer">$(document).ready(function () {
$('#search-courses-form').submit(
function (e) {
e.preventDefault();
var _form = $(this).closest("form");
_form.removeData('validator');
_form.removeData('unobtrusiveValidation');
$.validator.unobtrusive.parse(_form);
var isValid = $(_form).validate().form();
if (!isValid) {
return false;
}
$(this).prop('disabled', true);
var searchTerm = $("#course-search-div").find("#search-course-name").val();
var pageNumber = 1;
setQueryString('keyword', searchTerm);
setQueryString('page', pageNumber);
getCourseData(pageNumber, searchTerm);
});
loadCourses();
});
function getCourseData(pageNo, searchTerm) {
var data = {
searchCourseName: searchTerm,
pageNumber: pageNo
}
$.ajax({
type: "POST",
url: '/customer/searchmycourses',
data: JSON.stringify(data),
headers: {
"RequestVerificationToken":
$('input:hidden[name="__RequestVerificationToken"]').val(),
'Content-Type': 'application/json'
},
success: function (response) {
$('#course-list-parent').html(response);
$(this).prop('enabled', false);
},
failure: function (response) {
alert(JSON.stringify(response));
$(this).prop('enabled', false);
},
error: function (response) {
alert(JSON.stringify(response));
$(this).prop('enabled', false);
}
});
}
function addPagerHandlers() {
$('.pager [data-page]').each(function () {
var hrefl = $(this).attr('href');
var url = new URL(hrefl);
var seachText = getQueryString('keyword');
if (!seachText)
seachText = '';
url.searchParams.set("keyword", seachText);
var newUrl = url.href;
$(this).attr("href", newUrl);
});
$('.pager [data-page]').on('click', function (e) {
e.preventDefault();
var seachText = getQueryString('keyword');
if (!seachText)
seachText = '';
setQueryString("page", $(this).data('page'));
getCourseData($(this).data('page'), seachText);
return false;
});
}
function loadCourses() {
var keyword = getQueryString('keyword');
if (!keyword)
keyword = '';
var page = getQueryString('page');
if (!page || isNaN(page))
page = '1';
$("#course-search-div").find("#search-course-name").val(keyword);
getCourseData(page, keyword);
}
function getQueryString(key) {
const url = new URL(window.location.href);
return url.searchParams.get(key);
}
function setQueryString(key, value) {
const url = new URL(window.location.href);
url.searchParams.set(key, value);
window.history.replaceState(null, null, url);
}</script>

View File

@ -0,0 +1,16 @@
@{
Layout = "_Root.Head";
}
@await Component.InvokeAsync("Widget", new { widgetZone = PublicWidgetZones.BodyStartHtmlTagAfter })
@{ await Html.RenderPartialAsync("_Notifications"); }
@{ await Html.RenderPartialAsync("_JavaScriptDisabledWarning"); }
@{ await Html.RenderPartialAsync("_OldInternetExplorerWarning"); }
<div class=" loader-overlay" id="loader">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
@RenderBody()

View File

@ -0,0 +1,38 @@
@model Nop.Plugin.Misc.SimpleLMS.Models.LessonDetail
@using Nop.Core
@using Nop.Core.Domain.Catalog
@using Nop.Plugin.Misc.SimpleLMS.Domains
@inject CatalogSettings catalogSettings
@inject IWorkContext workContext
@{
var simpleLMSSettings = (SimpleLMSSettings)ViewData["simpleLMSSettings"];
}
@if (Model.LessonType == LessonType.Video)
{
<div id="video-content">
@if (Model.Video.VideoType == VideoType.Youtube)
{
<div class="video-container">
<iframe height="400" width="100%" src="https://www.youtube.com/embed/@Model.Video.VideoIdFromProvider?autoplay=1@(!string.IsNullOrEmpty(Model.Video.TimeCode)?"&amp;start="+Model.Video.TimeCode:"")"
title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
</div>
}
@if (Model.Video.VideoType == VideoType.Vimeo)
{
<div style="padding:56.25% 0 0 0;position:relative;">
<iframe src="https://player.vimeo.com/video/@(Model.Video.VideoIdFromProvider+(!string.IsNullOrEmpty(Model.Video.TimeCode)?"#"+Model.Video.TimeCode:"") )?autoplay=1&amp;badge=0&amp;autopause=0&amp;player_id=0&amp;byline=0&amp;title=0@(!string.IsNullOrEmpty(simpleLMSSettings.VimeoClient)?"&amp;app_id="+simpleLMSSettings.VimeoClient:"")'"
frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;"></iframe>
</div>
<script src="https://player.vimeo.com/api/player.js"></script>
}
</div>
}
<div class="p-3 mt-3">
@Html.Raw(Model.LessonContents)
</div>

View File

@ -0,0 +1,99 @@

@model CourseOverviewListModel
@inject CatalogSettings catalogSettings
@inject IWorkContext workContext
@{
}
@if (Model.Courses.Count() > 0)
{
<div class="product-grid">
<div class="item-grid">
@foreach (var course in Model.Courses)
{
<div class="item-box">
<div class="product-item" data-productid="@course.Id">
<div class="picture">
<a href="@Url.Action("CoursesDetails","Customer", new { courseId = course.Id })" title="@course.ParentProductName">
<img alt="@course.ParentProductName" src="@course.ProductMainImage" title="@course.ParentProductName" />
</a>
</div>
<div class="details">
<h2 class="product-title">
<a href="@Url.Action("CoursesDetails","Customer", new { courseId = course.Id })">@course.ParentProductName</a>
</h2>
<div class="progress"></div>
<div class="buttons">
<a href="@Url.Action("CoursesDetails","Customer", new { courseId = course.Id })" class="button-1">@(course.CourseProgress == 0 ? T("SimpleLMS.Start") : T("SimpleLMS.Resume"))</a>
</div>
</div>
</div>
</div>
}
</div>
</div>
var pager = Html.Pager(Model)
.QueryParam("page")
.RenderEmptyParameters(true);
@if (!await pager.IsEmpty())
{
<div class="pager">`
@pager
</div>
<script type="text/javascript">$(document).ready(function () {
addPagerHandlers();
});</script>
}
}
else
{
<div>@T("SimpleLMS.MyCourses.NoCoursesToShow")</div>
}
@*<div class="product-item" data-productid="@Model.Id">
<div class="picture">
<a href="@Url.RouteUrl("Product", new { productid = Model.Id })" title="@Model.DefaultPictureModel.Title">
<img alt="@Model.DefaultPictureModel.AlternateText" src="@Model.DefaultPictureModel.ImageUrl" title="@Model.DefaultPictureModel.Title" />
</a>
</div>
<div class="details">
<h2 class="product-title">
<a href="@Url.RouteUrl("CoursesDetails", new {productid = Model.Id})">@Model.Name</a>
</h2>
@if (catalogSettings.ShowSkuOnCatalogPages && !string.IsNullOrEmpty(Model.Sku))
{
<div class="sku">
@Model.Sku
</div>
}
@if (Model.ReviewOverviewModel.AllowCustomerReviews)
{
var ratingPercent = 0;
if (Model.ReviewOverviewModel.TotalReviews != 0)
{
ratingPercent = ((Model.ReviewOverviewModel.RatingSum*100)/Model.ReviewOverviewModel.TotalReviews)/5;
}
<div class="product-rating-box" title="@string.Format(T("Reviews.TotalReviews").Text, Model.ReviewOverviewModel.TotalReviews)">
<div class="rating">
<div style="width: @(ratingPercent)%">
</div>
</div>
</div>
}
<div class="description">
@Html.Raw(Model.ShortDescription)
</div>
</div>
</div>*@

View File

@ -0,0 +1,51 @@
@inherits Nop.Web.Framework.Mvc.Razor.NopRazorPage<TModel>
@inject INopHtmlHelper NopHtml
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@*we remove the default InputTagHelper to prevent the checkbox duplicating: https://stackoverflow.com/questions/42544961/asp-net-core-custom-input-tag-helper-rendering-duplicate-checkboxes*@
@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.InputTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Nop.Web.Framework
@addTagHelper *, MiniProfiler.AspNetCore.Mvc
@using System.Globalization;
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Mvc.ViewFeatures
@using Microsoft.Extensions.Primitives
@using static Nop.Services.Common.NopLinksDefaults
@using Nop.Web.Components
@using Nop.Web.Extensions
@using Nop.Web.Framework
@using Nop.Web.Framework.Events
@using Nop.Web.Framework.Extensions
@using Nop.Web.Framework.Infrastructure
@using Nop.Web.Framework.Models
@using Nop.Web.Framework.Mvc.Routing
@using Nop.Web.Framework.Security.Captcha
@using Nop.Web.Framework.Security.Honeypot
@using Nop.Web.Framework.Themes
@using Nop.Web.Framework.UI
@using Nop.Web.Models.Blogs
@using Nop.Web.Models.Boards
@using Nop.Web.Models.Catalog
@using Nop.Web.Models.Checkout
@using Nop.Web.Models.Cms
@using Nop.Web.Models.Common
@using Nop.Web.Models.Customer
@using Nop.Web.Models.Media
@using Nop.Web.Models.News
@using Nop.Web.Models.Newsletter
@using Nop.Web.Models.Order
@using Nop.Web.Models.Polls
@using Nop.Web.Models.PrivateMessages
@using Nop.Web.Models.Profile
@using Nop.Web.Models.ShoppingCart
@using Nop.Web.Models.Topics
@using Nop.Web.Models.Vendors
@using Nop.Core
@using Nop.Core.Domain.Catalog
@using Nop.Plugin.Misc.SimpleLMS.Models
@using Nop.Plugin.Misc.SimpleLMS

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,11 @@
{
"Group": "Misc",
"FriendlyName": "SimpleLMS",
"SystemName": "Misc.SimpleLMS",
"Version": "1.00",
"SupportedVersions": [ "4.50" ],
"Author": "www.slyko.tech",
"DisplayOrder": 1,
"FileName": "Nop.Plugin.Misc.SimpleLMS.dll",
"Description": "SimpleLMS is a learning management system plugin for NopCommerce."
}