JS/CSS updates

- Add tailwind css for layout - remove bootstrap
- Add primevue components (table)
- Update to Vue 3
- Add quasar framework (use q-list)
- Replace moment js (deprecated) with luxon
- Update fullcalendar
pull/3890/head
Julio Montoya 5 years ago
parent 9ad21d561b
commit c2cc7f4548
  1. 5
      .eslintrc.json
  2. 16
      .postcssrc.js
  3. 96
      assets/css/app.scss
  4. 65
      assets/css/bootstrap.scss
  5. 15
      assets/js/app.js
  6. 626
      assets/vue/App.vue
  7. 94
      assets/vue/components/ActionCell.vue
  8. 12
      assets/vue/components/Breadcrumb.vue
  9. 76
      assets/vue/components/ConfirmDelete.vue
  10. 80
      assets/vue/components/Toolbar.vue
  11. 8
      assets/vue/components/course/Form.vue
  12. 10
      assets/vue/components/coursecategory/Form.vue
  13. 27
      assets/vue/components/documents/Form.vue
  14. 49
      assets/vue/components/documents/FormNewDocument.vue
  15. 35
      assets/vue/components/documents/FormUpload.vue
  16. 17
      assets/vue/components/documents/ResourceLinkForm.vue
  17. 27
      assets/vue/components/layout/DashboardLayout.vue
  18. 6
      assets/vue/components/layout/EmptyLayout.vue
  19. 45
      assets/vue/components/layout/Footer.vue
  20. 141
      assets/vue/components/layout/Header.vue
  21. 108
      assets/vue/components/layout/Sidebar.vue
  22. 207
      assets/vue/components/sidebar/Header.vue
  23. 207
      assets/vue/components/sidebar/Sidebar.vue
  24. 11
      assets/vue/hooks/useSidebar.ts
  25. 22
      assets/vue/hooks/useState.js
  26. 8
      assets/vue/i18n.js
  27. 13
      assets/vue/layouts/Catalog.vue
  28. 13
      assets/vue/layouts/MyCourses.vue
  29. 164
      assets/vue/main.js
  30. 6
      assets/vue/mixins/CreateMixin.js
  31. 76
      assets/vue/mixins/ListMixin.js
  32. 53
      assets/vue/mixins/NotificationMixin.js
  33. 11
      assets/vue/mixins/UploadMixin.js
  34. 23
      assets/vue/pages/Index.vue
  35. 19
      assets/vue/quasar-user-options.js
  36. 10
      assets/vue/router/course.js
  37. 10
      assets/vue/router/coursecategory.js
  38. 16
      assets/vue/router/documents.js
  39. 72
      assets/vue/router/index.js
  40. 9
      assets/vue/services/api.js
  41. 7
      assets/vue/store/index.js
  42. 53
      assets/vue/store/modules/crud.js
  43. 7
      assets/vue/styles/quasar.sass
  44. 15
      assets/vue/styles/quasar.variables.sass
  45. 4
      assets/vue/utils/dates.js
  46. 74
      assets/vue/utils/fetch.js
  47. 24
      assets/vue/views/Home.vue
  48. 191
      assets/vue/views/Login.vue
  49. 274
      assets/vue/views/course/Catalog.vue
  50. 72
      assets/vue/views/course/CatalogSession.vue
  51. 75
      assets/vue/views/course/Home.vue
  52. 517
      assets/vue/views/documents/List.vue
  53. 30
      assets/vue/views/documents/Show.vue
  54. 68
      assets/vue/views/documents/Upload.vue
  55. 66
      assets/vue/views/user/courses/CourseCard.vue
  56. 55
      assets/vue/views/user/courses/CourseCardList.vue
  57. 27
      assets/vue/views/user/courses/List.vue
  58. 47
      assets/vue/views/user/profile/Home.vue
  59. 9
      assets/vue/views/user/sessions/List.vue
  60. 61
      assets/vue/views/user/sessions/SessionCard.vue
  61. 53
      assets/vue/views/user/sessions/SessionCardList.vue
  62. 98
      package.json
  63. 106
      public/main/install/index.php
  64. 64
      public/main/template/default/admin/index.html.twig
  65. 67
      public/main/template/default/agenda/month.html.twig
  66. 23
      src/CoreBundle/EventListener/TwigListener.php
  67. 20
      tailwind.config.js
  68. 10
      tsconfig.json
  69. 75
      webpack.config.js
  70. 7621
      yarn.lock

@ -1,7 +1,8 @@
{
"root": true,
"parser": "vue-eslint-parser",
"parserOptions": {
"ecmaVersion": 9,
"ecmaVersion": 2020,
"sourceType": "module"
//"parser": "babel-eslint"
},
@ -12,6 +13,6 @@
},
"extends": [
"eslint:recommended",
"plugin:vue/recommended"
"plugin:vue/vue3-recommended"
]
}

@ -0,0 +1,16 @@
const tailwindcss = require('tailwindcss');
const plugins = [
tailwindcss,
require('autoprefixer'),
]
if (process.env.QUASAR_RTL) {
plugins.push(
require('postcss-rtl')({})
)
}
module.exports = {
plugins
}

@ -1,15 +1,101 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import 'bootstrap.scss';
// Disable bootstrap
//@import 'bootstrap.scss';
// undo
@import "~@fortawesome/fontawesome-free/css/all.css";
@import '~cropper/dist/cropper.css';
@import '~flag-icon-css/sass/flag-icon.scss';
@import "~select2/dist/css/select2.css";
@import "~bootstrap-daterangepicker/daterangepicker.css";
//@import "~bootstrap-select/sass/bootstrap-select";
//@import '~pretty-checkbox/src/pretty-checkbox.scss';
//@import '~jquery-ui/themes/base/all.css';
@import "scss/index.scss";
@import "~bootstrap-select/sass/bootstrap-select";
@import '~pretty-checkbox/src/pretty-checkbox.scss';
@import '~jquery-ui/themes/base/all.css';
//@import "scss/index.scss";
@layer components {
//ripple
.btn {
//@apply inline-block text-black px-4 py-2 text-xs font-medium leading-6 text-center bg-gray-100 uppercase transition rounded shadow hover:shadow-lg focus:outline-none;
//disabled:cursor-not-allowed
//@apply block px-4 py-2 transition duration-100 ease-in-out focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none focus:ring-opacity-50 disabled:opacity-50 ;
@apply inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500;
}
.btn-primary {
@apply text-white bg-blue-700 hover:bg-blue-800;
}
.btn > .fa {
@apply -ml-1 mr-1 h-4 w-5 fill-current ;
}
.help-block {
@apply mt-2 text-xs text-gray-500;
}
.has-error .ch-form-label {
@apply h-8 text-red-500;
}
.has-error .help-block {
@apply text-red-500 italic text-xs;
}
.has-error .ch-form-control {
@apply border-red-500;
}
table {
@apply min-w-full divide-y divide-gray-200;
}
table thead {
@apply bg-gray-50;
}
table tbody {
@apply bg-white divide-y divide-gray-200;
}
//.q-card {
// @apply max-w-sm rounded bg-white overflow-hidden shadow-lg mt-6;
//}
//.ch-form-control {
// @apply appearance-none block w-full text-gray-700 border-blue-500 rounded py-3 px-4 mb-3 leading-tight;
//}
//
////
//.ch-form-label {
// @apply text-gray-500 font-bold;
//}
.p-component {
font-size: 13px;
}
.p-datatable .p-datatable-tbody > tr > td {
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
}
//@import "~jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css";
//

@ -1,3 +1,62 @@
@import '~bootstrap/scss/bootstrap';
@import '~bootstrap-vue/src/index.scss';
//
////import 'primevue/resources/themes/saga-blue/theme.css';
////import 'primevue/resources/primevue.min.css';
////@import '~bootstrap/scss/bootstrap';
//
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/utilities";
//
//// Layout & components
////@import "~bootstrap/scss/root";
////@import "~bootstrap/scss/reboot";
////@import "~bootstrap/scss/type";
////@import "~bootstrap/scss/images";
////@import "~bootstrap/scss/containers";
////@import "~bootstrap/scss/grid";
//@import "~bootstrap/scss/tables";
////@import "~bootstrap/scss/forms";
////@import "~bootstrap/scss/buttons";
////@import "~bootstrap/scss/transitions";
////@import "~bootstrap/scss/dropdown";
//@import "~bootstrap/scss/button-group";
@import "~bootstrap/scss/nav";
////@import "~bootstrap/scss/navbar";
//@import "~bootstrap/scss/card";
//@import "~bootstrap/scss/accordion";
//@import "~bootstrap/scss/breadcrumb";
//@import "~bootstrap/scss/pagination";
//@import "~bootstrap/scss/badge";
//@import "~bootstrap/scss/alert";
//@import "~bootstrap/scss/progress";
@import "~bootstrap/scss/list-group";
//@import "~bootstrap/scss/close";
//@import "~bootstrap/scss/toasts";
//@import "~bootstrap/scss/modal";
//@import "~bootstrap/scss/tooltip";
//@import "~bootstrap/scss/popover";
//@import "~bootstrap/scss/carousel";
//@import "~bootstrap/scss/spinners";
//@import "~bootstrap/scss/offcanvas";
//
//
////@import '~mdb-ui-kit/css/mdb.min.css';
////@import 'node_modules/mdb-ui-kit/src/scss/mdb.core';
////@import 'node_modules/mdb-ui-kit/src/scss/mdb.free';
//
////[class*="q-"].row {
//// margin-left: unset;
//// margin-right: unset;
////}
//
//header .row>* {
// width: auto !important;
// --bs-gutter-x: 0rem !important;
// --bs-gutter-y: 0rem !important;
//}
//
//.q-menu .row {
// --bs-gutter-x: 0rem !important;
// --bs-gutter-y: 0rem !important;
//}

@ -14,12 +14,15 @@ global.$ = global.jQuery = $
Routing.setRoutingData(routes);
const locale = document.querySelector('html').lang;
const moment = require('moment');
global.moment = moment;
require('select2/dist/js/select2.full.min');
require('flatpickr');
// moment
const { DateTime } = require("luxon");
window.luxon = global.luxon = DateTime;
import 'select2/dist/js/select2.full.min';
import 'select2/dist/css/select2.min.css';
//require('flatpickr');
//import('bootstrap-vue');
import('bootstrap');
//import('bootstrap');
require('webpack-jquery-ui');
require('webpack-jquery-ui/css');
@ -29,7 +32,7 @@ require('webpack-jquery-ui/css');
// window.frameReady = frameReady;
require('./vendor');
import('./main');
import './main';
require('bootstrap-daterangepicker');
import('qtip2');
//require('bootstrap-daterangepicker/daterangepicker.js');

@ -1,73 +1,414 @@
<template>
<div class="d-flex flex-column h-100">
<!-- <transition-->
<!-- name="fade"-->
<!-- mode="out-in"-->
<!-- appear-->
<!-- >-->
<Header/>
<!-- </transition>-->
<Sidebar/>
<main
role="main"
class="flex-shrink-0"
>
<b-container fluid>
<b-row>
<b-col cols="12">
<Breadcrumb :legacy="breadcrumb"/>
<!-- <snackbar />-->
<router-view/>
<!-- <div class="lang-dropdown">-->
<!-- <select v-model="$i18n.locale">-->
<!-- <option-->
<!-- v-for="(lang, i) in languageArray"-->
<!-- :key="`lang${i}`"-->
<!-- :value="lang"-->
<!-- >-->
<!-- {{ lang }}-->
<!-- </option>-->
<!-- </select>-->
<!-- </div>-->
<div
id="legacy_content"
v-html="legacy_content"
/>
</b-col>
</b-row>
</b-container>
</main>
<Footer/>
<component :is="layout">
<router-view />
<div id="legacy_content"
v-html="legacyContent"
/>
</component>
<div v-if="false" class="antialiased text-gray-900 bg-white">
<div class="flex h-screen overflow-y-hidden bg-white">
<!-- Sidebar -->
<Sidebar />
<div class="flex flex-col flex-1 h-full overflow-hidden py-70">
<!-- Navbar -->
<header class="flex-shrink-0 border-b">
<Navbar />
</header>
<!-- Main content -->
<main class="flex-1 max-h-full p-5 overflow-hidden overflow-y-scroll">
<router-view />
<div id="legacy_content"
v-html="legacyContent"
/>
</main>
<!-- Main footer -->
<footer class="flex items-center justify-between flex-shrink-0 p-4 border-t max-h-14">
<div>Chamilo 2020</div>
<div class="text-sm">
Made by
<a
class="text-blue-400 underline"
href="https://github.com/Kamona-WD"
target="_blank"
rel="noopener noreferrer"
>Ahmed Kamel</a
>
</div>
<div>
<!-- Github svg -->
<a
href="https://github.com/Kamona-WD/starter-dashboard-layout-vue"
target="_blank"
class="flex items-center space-x-1"
>
<svg class="w-6 h-6 text-gray-400" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
></path>
</svg>
<span class="hidden text-sm md:block">View on Github</span>
</a>
</div>
</footer>
</div>
<!-- Setting panel button -->
<div class="fixed right-0 transform rotate-90 translate-x-8 top-1/2">
<Button @click="isSettingsPanelOpen = true" class="text-sm font-medium text-white uppercase bg-gray-600">
Settings
</Button>
</div>
<SettingsPanel :show="isSettingsPanelOpen" @close="isSettingsPanelOpen = false" />
<SearchPanel left :show="isSearchPanelOpen" @close="isSearchPanelOpen = false" />
<NotificationsPanel left :show="isNotificationsPanelOpen" @close="isNotificationsPanelOpen = false" />
</div>
</div>
<!-- Component End -->
<!-- <q-layout view="hHh LpR lff" class="bg-grey-1">-->
<!-- <q-header bordered class="bg-white text-grey-8" height-hint="64">-->
<!-- <q-toolbar class="GNL__toolbar">-->
<!-- <q-btn-->
<!-- flat-->
<!-- dense-->
<!-- round-->
<!-- @click="leftDrawerOpen = !leftDrawerOpen"-->
<!-- aria-label="Menu"-->
<!-- icon="menu"-->
<!-- class="q-mr-sm"-->
<!-- />-->
<!-- <q-toolbar-title v-if="$q.screen.gt.xs" shrink class="row items-center no-wrap">-->
<!-- <a href="/">-->
<!-- <img style="width:200px" src="/build/css/themes/chamilo/images/header-logo.png" />-->
<!-- </a>-->
<!-- &lt;!&ndash; <span class="q-ml-sm">News</span>&ndash;&gt;-->
<!-- </q-toolbar-title>-->
<!--&lt;!&ndash; <q-select&ndash;&gt;-->
<!--&lt;!&ndash; v-if="isAuthenticated"&ndash;&gt;-->
<!--&lt;!&ndash; ref="search" dense use-input hide-selected&ndash;&gt;-->
<!--&lt;!&ndash; class="GL__toolbar-select"&ndash;&gt;-->
<!--&lt;!&ndash; color="black"&ndash;&gt;-->
<!--&lt;!&ndash; :stack-label="false"&ndash;&gt;-->
<!--&lt;!&ndash; label="Search or jump to..."&ndash;&gt;-->
<!--&lt;!&ndash; v-model="text"&ndash;&gt;-->
<!--&lt;!&ndash; :options="filteredOptions"&ndash;&gt;-->
<!--&lt;!&ndash; @filter="filter"&ndash;&gt;-->
<!--&lt;!&ndash; style="width: 300px"&ndash;&gt;-->
<!--&lt;!&ndash; >&ndash;&gt;-->
<!--&lt;!&ndash; <template v-slot:append>&ndash;&gt;-->
<!--&lt;!&ndash; <img src="https://cdn.quasar.dev/img/layout-gallery/img-github-search-key-slash.svg">&ndash;&gt;-->
<!--&lt;!&ndash; </template>&ndash;&gt;-->
<!--&lt;!&ndash; <template v-slot:no-option>&ndash;&gt;-->
<!--&lt;!&ndash; <q-item>&ndash;&gt;-->
<!--&lt;!&ndash; <q-item-section>&ndash;&gt;-->
<!--&lt;!&ndash; <div class="text-center">&ndash;&gt;-->
<!--&lt;!&ndash; <q-spinner-pie&ndash;&gt;-->
<!--&lt;!&ndash; color="grey-5"&ndash;&gt;-->
<!--&lt;!&ndash; size="24px"&ndash;&gt;-->
<!--&lt;!&ndash; />&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;-->
<!--&lt;!&ndash; </q-item-section>&ndash;&gt;-->
<!--&lt;!&ndash; </q-item>&ndash;&gt;-->
<!--&lt;!&ndash; </template>&ndash;&gt;-->
<!--&lt;!&ndash; <template v-slot:option="scope">&ndash;&gt;-->
<!--&lt;!&ndash; <q-item&ndash;&gt;-->
<!--&lt;!&ndash; v-bind="scope.itemProps"&ndash;&gt;-->
<!--&lt;!&ndash; v-on="scope.itemEvents"&ndash;&gt;-->
<!--&lt;!&ndash; class="GL__select-GL__menu-link"&ndash;&gt;-->
<!--&lt;!&ndash; >&ndash;&gt;-->
<!--&lt;!&ndash; <q-item-section side>&ndash;&gt;-->
<!--&lt;!&ndash; <q-icon name="collections_bookmark" />&ndash;&gt;-->
<!--&lt;!&ndash; </q-item-section>&ndash;&gt;-->
<!--&lt;!&ndash; <q-item-section>&ndash;&gt;-->
<!--&lt;!&ndash; <q-item-label v-html="scope.opt.label" />&ndash;&gt;-->
<!--&lt;!&ndash; </q-item-section>&ndash;&gt;-->
<!--&lt;!&ndash; <q-item-section side :class="{ 'default-type': !scope.opt.type }">&ndash;&gt;-->
<!--&lt;!&ndash; <q-btn outline dense no-caps text-color="blue-grey-5" size="12px" class="bg-grey-1 q-px-sm">&ndash;&gt;-->
<!--&lt;!&ndash; {{ scope.opt.type || 'Jump to' }}&ndash;&gt;-->
<!--&lt;!&ndash; <q-icon name="subdirectory_arrow_left" size="14px" />&ndash;&gt;-->
<!--&lt;!&ndash; </q-btn>&ndash;&gt;-->
<!--&lt;!&ndash; </q-item-section>&ndash;&gt;-->
<!--&lt;!&ndash; </q-item>&ndash;&gt;-->
<!--&lt;!&ndash; </template>&ndash;&gt;-->
<!--&lt;!&ndash; </q-select>&ndash;&gt;-->
<!-- <q-space />-->
<!-- <div v-if="isAuthenticated" class="GPLAY__toolbar-input-container row no-wrap">-->
<!-- <q-tabs align="center" dense inline-label>-->
<!-- <q-route-tab icon="home" to="/" label="Home" />-->
<!-- <q-route-tab to="/courses" label="My courses" />-->
<!-- <q-route-tab to="/main/calendar/agenda_js.php?type=personal" label="Agenda" />-->
<!-- </q-tabs>-->
<!-- </div>-->
<!-- <q-space />-->
<!-- <div class="q-gutter-sm row items-center no-wrap">-->
<!-- &lt;!&ndash; <q-btn v-if="$q.screen.gt.sm" round dense flat color="text-grey-7" icon="apps">&ndash;&gt;-->
<!-- &lt;!&ndash; <q-tooltip>Google Apps</q-tooltip>&ndash;&gt;-->
<!-- &lt;!&ndash; </q-btn>&ndash;&gt;-->
<!-- <q-btn v-if="isAuthenticated" round dense flat color="grey-8" icon="notifications">-->
<!-- <q-badge color="red" text-color="white" floating>-->
<!-- 2-->
<!-- </q-badge>-->
<!-- <q-tooltip>Notifications</q-tooltip>-->
<!-- </q-btn>-->
<!-- <q-btn v-if="!isAuthenticated"-->
<!-- :to="{ name: 'Login'}"-->
<!-- color="primary"-->
<!-- icon="mail"-->
<!-- label="Login"-->
<!-- />-->
<!-- &lt;!&ndash; <Button v-if="!isAuthenticated" :to="{ name: 'Login' }" label="Login" class="p-button-sm" />&ndash;&gt;-->
<!-- &lt;!&ndash; <q-btn v-if="isAuthenticated" round flat>&ndash;&gt;-->
<!-- &lt;!&ndash; <q-avatar size="26px">&ndash;&gt;-->
<!-- &lt;!&ndash; <img src="https://cdn.quasar.dev/img/boy-avatar.png">&ndash;&gt;-->
<!-- &lt;!&ndash; </q-avatar>&ndash;&gt;-->
<!-- &lt;!&ndash; <q-tooltip>Account</q-tooltip>&ndash;&gt;-->
<!-- &lt;!&ndash; </q-btn>&ndash;&gt;-->
<!-- <q-btn v-if="isAuthenticated" dense flat no-wrap>-->
<!-- <q-avatar size="26px">-->
<!-- <img :src="userAvatar + '?w=80&h=80&fit=crop'" />-->
<!-- &lt;!&ndash; <q-icon name="person" ></q-icon>&ndash;&gt;-->
<!-- </q-avatar>-->
<!-- <q-icon name="arrow_drop_down" size="16px" />-->
<!-- <q-menu auto-close>-->
<!-- <q-list dense>-->
<!-- <q-item class="GL__menu-link-signed-in">-->
<!-- <q-item-section>-->
<!-- <div>Signed in as <strong>{{ currentUser.username }}</strong></div>-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<!-- &lt;!&ndash; <q-separator />&ndash;&gt;-->
<!-- &lt;!&ndash; <q-item clickable class="GL__menu-link-status">&ndash;&gt;-->
<!-- &lt;!&ndash; <q-item-section>&ndash;&gt;-->
<!-- &lt;!&ndash; <div>&ndash;&gt;-->
<!-- &lt;!&ndash; <q-icon name="tag_faces" color="blue-9" size="18px" />&ndash;&gt;-->
<!-- &lt;!&ndash; Set your status&ndash;&gt;-->
<!-- &lt;!&ndash; </div>&ndash;&gt;-->
<!-- &lt;!&ndash; </q-item-section>&ndash;&gt;-->
<!-- &lt;!&ndash; </q-item>&ndash;&gt;-->
<!-- <q-separator />-->
<!-- <q-item replace :to="'/main/messages/index.php'" clickable class="">-->
<!-- <q-item-section>Inbox</q-item-section>-->
<!-- </q-item>-->
<!-- <q-item href="/account/home" tag="a" class="">-->
<!-- <q-item-section>-->
<!-- Your profile-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<!-- <q-item href="/account/edit" tag="a" class="">-->
<!-- <q-item-section>Settings</q-item-section>-->
<!-- </q-item>-->
<!-- <q-item href="/logout" tag="a" clickable class="">-->
<!-- <q-item-section>-->
<!-- Sign out-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<!-- </q-list>-->
<!-- </q-menu>-->
<!-- </q-btn>-->
<!-- </div>-->
<!-- </q-toolbar>-->
<!-- </q-header>-->
<!-- <q-drawer-->
<!-- v-model="leftDrawerOpen"-->
<!-- show-if-above-->
<!-- bordered-->
<!-- content-class="bg-white"-->
<!-- :width="250"-->
<!-- >-->
<!-- <q-scroll-area class="fit">-->
<!-- <q-list v-if="isAuthenticated" padding class="text-grey-8">-->
<!-- <q-item class="GNL__drawer-item" v-ripple v-for="link in links1" :key="link.text" :to="link.url" clickable>-->
<!-- <q-item-section avatar>-->
<!-- <q-icon :name="link.icon" />-->
<!-- </q-item-section>-->
<!-- <q-item-section>-->
<!-- <q-item-label>{{ link.text }}</q-item-label>-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<!-- <q-separator inset class="q-my-sm" />-->
<!-- <span v-if="isAdmin">-->
<!-- <q-item class="GNL__drawer-item" v-ripple v-for="link in links2" :key="link.text" :to="link.url" clickable>-->
<!-- <q-item-section avatar>-->
<!-- <q-icon :name="link.icon" />-->
<!-- </q-item-section>-->
<!-- <q-item-section>-->
<!-- <q-item-label>{{ link.text }}</q-item-label>-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<!-- </span>-->
<!-- <q-separator inset class="q-my-sm" />-->
<!-- <q-item class="GNL__drawer-item" v-ripple v-for="link in links3" :key="link.text" clickable>-->
<!-- <q-item-section>-->
<!-- <q-item-label>{{ link.text }} <q-icon v-if="link.icon" :name="link.icon" /></q-item-label>-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<!-- <div class="q-mt-md">-->
<!-- <div class="flex flex-center q-gutter-xs">-->
<!-- <a class="GNL__drawer-footer-link" href="javascript:void(0)" aria-label="About">Chamilo</a>-->
<!-- </div>-->
<!-- </div>-->
<!-- </q-list>-->
<!-- <q-list v-else padding class="text-grey-8">-->
<!-- <q-item class="GNL__drawer-item" v-ripple v-for="link in linksAnon" :key="link.text" :to="link.url" clickable>-->
<!-- <q-item-section avatar>-->
<!-- <q-icon :name="link.icon" />-->
<!-- </q-item-section>-->
<!-- <q-item-section>-->
<!-- <q-item-label>{{ link.text }}</q-item-label>-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<!-- </q-list>-->
<!-- </q-scroll-area>-->
<!-- </q-drawer>-->
<!-- <q-page-container>-->
<!-- <q-page-->
<!-- class="q-layout-padding"-->
<!-- >-->
<!-- <router-view />-->
<!-- <div id="legacy_content"-->
<!-- v-html="legacyContent"-->
<!-- />-->
<!-- </q-page>-->
<!-- </q-page-container>-->
<!-- </q-layout>-->
</template>
<style>
</style>
<script>
<script>
import {mapGetters} from 'vuex';
import NotificationMixin from './mixins/NotificationMixin';
import Breadcrumb from './components/Breadcrumb';
import Breadcrumb from './components/Breadcrumb.vue';
import axios from "axios";
import Header from "./components/layout/Header";
import Sidebar from "./components/layout/Sidebar";
import Footer from "./components/layout/Footer";
import { onMounted, onUnmounted, ref, computed } from 'vue';
import isEmpty from 'lodash/isEmpty';
//import Header from "./components/layout/Header.vue";
//import Sidebar from "./components/layout/Sidebar.vue";
//import Footer from "./components/layout/Footer.vue";
import { fasGlobeAmericas, fasFlask } from '@quasar/extras/fontawesome-v5'
import { useRouter } from 'vue-router'
import useState from './hooks/useState'
import Sidebar from './components/sidebar/Sidebar.vue'
import Navbar from './components/navbar/Navbar.vue'
import SettingsPanel from './components/panels/SettingsPanel.vue'
import SearchPanel from './components/panels/SearchPanel.vue'
import NotificationsPanel from './components/panels/NotificationsPanel.vue'
import Button from './components/global/Button.vue'
const defaultLayout = "Dashboard";
export default {
name: "App",
components: {
Header,
Navbar,
Sidebar,
Footer,
SettingsPanel,
SearchPanel,
NotificationsPanel,
Button,
Breadcrumb,
},
setup () {
const { isSidebarOpen, isSettingsPanelOpen, isSearchPanelOpen, isNotificationsPanelOpen } = useState()
isSidebarOpen.value = true;
const { currentRoute } = useRouter();
const layout = computed(
() => `${currentRoute.value.meta.layout || defaultLayout}Layout`
);
/*const checkScreen = () => {
if (window.innerWidth <= 1024) {
isSidebarOpen.value = false
}
}*/
onMounted(() => {
//window.addEventListener('resize', checkScreen)
})
onUnmounted(() => {
//window.removeEventListener('resize', checkScreen)
})
const leftDrawerOpen = ref(false)
const rightDrawerOpen = ref(false)
return {
layout,
isSidebarOpen,
isSettingsPanelOpen,
isSearchPanelOpen,
isNotificationsPanelOpen,
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
},
rightDrawerOpen,
toggleRightDrawer () {
rightDrawerOpen.value = !rightDrawerOpen.value
}
}
},
mixins: [NotificationMixin],
data: () => ({
user: {},
userAvatar: '',
firstTime: false,
search: '',
showAdvanced: false,
showDateOptions: false,
exactPhrase: '',
hasWords: '',
excludeWords: '',
byWebsite: '',
byDate: 'Any time',
moved: true,
links1: [
// { icon: 'person', url: '/courses', text: 'My courses' },
// { icon: 'star_border', url: '/sessions', text: 'Sessions' },
//{ icon: 'star_border', url: '/calendar', text: 'My calendar' },
{ icon: 'compass', url: '/catalog', text: 'Explore' },
// { icon: 'star_border', url: '/news', text: 'News' },
],
links2: [
{ icon: 'users', url: '/main/admin/user_list.php', text: 'Users' },
{ icon: 'book', url: '/main/admin/course_list.php', text: 'Courses' },
{ icon: 'book-open', url: '/main/session/session_list.php', text: 'Sessions' },
//{ icon: fasFlask, url: '/main/admin/index.php', text: 'Administration' },
{ icon: 'cogs', url: '/main/admin/index.php', text: 'Administration' },
],
links3: [
//{ icon: '', text: 'Settings' },
// { icon: 'open_in_new', text: 'open in new' },
],
linksAnon: [
{ icon: 'home', url: '/', text: 'Home' },
],
drawer: true,
breadcrumb: [],
languageArray: ['en', 'fr'],
@ -75,85 +416,145 @@ export default {
['Courses', 'mdi-book', 'CourseList'],
['Courses category', 'mdi-book', 'CourseCategoryList'],
],
cruds: [
/*cruds: [
['Create', 'add'],
['Read', 'insert_drive_file'],
['Update', 'update'],
['Delete', 'delete'],
],
legacy_content: null,
],*/
legacyContent: '',
}),
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'currentUser': 'security/getUser',
}),
showMenu() {
return this.$route.path !== '/login';
}
},
watch: {
$route() {
this.$data.legacy_content = '';
if (document.querySelector("#sectionMainContent")) {
document.querySelector("#sectionMainContent").remove();
console.log('App.vue watch $route');
if ('Login' === this.$route.name) {
this.leftDrawerOpen = false;
} else {
this.leftDrawerOpen = true;
}
console.log(this.$route.name);
//let content = document.getElementById("sectionMainContent");
this.legacyContent = '';
/*if (content && false === this.contentLoaded) {
console.log('updated ok ');
content.style.display = 'block';
this.legacyContent = content.outerHTML;
if (document.querySelector("#sectionMainContent")) {
console.log('remove sectionMainContent ');
document.querySelector("#sectionMainContent").remove();
}
}*/
let url = window.location.href;
var n = url.indexOf("main/");
if (n > 0) {
axios.get(url, {
params: {
from_vue: 1
if (this.firstTime) {
let content = document.querySelector("#sectionMainContent");
if (content) {
content.style.display = 'block';
document.querySelector("#sectionMainContent").remove();
this.legacyContent = content.outerHTML;
}
} else {
if (document.querySelector("#sectionMainContent")) {
document.querySelector("#sectionMainContent").remove();
console.log('remove');
}
})
.then((response) => {
// handle success
this.$data.legacy_content = response.data;
}).catch(function (error) {
if (error.response) {
// Request made and server responded
//this.showMessage(error.response.data.detail);
/*console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);*/
} else if (error.request) {
// The request was made but no response was received
//console.log(error.request);
} else {
//console.log('Error', error.message);
}
window.location.replace(url);
/*axios.get(url, {
params: {
from_vue: 1
},
})
.then((response) => {
console.log('updated page using axios');
this.legacyContent = response.data;
}).catch(function (error) {
if (error.response) {
// Request made and server responded
console.log(error.response.status);
console.log(error.response.data);
} else if (error.request) {
// The request was made but no response was received
console.log(error.request);
} else {
console.log('Error', error.message);
}
});*/
}
} else {
if (this.firstTime) {
let content = document.querySelector("#sectionMainContent");
if (content) {
content.style.display = 'block';
document.querySelector("#sectionMainContent").remove();
this.legacyContent = content.outerHTML;
}
} else {
let content = document.querySelector("#sectionMainContent");
if (content) {
document.querySelector("#sectionMainContent").remove();
}
this.legacyContent = '';
}
}
this.firstTime = false;
},
legacy_content: {
legacyContent: {
handler: function () {
},
immediate: true
},
},
mounted() {
let legacyContent = document.querySelector("#sectionMainContent");
if (legacyContent) {
document.querySelector("#sectionMainContent").remove();
legacyContent.style.display = 'block';
this.$data.legacy_content = legacyContent.outerHTML;
}
},
created() {
this.$data.legacy_content = '';
let isAuthenticated = false;
if (this.$parent.$el.attributes["data-is-authenticated"].value) {
isAuthenticated = JSON.parse(this.$parent.$el.attributes["data-is-authenticated"].value);
console.log('created');
// @todo
if (this.isAuthenticated) {
this.links1.unshift({icon: 'user-circle', url: '/account/profile', text: this.currentUser.username});
}
let app = document.getElementById('app');
this.legacyContent = '';
console.log('updated empty created');
let isAuthenticated = false;
/*if (app && app.attributes['data-is-authenticated'].value) {
isAuthenticated = JSON.parse(app.attributes['data-is-authenticated'].value);
}*/
console.log('isAuthenticated');
console.log(isAuthenticated);
console.log(window.user);
let user = null;
if (this.$parent.$el.attributes["data-user-json"].value) {
user = JSON.parse(this.$parent.$el.attributes["data-user-json"].value);
if (!isEmpty(window.user)) {
// console.log('is logged in as ' + window.user.username);
this.user = window.user;
this.userAvatar = window.userAvatar;
isAuthenticated = true;
}
let payload = {isAuthenticated: isAuthenticated, user: user};
/*if (app && app.attributes["data-user-json"].value) {
this.user = JSON.parse(app.attributes["data-user-json"].value);
this.userAvatar = app.attributes["data-user-avatar"].value;
}*/
console.log(this.user);
let payload = {isAuthenticated: isAuthenticated, user: this.user};
this.$store.dispatch("security/onRefresh", payload);
if (this.$parent.$el.attributes["data-flashes"]) {
let flashes = JSON.parse(this.$parent.$el.attributes["data-flashes"].value);
if (app && app.attributes["data-flashes"]) {
let flashes = JSON.parse(app.attributes["data-flashes"].value);
if (flashes) {
for (const key in flashes) {
for (const text in flashes[key]) {
@ -163,8 +564,8 @@ export default {
}
}
if (this.$parent.$el.attributes["data-breadcrumb"]) {
this.breadcrumb = JSON.parse(this.$parent.$el.attributes["data-breadcrumb"].value);
if (app && app.attributes["data-breadcrumb"]) {
this.breadcrumb = JSON.parse(app.attributes["data-breadcrumb"].value);
}
axios.interceptors.response.use(undefined, (err) => {
@ -182,6 +583,27 @@ export default {
throw err;
});
});
},
mounted() {
console.log('mounted');
this.firstTime = true;
},
methods: {
dropdownHandler(event) {
let single = event.currentTarget.getElementsByTagName("ul")[0];
single.classList.toggle("hidden");
},
sidebarHandler() {
var sideBar = document.getElementById("mobile-nav");
sideBar.style.transform = "translateX(-260px)";
if (this.$data.moved) {
sideBar.style.transform = "translateX(0px)";
this.$data.moved = false;
} else {
sideBar.style.transform = "translateX(-260px)";
this.$data.moved = true;
}
},
}
}
</script>

@ -1,47 +1,80 @@
<template>
<div>
<b-button-toolbar>
<b-button
<!-- <div>-->
<!-- <b-button-toolbar>-->
<!-- <b-button-->
<!-- v-if="handleShow"-->
<!-- variant="info"-->
<!-- size="sm"-->
<!-- class="mr-2"-->
<!-- @click="handleShow"-->
<!-- >{{ $t('Info') }}-->
<!-- </b-button>-->
<!-- <b-button-->
<!-- v-if="handleEdit"-->
<!-- size="sm"-->
<!-- class="mr-2"-->
<!-- @click="handleEdit"-->
<!-- >{{ $t('Edit') }}-->
<!-- </b-button>-->
<!-- <b-button-->
<!-- v-if="handleDelete"-->
<!-- variant="danger"-->
<!-- size="sm"-->
<!-- @click="confirmDelete = true"-->
<!-- >{{ $t('Delete') }}-->
<!-- </b-button>-->
<!-- </b-button-toolbar>-->
<!-- <ConfirmDelete-->
<!-- v-if="handleDelete"-->
<!-- :visible="confirmDelete"-->
<!-- :handle-delete="handleDelete"-->
<!-- @close="confirmDelete = false"-->
<!-- />-->
<!-- </div>-->
<q-td slot="body-cell-action" auto-width>
<q-btn
v-if="handleShow"
variant="info"
size="sm"
class="mr-2"
flat
round
dense
color="secondary"
@click="handleShow"
>{{ $t('Info') }}
</b-button>
<b-button
v-if="handleEdit"
size="sm"
class="mr-2"
@click="handleEdit"
>{{ $t('Edit') }}
</b-button>
<b-button
icon="format_align_justify"
/>
<q-btn v-if="handleEdit" flat round dense color="secondary" @click="handleEdit" icon="edit" />
<q-btn
v-if="handleDelete"
variant="danger"
size="sm"
icon="delete"
flat
round
dense
color="secondary"
@click="confirmDelete = true"
>{{ $t('Delete') }}
</b-button>
</b-button-toolbar>
/>
<ConfirmDelete
v-if="handleDelete"
:visible="confirmDelete"
:handle-delete="handleDelete"
@close="confirmDelete = false"
v-if="handleDelete"
:show="confirmDelete"
:handle-delete="handleDelete"
:handle-cancel="() => (confirmDelete = false)"
/>
</div>
</q-td>
</template>
<script>
import ConfirmDelete from './ConfirmDelete';
import ConfirmDelete from './ConfirmDelete.vue';
export default {
name: 'ActionCell',
components: {
ConfirmDelete
},
data() {
return {
confirmDelete: false
};
},
props: {
handleShow: {
type: Function,
@ -55,11 +88,6 @@ export default {
type: Function,
required: false
}
},
data() {
return {
confirmDelete: false
};
}
};
</script>

@ -1,8 +1,8 @@
<template>
<div>
<b-breadcrumb
<v-breadcrumbs
:items="items"
divider="/"
large
:class="layoutClass"
/>
</div>
@ -39,13 +39,15 @@ export default {
href: '/course/' + this.$route.query.cid + '/home'
});
}*/
for (let i = 0, len = this.legacy.length; i < len; i += 1) {
//console.log(this.legacy[i]);
if (this.legacy) {
for (let i = 0, len = this.legacy.length; i < len; i += 1) {
//console.log(this.legacy[i]);
items.push({
text: this.legacy[i]['name'] ,
text: this.legacy[i]['name'],
//disabled: route.path === path || lastItem.path === route.path,
href: this.legacy[i]['url']
});
}
}

@ -1,51 +1,55 @@
<template>
<b-modal
v-model="show"
persistent
hide-footer
>
<b-card>
<b-card-text>{{ $t('Are you sure you want to delete this item?') }}</b-card-text>
<b-button
color="error darken-1"
@click="handleDelete"
>{{ $t('Yes') }}
</b-button>
<b-button
color="secondary darken-1"
text
@click.stop="show = false"
>{{ $t('No') }}
</b-button>
</b-card>
</b-modal>
<!-- <b-modal-->
<!-- v-model="show"-->
<!-- persistent-->
<!-- hide-footer-->
<!-- >-->
<!-- <b-card>-->
<!-- <b-card-text>{{ $t('Are you sure you want to delete this item?') }}</b-card-text>-->
<!-- <b-button-->
<!-- color="error darken-1"-->
<!-- @click="handleDelete"-->
<!-- >{{ $t('Yes') }}-->
<!-- </b-button>-->
<!-- <b-button-->
<!-- color="secondary darken-1"-->
<!-- text-->
<!-- @click.stop="show = false"-->
<!-- >{{ $t('No') }}-->
<!-- </b-button>-->
<!-- </b-card>-->
<!-- </b-modal>-->
<q-dialog :value="show" persistent>
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="warning" color="primary" text-color="white" />
<span class="q-ml-sm">{{ $t('Are you sure you want to delete this item?') }}</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup @click="handleCancel" />
<q-btn flat label="Delete" color="primary" v-close-popup @click="handleDelete" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script>
export default {
name: 'ConfirmDelete',
props: {
visible: {
show: {
type: Boolean,
required: true,
default: () => false
},
handleDelete: {
type: Function,
required: true
}
required: true,
},
handleCancel: {
type: Function,
required: false,
},
},
computed: {
show: {
get() {
return this.visible;
},
set(value) {
if (!value) {
this.$emit('close');
}
}
}
}
};
</script>

@ -1,33 +1,38 @@
<template>
<div class="">
<q-toolbar class="q-my-md">
<slot name="left" />
<b-button
<q-space />
<q-btn
v-if="handleList"
:loading="isLoading"
variant="primary"
color="primary"
@click="listItem"
unelevated
>
{{ $t('List') }}
</b-button>
<b-button
</q-btn>
<q-btn
v-if="handleEdit"
:loading="isLoading"
variant="primary"
color="primary"
@click="editItem"
unelevated
>
{{ $t('Edit') }}
</b-button>
</q-btn>
<b-button
<q-btn
v-if="handleSubmit"
:loading="isLoading"
variant="primary"
color="primary"
@click="submitItem"
unelevated
>
<font-awesome-icon icon="save" />
{{ $t('Submit') }}
</b-button>
</q-btn>
<!-- <v-btn-->
<!-- v-if="handleReset"-->
<!-- color="primary"-->
@ -36,53 +41,54 @@
<!-- >-->
<!-- {{ $t('Reset') }}-->
<!-- </v-btn>-->
<b-button
<q-btn
v-if="handleDelete"
variant="danger"
color="red"
unelevated
class="ml-sm-2"
@click="confirmDelete = true"
>
{{ $t('Delete') }}
</b-button>
</q-btn>
<b-button
<q-btn
v-if="handleAdd"
variant="primary"
color="primary"
rounded
@click="addItem"
>
<font-awesome-icon icon="folder-plus" /> New folder
</b-button>
</q-btn>
<b-button
<q-btn
v-if="handleAddDocument"
variant="primary"
color="primary"
rounded
@click="addDocument"
>
<font-awesome-icon icon="file-alt" /> New document
</b-button>
</q-btn>
<b-button
<q-btn
v-if="handleUploadDocument"
variant="primary"
color="primary"
rounded
@click="uploadDocument"
>
<font-awesome-icon icon="cloud-upload-alt" /> File upload
</b-button>
</q-btn>
<DataFilter
v-if="filters"
:handle-filter="onSendFilter"
:handle-reset="resetFilter"
>
<DocumentsFilterForm
ref="filterForm"
slot="filter"
:values="filters"
/>
</DataFilter>
<!-- <DataFilter-->
<!-- v-if="filters"-->
<!-- :handle-filter="onSendFilter"-->
<!-- :handle-reset="resetFilter"-->
<!-- >-->
<!-- <DocumentsFilterForm-->
<!-- ref="filterForm"-->
<!-- slot="filter"-->
<!-- :values="filters"-->
<!-- />-->
<!-- </DataFilter>-->
<ConfirmDelete
v-if="handleDelete"
@ -90,13 +96,13 @@
:handle-delete="handleDelete"
@close="confirmDelete = false"
/>
</div>
</q-toolbar>
</template>
<script>
import ConfirmDelete from './ConfirmDelete';
import DocumentsFilterForm from './documents/Filter';
import DataFilter from './DataFilter';
import ConfirmDelete from './ConfirmDelete.vue';
import DocumentsFilterForm from './documents/Filter.vue';
import DataFilter from './DataFilter.vue';
export default {
name: 'Toolbar',

@ -55,14 +55,16 @@
<script>
import has from 'lodash/has';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { mapActions } from 'vuex';
import { mapFields } from 'vuex-map-fields';
export default {
name: 'CourseForm',
mixins: [validationMixin],
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,

@ -32,14 +32,14 @@
<script>
import has from 'lodash/has';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import { mapActions } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
export default {
name: 'CourseCategoryForm',
mixins: [validationMixin],
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,

@ -1,12 +1,6 @@
<template>
<b-form>
<b-row>
<b-col
cols="12"
sm="6"
md="6"
>
<b-form-input
<q-input
id="item_title"
v-model="item.title"
:error-messages="titleErrors"
@ -15,22 +9,21 @@
@input="$v.item.title.$touch()"
@blur="$v.item.title.$touch()"
/>
</b-col>
</b-row>
<br>
</b-form>
</template>
<script>
import has from 'lodash/has';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import { mapActions } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
//import { mapActions } from 'vuex';
//import { mapFields } from 'vuex-map-fields';
export default {
name: 'DocumentsForm',
mixins: [validationMixin],
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,

@ -1,22 +1,17 @@
<template>
<b-form>
<b-row>
<b-col
cols="12"
sm="6"
md="6"
>
<b-form-group>
<b-form-input
<div class="q-pa-md">
<q-form>
<!-- @input="$v.item.title.$touch()"-->
<!-- @blur="$v.item.title.$touch()"-->
<q-input
outlined
id="item_title"
v-model="item.title"
:error-messages="titleErrors"
:placeholder="$t('Title')"
required
@input="$v.item.title.$touch()"
@blur="$v.item.title.$touch()"
/>
</b-form-group>
<editor
id="item_content"
@ -63,15 +58,14 @@
}
"
/>
</b-col>
</b-row>
</b-form>
</q-form>
</div>
</template>
<script>
import has from 'lodash/has';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
//import UploadAdapter from './UploadAdapter';
import Editor from '../Editor'
@ -80,7 +74,9 @@ export default {
components: {
'editor': Editor
},
mixins: [validationMixin],
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,
@ -97,6 +93,7 @@ export default {
},
data() {
return {
v$: useVuelidate(),
title: null,
contentFile: null,
parentResourceNodeId: null,
@ -109,20 +106,22 @@ export default {
},
titleErrors() {
const errors = [];
if (!this.$v.item.title.$dirty) return errors;
// @todo fix errors
/*if (!this.$v.item.title.$dirty) return errors;
has(this.violations, 'title') && errors.push(this.violations.title);
!this.$v.item.title.required && errors.push(this.$t('Field is required'));
!this.$v.item.title.required && errors.push(this.$t('Field is required'));*/
return errors;
},
contentFileErrors() {
const errors = [];
if (this.item.resourceNode && this.item.resourceNode.resourceFile && this.item.resourceNode.resourceFile.text) {
/*if (this.item.resourceNode && this.item.resourceNode.resourceFile && this.item.resourceNode.resourceFile.text) {
if (!this.$v.item.contentFile.$dirty) return errors;
has(this.violations, 'contentFile') && errors.push(this.violations.contentFile);
!this.$v.item.contentFile.required && errors.push(this.$t('Content is required'));
}
}*/
return errors;
},
@ -149,17 +148,17 @@ export default {
info = file.name + ' (' + fm.formatSize(file.size) + ')';
// Provide file and text for the link dialog
if (meta.filetype == 'file') {
if (meta.filetype === 'file') {
callback(url, {text: info, title: info});
}
// Provide image and alt text for the image dialog
if (meta.filetype == 'image') {
if (meta.filetype === 'image') {
callback(url, {alt: info});
}
// Provide alternative source and posted for the media dialog
if (meta.filetype == 'media') {
if (meta.filetype === 'media') {
callback(url);
}
}

@ -1,11 +1,12 @@
<template>
<b-form>
<b-row>
<b-col
cols="12"
sm="6"
md="6"
>
<q-form>
<!-- <q-uploader-->
<!-- :factory="processFiles"-->
<!-- label="Batch upload"-->
<!-- multiple-->
<!-- style="max-width: 800px;width: 800px"-->
<!-- />-->
<!-- v-model="item.uploadFile"-->
<!-- <b-form-file-->
<!-- ref="fileList"-->
@ -14,7 +15,6 @@
<!-- />-->
<div class="input-group mb-3">
<div class="custom-file">
<input
id="file_upload"
type="file"
@ -24,7 +24,6 @@
placeholder="File upload"
@change="selectFile"
/>
<label
class="custom-file-label"
for="file_upload"
@ -55,22 +54,22 @@
</div>
</div>
</div>
</b-col>
</b-row>
</b-form>
</q-form>
</template>
<script>
import has from 'lodash/has';
import map from 'lodash/map';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { mapActions } from 'vuex';
import { mapFields } from 'vuex-map-fields';
export default {
name: 'DocumentsFormUpload',
mixins: [validationMixin],
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Array,
@ -85,7 +84,11 @@ export default {
errors: {
type: Object,
default: () => {}
}
},
processFiles: {
type: Function,
required: false
},
},
data() {
return {

@ -1,6 +1,6 @@
<template>
<b-row>
<b-col
<v-row>
<v-col
cols="12"
sm="6"
md="6"
@ -23,7 +23,7 @@
{{ $t('Group') }}: {{ link.session.resourceNode.title }}
</div>
<b-form-select
<v-select
v-model="link.visibility"
:options="visibilityList"
label="Status"
@ -33,17 +33,20 @@
</ul>
</div>
</div>
</b-col>
</b-row>
</v-col>
</v-row>
</template>
<script>
import has from 'lodash/has';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
export default {
name: 'ResourceLinkForm',
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,

@ -0,0 +1,27 @@
<template>
<div class="flex h-screen bg-gray-200 font-roboto">
<Sidebar />
<div class="flex-1 flex flex-col overflow-hidden">
<Header />
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100">
<div class="container mx-auto px-6 py-8">
<slot />
</div>
</main>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import Sidebar from "./../sidebar/Sidebar.vue";
import Header from "./../sidebar/Header.vue";
export default defineComponent({
components: {
Header,
Sidebar,
},
});
</script>

@ -0,0 +1,6 @@
<template>
<div class="flex flex-col items-center justify-center min-h-screen p-4 space-y-4 antialiased text-gray-900 bg-white">
<slot></slot>
</div>
</template>

@ -1,45 +0,0 @@
<template>
<footer class="footer mt-auto px-4">
<b-row
align-v="center"
class="justify-content-lg-between"
>
<b-col lg="6">
<div class="text-center text-lg-left text-muted">
© {{ year }}
<a
href="https://www.chamilo.org"
class="font-weight-bold ml-1"
target="_blank"
>
Chamilo
</a>
</div>
</b-col>
<b-col lg="6">
<b-nav
align="center"
class="nav-footer justify-content-lg-end"
>
<b-nav-item
href="#"
target="_blank"
>
About
</b-nav-item>
</b-nav>
</b-col>
</b-row>
</footer>
</template>
<script>
export default {
data() {
return {
year: new Date().getFullYear()
};
}
};
</script>
<style></style>

@ -1,141 +0,0 @@
<template>
<b-navbar
toggleable="sm"
type="dark"
variant="primary"
fixed="top"
>
<button
v-b-toggle.sidebar-1
type="button"
aria-label="Toggle navigation"
class="navbar-toggler mr-3"
aria-controls="sidebar-1"
>
<span class="navbar-toggler-icon" />
</button>
<b-navbar-brand
href="/"
class="mr-auto mr-sm-0"
>
Chamilo
</b-navbar-brand>
<b-collapse
id="nav-collapse"
is-nav
>
<b-navbar-nav>
<!-- <b-nav-item href="#">-->
<!-- Link-->
<!-- </b-nav-item>-->
<!-- <b-nav-item-->
<!-- href="#"-->
<!-- disabled-->
<!-- >-->
<!-- Disabled-->
<!-- </b-nav-item>-->
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<b-navbar-nav v-if="!isAuthenticated">
<b-nav-item :to="'/login'">
Login
</b-nav-item>
<b-nav-item :to="'/register'">
Register
</b-nav-item>
</b-navbar-nav>
<!-- <b-nav-form>-->
<!-- <b-form-input-->
<!-- size="sm"-->
<!-- class="mr-sm-2"-->
<!-- placeholder="Search"-->
<!-- />-->
<!-- <b-button-->
<!-- size="sm"-->
<!-- class="my-2 my-sm-0"-->
<!-- type="submit"-->
<!-- >-->
<!-- Search-->
<!-- </b-button>-->
<!-- </b-nav-form>-->
<!-- <b-nav-item-dropdown-->
<!-- text="Lang"-->
<!-- right-->
<!-- >-->
<!-- <b-dropdown-item href="#">-->
<!-- EN-->
<!-- </b-dropdown-item>-->
<!-- <b-dropdown-item href="#">-->
<!-- ES-->
<!-- </b-dropdown-item>-->
<!-- <b-dropdown-item href="#">-->
<!-- RU-->
<!-- </b-dropdown-item>-->
<!-- <b-dropdown-item href="#">-->
<!-- FA-->
<!-- </b-dropdown-item>-->
<!-- </b-nav-item-dropdown>-->
<b-nav-item-dropdown
v-if="isAuthenticated"
right
no-caret
toggle-class="p-0"
>
<!-- Using 'button-content' slot -->
<template v-slot:button-content>
<b-avatar variant="light" />
</template>
<b-dropdown-text style="width: 240px;">
{{ currentUser.username }}
</b-dropdown-text>
<b-dropdown-divider />
<b-dropdown-item href="/main/messages/inbox.php">
Inbox
</b-dropdown-item>
<b-dropdown-item href="/account/home">
Profile
</b-dropdown-item>
<b-dropdown-item href="/logout">
Logout
</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
components: {},
props: {
type: {
type: String,
default: 'default', // default|light
description: 'Look of the dashboard navbar. Default (Green) or light (gray)'
}
},
data() {
return {
activeNotifications: false,
showMenu: false,
searchModalVisible: false,
searchQuery: ''
};
},
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'currentUser': 'security/getUser',
}),
},
methods: {}
};
</script>

@ -1,108 +0,0 @@
<template>
<b-sidebar
id="sidebar-1"
title="Chamilo"
backdrop
>
<template
v-slot:footer
>
<b-nav
v-if="!isAuthenticated"
class="d-sm-none bg-dark text-light"
>
<b-nav-item to="/login">
{{ $t('Login') }}
</b-nav-item>
<b-nav-item to="/register">
{{ $t('Register') }}
</b-nav-item>
</b-nav>
<p
v-if="isAuthenticated"
class="d-sm-none px-3 py-2 mb-0 bg-dark text-light"
>
{{ currentUser.username }}
</p>
<b-nav
v-if="isAuthenticated"
class="d-sm-none bg-dark text-light"
>
<b-nav-item href="/main/messages/inbox.php">
{{ $t('Inbox') }}
</b-nav-item>
<b-nav-item href="/account/home">
{{ $t('Profile') }}
</b-nav-item>
<b-nav-item href="/logout">
{{ $t('Logout') }}
</b-nav-item>
</b-nav>
</template>
<b-nav vertical>
<b-nav-item :to="{ name: 'Index' }">
{{ $t('Home') }}
</b-nav-item>
</b-nav>
<template v-if="isAuthenticated">
<b-nav vertical>
<b-nav-item
:to="{ name: 'MyCourses' }"
>
{{ $t('Courses') }}
</b-nav-item>
<b-nav-item
:to="{ name: 'MySessions' }"
>
{{ $t('Sessions') }}
</b-nav-item>
</b-nav>
</template>
<template v-if="isAuthenticated && isAdmin">
<b-nav vertical>
<h4 class="pt-3 px-3 mb-0">
{{ $t('Administration') }}
</h4>
<b-nav-item
:to="'/main/admin/user_list.php'"
>
{{ $t('Users') }}
</b-nav-item>
<b-nav-item
:to="'/main/admin/course_list.php'"
>
{{ $t('Courses') }}
</b-nav-item>
<b-nav-item
:to="'/main/session/session_list.php'"
>
{{ $t('Sessions') }}
</b-nav-item>
<b-nav-item
:to="'/main/admin/index.php'"
>
{{ $t('Administration') }}
</b-nav-item>
</b-nav>
</template>
</b-sidebar>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'currentUser': 'security/getUser',
'isAdmin': 'security/isAdmin',
}),
},
};
</script>

@ -0,0 +1,207 @@
<template>
<header
class="flex justify-between items-center py-4 px-6 bg-white border-b-2 border-gray-300"
>
<div class="flex items-center" v-if="isAuthenticated">
<q-tabs align="center" dense inline-label no-caps>
<q-route-tab to="/" label="Home" />
<q-route-tab to="/courses" label="My courses" />
<q-route-tab to="/main/calendar/agenda_js.php?type=personal" label="Agenda" />
</q-tabs>
<!-- <router-link :to="{name: 'Home'}" tag="button" class="flex items-center justify-center h-10 px-4 ml-auto text-sm font-medium rounded hover:bg-gray-300">-->
<!-- Home-->
<!-- </router-link>-->
<!-- <router-link :to="{name: 'MyCourses'}" tag="button" class="flex items-center justify-center h-10 px-4 ml-2 text-sm font-medium bg-gray-200 rounded hover:bg-gray-300">-->
<!-- My courses-->
<!-- </router-link>-->
</div>
<div class="flex items-center">
<button
@click="isOpen = true"
class="text-gray-500 focus:outline-none lg:hidden"
>
<svg
class="h-6 w-6"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 6H20M4 12H20M4 18H11"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<!-- <div class="relative mx-4 lg:mx-0">-->
<!-- <span class="absolute inset-y-0 left-0 pl-3 flex items-center">-->
<!-- <svg class="h-5 w-5 text-gray-500" viewBox="0 0 24 24" fill="none">-->
<!-- <path-->
<!-- d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z"-->
<!-- stroke="currentColor"-->
<!-- stroke-width="2"-->
<!-- stroke-linecap="round"-->
<!-- stroke-linejoin="round"-->
<!-- />-->
<!-- </svg>-->
<!-- </span>-->
<!-- <input-->
<!-- class="w-32 sm:w-64 rounded-md pl-10 pr-4 focus:border-indigo-600"-->
<!-- type="text"-->
<!-- placeholder="Search"-->
<!-- />-->
<!-- </div>-->
</div>
<div class="flex items-center">
<button class="flex mx-4 text-gray-600 focus:outline-none">
<svg
class="h-6 w-6"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 17H20L18.5951 15.5951C18.2141 15.2141 18 14.6973 18 14.1585V11C18 8.38757 16.3304 6.16509 14 5.34142V5C14 3.89543 13.1046 3 12 3C10.8954 3 10 3.89543 10 5V5.34142C7.66962 6.16509 6 8.38757 6 11V14.1585C6 14.6973 5.78595 15.2141 5.40493 15.5951L4 17H9M15 17V18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18V17M15 17H9"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<div v-if="isAuthenticated" class="relative">
<button
@click="dropdownOpen = !dropdownOpen"
class="relative z-10 block h-8 w-8 rounded-full overflow-hidden shadow focus:outline-none"
>
<img
class="h-full w-full object-cover"
:src="userAvatar + '?w=80&h=80&fit=crop'"
alt="Your avatar"
/>
</button>
<div
v-show="dropdownOpen"
@click="dropdownOpen = false"
class="fixed inset-0 h-full w-full z-10"
></div>
<div
v-show="dropdownOpen"
class="absolute right-0 mt-2 py-2 w-48 bg-white rounded-md shadow-xl z-20"
>
<!-- <a-->
<!-- href="/main/messages/inbox.php"-->
<!-- class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-200 hover:text-white"-->
<!-- >Inbox</a>-->
<!-- <a-->
<!-- href="/account/edit"-->
<!-- class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-200 hover:text-white"-->
<!-- >Settings</a>-->
<!-- <a-->
<!-- href="/logout"-->
<!-- class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-200 hover:text-white"-->
<!-- >Logout</a>-->
<!-- -->
<q-list dense>
<q-item class="GL__menu-link-signed-in">
<q-item-section>
<div>Signed in as <strong>{{ currentUser.username }}</strong></div>
</q-item-section>
</q-item>
<!-- <q-separator />-->
<!-- <q-item clickable class="GL__menu-link-status">-->
<!-- <q-item-section>-->
<!-- <div>-->
<!-- <q-icon name="tag_faces" color="blue-9" size="18px" />-->
<!-- Set your status-->
<!-- </div>-->
<!-- </q-item-section>-->
<!-- </q-item>-->
<q-separator />
<q-item replace :to="'/main/messages/index.php'" clickable class="">
<q-item-section>Inbox</q-item-section>
</q-item>
<q-item href="/account/home" tag="a" class="">
<q-item-section>
Your profile
</q-item-section>
</q-item>
<q-item href="/account/edit" tag="a" class="">
<q-item-section>Settings</q-item-section>
</q-item>
<q-item href="/logout" tag="a" clickable class="">
<q-item-section>
Sign out
</q-item-section>
</q-item>
</q-list>
</div>
</div>
<div v-else class="relative">
<router-link :to="'/login'" tag="button" class="flex items-center justify-center h-10 px-4 ml-2 text-sm font-medium bg-gray-200 rounded hover:bg-gray-300">
Login
</router-link>
</div>
</div>
</header>
</template>
<script lang="ts">
import { defineComponent, ref, toRefs } from "vue";
import { useSidebar } from "../../hooks/useSidebar";
import isEmpty from "lodash";
import {mapGetters} from "vuex";
export default defineComponent({
setup() {
const dropdownOpen = ref(false);
const { isOpen } = useSidebar();
const userAvatar = ref(window.userAvatar);
console.log('defineComponent window.user');
console.log(window.user);
console.log(window.userAvatar);
if (!isEmpty(window.user)) {
console.log('is logged in as ' + window.user.username);
//this.user = window.user;
userAvatar.value = window.userAvatar;
//isAuthenticated = true;
}
console.log(userAvatar.value);
return {
userAvatar,
isOpen,
dropdownOpen,
};
},
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'currentUser': 'security/getUser',
}),
},
});
</script>

@ -0,0 +1,207 @@
<template>
<div class="flex">
<!-- Backdrop -->
<div
:class="isOpen ? 'block' : 'hidden'"
@click="isOpen = false"
class="fixed z-20 inset-0 bg-white opacity-50 transition-opacity lg:hidden"
></div>
<!-- End Backdrop -->
<div
:class="isOpen ? 'translate-x-0 ease-out' : '-translate-x-full ease-in'"
class="fixed z-30 inset-y-0 left-0 w-64 transition duration-300 transform bg-white overflow-y-auto lg:translate-x-0 lg:static lg:inset-0"
>
<div class="flex items-center justify-center mt-4">
<div class="flex items-center">
<span class="text-white text-2xl mx-2 font-semibold">
<img style="width:200px" src="/build/css/themes/chamilo/images/header-logo.png" />
</span>
</div>
</div>
<nav class="mt-2">
<q-list v-if="isAuthenticated" padding class="text-grey-8">
<q-item class="GNL__drawer-item" v-ripple v-for="link in links1" :key="link.text" :to="link.url" clickable>
<q-item-section avatar>
<!-- <q-icon :name="link.icon" />-->
<span class="w-6 h-6 stroke-current">
<font-awesome-icon :icon="link.icon" size="lg" />
</span>
</q-item-section>
<q-item-section>
<q-item-label>{{ link.text }}</q-item-label>
</q-item-section>
</q-item>
<q-separator inset class="q-my-sm" />
<span v-if="isAdmin">
<q-item class="GNL__drawer-item" v-ripple v-for="link in links2" :key="link.text" :to="link.url" clickable>
<q-item-section avatar>
<!-- <q-icon :name="link.icon" />-->
<span class="w-6 h-6 stroke-current">
<font-awesome-icon :icon="link.icon" size="lg" />
</span>
</q-item-section>
<q-item-section>
<q-item-label>{{ link.text }}</q-item-label>
</q-item-section>
</q-item>
</span>
<q-separator inset class="q-my-sm" />
<q-item class="GNL__drawer-item" v-ripple v-for="link in links3" :key="link.text" clickable>
<q-item-section>
<q-item-label>{{ link.text }}
<q-icon v-if="link.icon" :name="link.icon" />
</q-item-label>
</q-item-section>
</q-item>
<div class="q-mt-md">
<div class="flex flex-center q-gutter-xs">
<a class="GNL__drawer-footer-link" href="javascript:void(0)" aria-label="About">Chamilo</a>
</div>
</div>
</q-list>
<q-list v-else padding class="text-grey-8">
<q-item class="GNL__drawer-item" v-ripple v-for="link in linksAnon" :key="link.text" :to="link.url" clickable>
<q-item-section avatar>
<q-icon :name="link.icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ link.text }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<!-- <router-link-->
<!-- class="flex items-center duration-200 mt-4 py-2 px-6 border-l-4"-->
<!-- :class="[$route.name === 'Dashboard' ? activeClass : inactiveClass]"-->
<!-- to="/dashboard"-->
<!-- >-->
<!-- <svg-->
<!-- class="h-5 w-5"-->
<!-- viewBox="0 0 20 20"-->
<!-- fill="none"-->
<!-- xmlns="http://www.w3.org/2000/svg"-->
<!-- >-->
<!-- <path-->
<!-- d="M2 10C2 5.58172 5.58172 2 10 2V10H18C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10Z"-->
<!-- fill="currentColor"-->
<!-- />-->
<!-- <path-->
<!-- d="M12 2.25195C14.8113 2.97552 17.0245 5.18877 17.748 8.00004H12V2.25195Z"-->
<!-- fill="currentColor"-->
<!-- />-->
<!-- </svg>-->
<!-- <span class="mx-4">Dashboard</span>-->
<!-- </router-link>-->
<!-- <span v-if="!isAuthenticated">-->
<!-- <router-link-->
<!-- class="flex items-center duration-200 mt-4 py-2 px-6 border-l-4"-->
<!-- v-for="link in linksAnon" :to="link.url"-->
<!-- >-->
<!-- <span class="w-6 h-6 stroke-current">-->
<!-- <font-awesome-icon :icon="link.icon" size="lg" />-->
<!-- </span>-->
<!-- <span class="mx-4 ">-->
<!-- {{ link.text }}-->
<!-- </span>-->
<!-- </router-link>-->
<!-- </span>-->
<!-- <span v-if="isAuthenticated">-->
<!-- <router-link-->
<!-- class="flex items-center duration-200 mt-4 py-2 px-6 border-l-4"-->
<!-- v-for="link in links1" :to="link.url"-->
<!-- >-->
<!-- <span class="w-6 h-6 stroke-current">-->
<!-- <font-awesome-icon :icon="link.icon" size="lg" />-->
<!-- </span>-->
<!-- <span class="mx-4 ">-->
<!-- {{ link.text }}-->
<!-- </span>-->
<!-- </router-link>-->
<!-- <router-link-->
<!-- class="flex items-center duration-200 mt-4 py-2 px-6 border-l-4"-->
<!-- v-for="link in links2" :to="link.url">-->
<!-- <span class="w-6 h-6 stroke-current">-->
<!-- <font-awesome-icon :icon="link.icon" size="lg" />-->
<!-- </span>-->
<!-- <span class="mx-4 ">-->
<!-- {{ link.text }}-->
<!-- </span>-->
<!-- </router-link>-->
<!-- </span>-->
</nav>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from "vue";
import { useSidebar } from "../../hooks/useSidebar";
import {mapGetters} from "vuex";
export default defineComponent({
setup() {
const { isOpen } = useSidebar();
const activeClass = ref(
"bg-gray-600 bg-opacity-25 text-gray-100 border-gray-100"
);
const inactiveClass = ref(
"border-gray-900 text-gray-500 hover:bg-gray-600 hover:bg-opacity-25 hover:text-gray-100"
);
const links1 = [
// { icon: 'person', url: '/courses', text: 'My courses' },
// { icon: 'star_border', url: '/sessions', text: 'Sessions' },
//{ icon: 'star_border', url: '/calendar', text: 'My calendar' },
{ icon: 'compass', url: '/catalog', text: 'Explore' },
// { icon: 'star_border', url: '/news', text: 'News' },
];
const links2 = [
{ icon: 'users', url: '/main/admin/user_list.php', text: 'Users' },
{ icon: 'book', url: '/main/admin/course_list.php', text: 'Courses' },
{ icon: 'book-open', url: '/main/session/session_list.php', text: 'Sessions' },
//{ icon: fasFlask, url: '/main/admin/index.php', text: 'Administration' },
{ icon: 'cogs', url: '/main/admin/index.php', text: 'Administration' },
];
const linksAnon = [
{ icon: 'home', url: '/', text: 'Home' },
];
return {
linksAnon,
links1,
links2,
isOpen,
activeClass,
inactiveClass,
};
},
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'currentUser': 'security/getUser',
}),
},
});
</script>

@ -0,0 +1,11 @@
import { reactive, toRefs } from "vue";
const state = reactive({
isOpen: false,
});
export function useSidebar() {
return {
...toRefs(state),
};
}

@ -0,0 +1,22 @@
import { ref } from 'vue'
const isSidebarOpen = ref(false)
const toggleSidebar = () => {
isSidebarOpen.value = !isSidebarOpen.value
}
const isSettingsPanelOpen = ref(false)
const isSearchPanelOpen = ref(false)
const isNotificationsPanelOpen = ref(false)
export default function useState() {
return {
isSidebarOpen,
toggleSidebar,
isSettingsPanelOpen,
isSearchPanelOpen,
isNotificationsPanelOpen,
}
}

@ -1,10 +1,8 @@
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import messages from './locales/en';
import { createI18n } from 'vue-i18n';
Vue.use(VueI18n);
import messages from './locales/en';
export default new VueI18n({
export default createI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: {

@ -0,0 +1,13 @@
<template>
<q-layout>
<q-tabs align="left" dense inline-label no-caps>
<q-route-tab to="/catalog/course" label="Courses" />
<q-route-tab to="/catalog/session" label="Sessions" />
</q-tabs>
<!-- this is where the Pages are injected -->
<q-page-container>
<router-view></router-view>
</q-page-container>
</q-layout>
</template>

@ -0,0 +1,13 @@
<template>
<q-layout>
<q-tabs align="left" dense inline-label no-caps>
<q-route-tab to="/courses" label="My courses" />
<q-route-tab to="/sessions" label="My sessions" />
</q-tabs>
<!-- this is where the Pages are injected -->
<q-page-container>
<router-view></router-view>
</q-page-container>
</q-layout>
</template>

@ -1,19 +1,19 @@
import Vue from 'vue';
import App from './App';
import { createApp } from 'vue';
import App from './App.vue';
import i18n from './i18n';
import router from './router';
import store from './store';
import axios from 'axios'
import courseCategoryService from './services/coursecategory';
import documentsService from './services/documents';
import courseService from './services/course';
import resourceLinkService from './services/resourcelink';
import resourceNodeService from './services/resourcenode';
import makeCrudModule from './store/modules/crud';
//import vuetify from './plugins/vuetify' // path to vuetify export
require('@fancyapps/fancybox');
require('@fancyapps/fancybox/dist/jquery.fancybox.css');
import Vuelidate from 'vuelidate';
//require('@fancyapps/fancybox');
//require('@fancyapps/fancybox/dist/jquery.fancybox.css');
/*
import VueApollo from 'vue-apollo';
@ -23,44 +23,27 @@ const apolloClient = new ApolloClient({
uri: '/api/graphql/'
});*/
import { BootstrapVue } from 'bootstrap-vue';
// Install BootstrapVue
Vue.use(BootstrapVue);
// Optionally install the BootstrapVue icon components plugin
//Vue.use(IconsPlugin)
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { fas } from '@fortawesome/free-solid-svg-icons';
library.add(fas);
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.config.productionTip = true;
Vue.use(Vuelidate);
//Vue.use(Vuelidate);
//Vue.use(VueApollo);
Vue.use(require('vue-moment'));
import Toast from 'vue-toastification';
import 'vue-toastification/dist/index.css';
Vue.use(Toast, {
const toastOptions = {
transition: 'Vue-Toastification__fade',
maxToasts: 20,
newestOnTop: true
});
};
/*const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});*/
const prettyBytes = require('pretty-bytes');
Vue.filter('prettyBytes', function (num) {
return prettyBytes(num);
});
import flatPickr from 'vue-flatpickr-component';
import VueFlatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
Vue.component('flat-pickr', flatPickr);
store.registerModule(
'course',
@ -97,16 +80,121 @@ store.registerModule(
})
);
if (document.getElementById('app')) {
new Vue({
i18n,
components: {App},
//apolloProvider,
data: {},
store,
router,
mounted() {
// Vuetify.
/*import '@mdi/font/css/materialdesignicons.css';
import { createVuetify } from 'vuetify';
import 'vuetify/lib/styles/main.sass';
import * as components from 'vuetify/lib/components';
import * as directives from 'vuetify/lib/directives';
import { aliases, mdi } from 'vuetify/lib/iconsets/mdi'
const options = {
components,
directives,
defaults: {
global: {
ripple: false,
},
VSheet: {
elevation: 4,
},
render: h => h(App)
}).$mount('#app');
},
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi,
}
},
theme: {
defaultTheme: 'light'
},
}
const vuetify = createVuetify(options);*/
import DashboardLayout from './components/layout/DashboardLayout.vue'
import EmptyLayout from './components/layout/EmptyLayout.vue'
// Vue setup.
const app = createApp(App);
// Quasar
import { Quasar } from 'quasar'
import quasarUserOptions from './quasar-user-options'
// Prime
import PrimeVue from 'primevue/config'
import DataView from 'primevue/dataview';
import DataTable from 'primevue/datatable';
import Dropdown from 'primevue/dropdown';
import DataViewLayoutOptions from 'primevue/dataviewlayoutoptions';
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import Button from 'primevue/button';
import Column from 'primevue/column';
import ColumnGroup from 'primevue/columngroup';
import 'primevue/resources/themes/mdc-light-indigo/theme.css';
//import 'primevue/resources/themes/bootstrap4-light-blue/theme.css';
import 'primevue/resources/primevue.min.css';
import 'primeflex/primeflex.css';
import "primeicons/primeicons.css";
//import './mdb/scss/index.free.scss';
app.component('Dialog', Dialog);
app.component('DataView', DataView);
app.component('DataTable', DataTable);
app.component('Dropdown', Dropdown);
app.component('DataViewLayoutOptions', DataViewLayoutOptions);
app.component('InputText', InputText);
app.component('Button', Button);
app.component('Column', Column);
app.component('ColumnGroup', ColumnGroup);
app.component('font-awesome-icon', FontAwesomeIcon);
app.component('DashboardLayout', DashboardLayout)
app.component('EmptyLayout', EmptyLayout)
app.config.globalProperties.axios = axios;
const prettyBytes = require('pretty-bytes');
const { DateTime } = require("luxon");
app.config.globalProperties.$luxonDateTime = DateTime;
app.config.globalProperties.$filters = {
prettyBytes(num) {
return prettyBytes(num);
},
}
app
.use(PrimeVue, {ripple: true})
.use(Quasar, quasarUserOptions)
.use(VueFlatPickr)
//.use(VuelidatePlugin)
//.use(vuetify)
.use(router)
.use(store)
.use(i18n)
.use(Toast, toastOptions)
;
app.mount('#app');
/*
new Vue({
vuetify,
i18n,
components: {App},
//apolloProvider,
data: {},
store,
router,
mounted() {
},
render: h => h(App)
}).$mount('#app');*/

@ -17,9 +17,9 @@ export default {
},
onSendForm() {
const createForm = this.$refs.createForm;
createForm.$v.$touch();
if (!createForm.$v.$invalid) {
this.create(createForm.$v.item.$model);
createForm.v$.$touch();
if (!createForm.v$.$invalid) {
this.create(createForm.v$.item.$model);
}
},
resetForm() {

@ -6,25 +6,41 @@ import NotificationMixin from './NotificationMixin';
export default {
mixins: [NotificationMixin],
created() {
//console.log('created');
},
data() {
return {
pagination: {
sortBy: null,
descending: false,
page: 1, // page to be displayed
rowsPerPage: 3, // maximum displayed rows
rowsNumber: 10, // max number of rows
},
nextPage: null,
filters: {},
filtration: {},
expandedFilter: false,
options: {
//sortBy: [], vuetify
//sortDesc: [], , vuetify
page: 1,
itemsPerPage: 20
},
filters: {}
//filters: {}
};
},
watch: {
$route() {
console.log('watch listmixin');
// react to route changes...
this.resetList = true;
this.onUpdateOptions(this.options);
let nodeId = this.$route.params['node'];
this.findResourceNode('/api/resource_nodes/'+ nodeId);
if (!isEmpty(nodeId)) {
this.findResourceNode('/api/resource_nodes/'+ nodeId);
}
},
deletedItem(item) {
@ -41,6 +57,27 @@ export default {
}
},
methods: {
onRequest(props, init) {
const { pagination: { page, rowsPerPage: itemsPerPage, sortBy, descending }} = props;
this.nextPage = page;
let params = {
...this.filtration,
};
if (itemsPerPage > 0) {
params = { ...params, itemsPerPage, page };
}
if (sortBy) {
params[`order[${sortBy}]`] = descending ? "DESC" : "ASC";
}
this.getPage({ params }).then(() => {
this.pagination.sortBy = sortBy;
this.pagination.descending = descending;
this.pagination.rowsPerPage = itemsPerPage;
if (!init) {
this.filters = { ...this.filtration };
}
});
},
fetchNewItems({ page, itemsPerPage, sortBy, sortDesc, totalItems } = {}) {
let params = {
...this.filters
@ -61,15 +98,14 @@ export default {
//this.resetList = true;
this.getPage(params).then(() => {
this.options.sortBy = sortBy;
this.options.sortDesc = sortDesc;
this.options.itemsPerPage = itemsPerPage;
this.options.totalItems = totalItems;
});
//this.getPage(params).then(() => {
this.options.sortBy = sortBy;
this.options.sortDesc = sortDesc;
this.options.itemsPerPage = itemsPerPage;
this.options.totalItems = totalItems;
//});
},
onUpdateOptions({ page, itemsPerPage, sortBy, sortDesc, totalItems } = {}) {
//console.log({ page, itemsPerPage, sortBy, sortDesc, totalItems });
onUpdateOptions({ page, itemsPerPage, sortBy, sortDesc, totalItems, getPage } = {}) {
let params = {
...this.filters
};
@ -88,13 +124,17 @@ export default {
}
this.resetList = true;
this.getPage(params).then(() => {
this.options.sortBy = sortBy;
this.options.sortDesc = sortDesc;
this.options.itemsPerPage = itemsPerPage;
this.options.totalItems = totalItems;
});
console.log('onUpdateOptions');
console.log(params);
if (getPage) {
getPage(params).then(() => {
this.options.sortBy = sortBy;
this.options.sortDesc = sortDesc;
this.options.itemsPerPage = itemsPerPage;
this.options.totalItems = totalItems;
});
}
//console.log('end');
},
onSendFilter() {

@ -1,7 +1,13 @@
import {mapFields} from 'vuex-map-fields';
import Component from "../components/Snackbar.vue";
import Snackbar from "../components/Snackbar.vue";
//import { useToast } from "vue-toastification";
// inside of a Vue file
//import { useQuasar } from 'quasar'
export default {
setup() {
},
computed: {
...mapFields('notifications', ['color', 'show', 'subText', 'text', 'timeout'])
},
@ -16,27 +22,58 @@ export default {
this.showMessage(error, 'danger');
},
showMessage(message, type = 'success') {
const content = {
/*const content = {
// Your component or JSX template
component: Component,
component: Snackbar,
// Props are just regular props, but these won't be reactive
props: {
message: message
},
// Listeners will listen to and execute on event emission
listeners: {
//click: () => console.log("Clicked!"),
//myEvent: myEventHandler
}
};
};*/
let color = 'primary';
let icon = 'info';
switch (type) {
case 'info':
break;
case 'success':
color = 'green';
break;
case 'error':
case 'danger':
color = 'red';
icon: 'error';
break;
case 'warning':
color = 'yellow';
break;
}
if ('danger' === type) {
type = 'error';
}
this.$toast(content, {
this.$q.notify({
position: 'top',
timeout: 10000,
message: message,
color: color,
html: true,
multiLine: true,
})
/*const toast = useToast();
console.log('toast');
console.log(message);
console.log(content);
toast(content, {
type: type,
position: 'top-center',
timeout: 10000, // 10 seconds
@ -50,7 +87,7 @@ export default {
closeButton: "button",
icon: true,
rtl: false
});
});*/
/*this.show = true;
this.color = color;

@ -8,7 +8,6 @@ export default {
onCreated(item) {
this.showMessage(this.$i18n.t('{resource} created', {'resource': item['resourceNode'].title}));
const createForm = this.$refs.createForm;
let folderParams = this.$route.query;
/*this.$router.push({
@ -20,15 +19,15 @@ export default {
onUploadForm() {
console.log('onUploadForm');
const createForm = this.$refs.createForm;
createForm.$v.$touch();
if (!createForm.$v.$invalid) {
//createForm.processFiles();
//createForm.$v.$touch();
//if (!createForm.$v.$invalid) {
for (let i = 0; i < createForm.files.length; i++) {
let file = createForm.files[i];
this.create(file);
}
}
}
//}
},
},
watch: {
created(created) {

@ -0,0 +1,23 @@
<template>
<div>
Welcome !
</div>
</template>
<script>
export default {
name: "Index",
data() {
return {
};
},
computed: {
},
created() {
},
methods: {
}
}
</script>

@ -0,0 +1,19 @@
import './styles/quasar.sass'
import '@quasar/extras/material-icons/material-icons.css'
import '@quasar/extras/fontawesome-v5/fontawesome-v5.css'
import { Notify } from 'quasar'
export default {
config: {
notify: {
}
},
plugins: [
Notify
],
extras: [
'material-icons',
'fontawesome-v5',
]
}

@ -2,28 +2,28 @@ export default {
path: '/resources/courses',
meta: { requiresAuth: true },
name: 'courses',
component: () => import('../components/course/Layout'),
component: () => import('../components/course/Layout.vue'),
redirect: { name: 'CourseList' },
children: [
{
name: 'CourseList',
path: '',
component: () => import('../views/course/List')
component: () => import('../views/course/List.vue')
},
{
name: 'CourseCreate',
path: 'new',
component: () => import('../views/course/Create')
component: () => import('../views/course/Create.vue')
},
{
name: 'CourseUpdate',
path: ':id/edit',
component: () => import('../views/course/Update')
component: () => import('../views/course/Update.vue')
},
{
name: 'CourseShow',
path: ':id',
component: () => import('../views/course/Show')
component: () => import('../views/course/Show.vue')
}
]
};

@ -2,28 +2,28 @@ export default {
path: '/resources/course_categories',
meta: { requiresAuth: true },
name: 'course_categories',
component: () => import('../components/coursecategory/Layout'),
component: () => import('../components/coursecategory/Layout.vue'),
redirect: { name: 'CourseCategoryList' },
children: [
{
name: 'CourseCategoryList',
path: '',
component: () => import('../views/coursecategory/List')
component: () => import('../views/coursecategory/List.vue')
},
{
name: 'CourseCategoryCreate',
path: 'new',
component: () => import('../views/coursecategory/Create')
component: () => import('../views/coursecategory/Create.vue')
},
{
name: 'CourseCategoryUpdate',
path: ':id/edit',
component: () => import('../views/coursecategory/Update')
component: () => import('../views/coursecategory/Update.vue')
},
{
name: 'CourseCategoryShow',
path: ':id',
component: () => import('../views/coursecategory/Show')
component: () => import('../views/coursecategory/Show.vue')
}
]
};

@ -2,46 +2,46 @@ export default {
path: '/resources/document/:node/',
meta: { requiresAuth: true },
name: 'documents',
component: () => import('../components/documents/Layout'),
component: () => import('../components/documents/Layout.vue'),
redirect: { name: 'DocumentsList' },
children: [
{
name: 'DocumentsList',
path: '',
component: () => import('../views/documents/List')
component: () => import('../views/documents/List.vue')
},
{
name: 'DocumentsCreate',
path: 'new',
component: () => import('../views/documents/Create')
component: () => import('../views/documents/Create.vue')
},
{
name: 'DocumentsCreateFile',
path: 'new_file',
component: () => import('../views/documents/CreateFile')
component: () => import('../views/documents/CreateFile.vue')
},
{
name: 'DocumentsUploadFile',
path: 'upload',
component: () => import('../views/documents/Upload')
component: () => import('../views/documents/Upload.vue')
},
{
name: 'DocumentsUpdate',
//path: ':id/edit',
path: 'edit',
component: () => import('../views/documents/Update')
component: () => import('../views/documents/Update.vue')
},
{
name: 'DocumentsUpdateFile',
//path: ':id/edit',
path: 'edit_file',
component: () => import('../views/documents/UpdateFile')
component: () => import('../views/documents/UpdateFile.vue')
},
{
name: 'DocumentsShow',
//path: ':id',
path: 'show',
component: () => import('../views/documents/Show')
component: () => import('../views/documents/Show.vue')
}
]
};

@ -1,34 +1,70 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import i18n from '../i18n';
Vue.use(VueRouter);
import { createRouter, createWebHistory } from 'vue-router';
import courseRoutes from './course';
import courseCategoryRoutes from './coursecategory';
//import courseCategoryRoutes from './coursecategory';
import documents from './documents';
import store from '../store';
import Login from '../views/Login';
//import Legacy from '../views/Legacy';
import Home from '../views/Home';
import MyCourseList from '../views/user/courses/List';
import MySessionList from '../views/user/sessions/List';
import Login from '../views/Login.vue';
//import Legacy from '../views/Legacy.vue';
//import Home from '../views/Home.vue';
import MyCourseList from '../views/user/courses/List.vue';
import MySessionList from '../views/user/sessions/List.vue';
import UserHome from '../views/user/profile/Home.vue';
import CatalogLayout from '../layouts/Catalog.vue';
import MyCoursesLayout from '../layouts/MyCourses.vue';
import CourseCatalog from '../views/course/Catalog.vue';
import SessionCatalog from '../views/course/CatalogSession.vue';
import CourseHome from '../views/course/Home.vue';
import Index from '../pages/Index.vue';
let router = new VueRouter({
mode: 'history',
const router = createRouter({
history: createWebHistory(),
routes: [
{path: '/', name: 'Index'},
{path: '/', name: 'Home', component: Index},
{path: '/login', name: 'Login', component: Login},
{
path: '/courses', name: 'MyCourses', component: MyCourseList,
path: '/course/:id/home', name: 'CourseHome', component: CourseHome
},
{
path: '/courses',
component: MyCoursesLayout,
children: [
{
path: '/courses', name: 'MyCourses', component: MyCourseList,
meta: {requiresAuth: true},
},
{
path: '/sessions', name: 'MySessions', component: MySessionList,
meta: {requiresAuth: true},
},
],
},
{
path: '/catalog',
redirect: '/catalog/course',
name: 'Catalog',
component: CatalogLayout,
children: [
{
path: 'course',
component: CourseCatalog
},
{
path: 'session',
component: SessionCatalog
},
],
meta: {requiresAuth: true},
},
{
path: '/sessions', name: 'MySessions', component: MySessionList,
path: '/account/profile', name: 'Profile', component: UserHome,
meta: {requiresAuth: true},
},
courseRoutes,
courseCategoryRoutes,
//courseCategoryRoutes,
documents
]
});

@ -3,14 +3,19 @@ import fetch from '../utils/fetch';
export default function makeService(endpoint) {
return {
find(id) {
console.log('find');
console.log(id);
let options = {params: {getFile: true}};
return fetch(`${id}`, options);
},
findAll(params) {
//console.log('findAll');
console.log('findAll');console.log(params);
return fetch(endpoint, params);
},
async createFile(payload) {
return fetch(endpoint, { method: 'POST', body: payload });
//return fetch(endpoint, { method: 'POST', body: JSON.stringify(payload) });
},
create(payload) {
return fetch(endpoint, { method: 'POST', body: payload });
//return fetch(endpoint, { method: 'POST', body: JSON.stringify(payload) });

@ -1,12 +1,9 @@
import Vue from "vue";
import Vuex from "vuex";
import { createStore } from "vuex";
import notifications from './modules/notifications';
import SecurityModule from "./security";
import createPersistedState from "vuex-persistedstate";
Vue.use(Vuex);
export default new Vuex.Store({
export default createStore({
plugins: [createPersistedState()],
modules: {
notifications,

@ -1,7 +1,7 @@
import Vue from 'vue';
import { getField, updateField } from 'vuex-map-fields';
import remove from 'lodash/remove';
import SubmissionError from '../../error/SubmissionError';
import isEmpty from 'lodash/isEmpty';
const initialState = () => ({
allIds: [],
@ -15,7 +15,8 @@ const initialState = () => ({
totalItems: 0,
updated: null,
view: null,
violations: null
violations: null,
resourceNode: null
});
const handleError = (commit, e) => {
@ -62,6 +63,21 @@ export default function makeCrudModule({
} = {}) {
return {
actions: {
createFile: ({ commit }, values) => {
commit(ACTIONS.SET_ERROR, '');
commit(ACTIONS.TOGGLE_LOADING);
service
.createFile(values)
.then(response => response.json())
.then(data => {
commit(ACTIONS.TOGGLE_LOADING);
commit(ACTIONS.ADD, data);
commit(ACTIONS.SET_CREATED, data);
})
.catch(e => handleError(commit, e));
},
create: ({ commit }, values) => {
commit(ACTIONS.SET_ERROR, '');
commit(ACTIONS.TOGGLE_LOADING);
@ -77,6 +93,8 @@ export default function makeCrudModule({
.catch(e => handleError(commit, e));
},
del: ({ commit }, item) => {
console.log('del');
commit(ACTIONS.TOGGLE_LOADING);
service
@ -104,7 +122,6 @@ export default function makeCrudModule({
commit(ACTIONS.TOGGLE_LOADING);
}
},
fetchAll: ({ commit, state }, params) => {
if (!service) throw new Error('No service specified!');
@ -114,12 +131,11 @@ export default function makeCrudModule({
.findAll({ params })
.then(response => response.json())
.then(retrieved => {
commit(ACTIONS.TOGGLE_LOADING);
//console.log(retrieved['hydra:totalItems']);
//console.log(retrieved['hydra:view']);
commit(
ACTIONS.SET_TOTAL_ITEMS,
retrieved['hydra:totalItems']
);
commit(ACTIONS.TOGGLE_LOADING);
commit(ACTIONS.SET_TOTAL_ITEMS, retrieved['hydra:totalItems']);
commit(ACTIONS.SET_VIEW, retrieved['hydra:view']);
if (true === state.resetList) {
commit(ACTIONS.RESET_LIST);
@ -154,6 +170,10 @@ export default function makeCrudModule({
load: ({ commit }, id, options = {}) => {
if (!service) throw new Error('No service specified!');
if (isEmpty(id)) {
throw new Error('Incorrect id');
}
commit(ACTIONS.TOGGLE_LOADING);
service
.find(id, options)
@ -165,6 +185,7 @@ export default function makeCrudModule({
.catch(e => handleError(commit, e));
},
findResourceNode: ({ commit }, id) => {
//console.log('findResourceNode');
if (!service) throw new Error('No service specified!');
service
@ -219,13 +240,19 @@ export default function makeCrudModule({
mutations: {
updateField,
[ACTIONS.ADD_RESOURCE_NODE]: (state, item) => {
Vue.set(state, 'resourceNode', item);
Vue.set(state, 'isLoading', false);
state.resourceNode = item;
state.isLoading = false;
//this.$set(state, 'resourceNode', item);
//this.$set(state, 'isLoading', false);
},
[ACTIONS.ADD]: (state, item) => {
Vue.set(state.byId, item['@id'], item);
Vue.set(state, 'isLoading', false);
if (state.allIds.includes(item['@id'])) return;
//this.$set(state.byId, item['@id'], item);
state.byId[item['@id']] = item;
state.isLoading = false;
//this.$set(state, 'isLoading', false);
if (state.allIds.includes(item['@id'])) {
return;
}
state.allIds.push(item['@id']);
},
[ACTIONS.RESET_CREATE]: state => {

@ -0,0 +1,7 @@
@import 'quasar.variables'
@import '~quasar/dist/quasar.sass'
//@import '~quasar-style'
// @import '~quasar-addon-styl'

@ -0,0 +1,15 @@
// It's highly recommended to change the default colors
// to match your app's branding.
$primary : #027BE3
$secondary : #26A69A
$accent : #9C27B0
$dark : #1D1D1D
$positive : #21BA45
$negative : #C10015
$info : #31CCEC
$warning : #F2C037
//@import '~quasar-variables-styl'

@ -1,9 +1,9 @@
import moment from 'moment';
const { DateTime } = require("luxon");
const formatDateTime = function(date) {
if (!date) return null;
return moment(date).format('DD/MM/YYYY');
return DateTime(date).format('DD/MM/YYYY');
};
export { formatDateTime };

@ -1,18 +1,35 @@
import isObject from 'lodash/isObject';
import axios from "axios";
import { isArray, isObject, isUndefined, forEach } from 'lodash';
import { ENTRYPOINT } from '../config/entrypoint';
import SubmissionError from '../error/SubmissionError';
import { normalize } from './hydra';
const MIME_TYPE = 'application/ld+json';
const transformRelationToIri = (payload) => {
forEach(payload, (value, property) => {
if (isObject(value) && !isUndefined(value['@id'])) {
payload[property] = value['@id'];
}
if (isArray(value)) payload[property] = transformRelationToIri(value);
});
return payload;
};
const makeParamArray = (key, arr) =>
arr.map(val => `${key}[]=${val}`).join('&');
export default function(id, options = {}) {
console.log('fetch');
console.log(options.method);
if ('undefined' === typeof options.headers) options.headers = new Headers();
if (null === options.headers.get('Accept'))
if (null === options.headers.get('Accept')) {
options.headers.set('Accept', MIME_TYPE);
}
/*if (
'undefined' !== options.body &&
@ -35,10 +52,15 @@ export default function(id, options = {}) {
const entryPoint = ENTRYPOINT + (ENTRYPOINT.endsWith('/') ? '' : '/');
/*let useAxios = false;
let originalBody = options.body;*/
let formData = new FormData();
if ('POST' === options.method) {
if (options.body) {
let formData = new FormData();
Object.keys(options.body).forEach(function (key) {
/*if (key === 'uploadFile') {
useAxios = true;
}*/
// key: the name of the object key
// index: the ordinal position of the key within the object
formData.append(key, options.body[key]);
@ -51,14 +73,54 @@ export default function(id, options = {}) {
const payload = options.body && JSON.parse(options.body);
if (isObject(payload) && payload['@id']) {
options.body = JSON.stringify(normalize(payload));
//options.body = JSON.stringify(transformRelationToIri(payload));
}
}
/*if (useAxios) {
console.log('axios');
let url = new URL(id, entryPoint);
console.log(formData);
return axios({
url: url.toString(),
method: 'POST',
//headers: options.headers,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: function (progressEvent) {
console.log('progress');
//console.log(progressEvent);
console.log(options.body);
let uploadPercentage = parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100));
options.body['__progress'] = uploadPercentage;
options.body['__progressLabel'] = uploadPercentage;
this.uploadPercentage = uploadPercentage;
console.log(options.body);
//options.body.set('__progressLabel', uploadPercentage);
}.bind(this)
}
).then(response => {
options.body['__uploaded'] = 1;
options.body['uploadFile']['__uploaded'] = 1
console.log(response);
console.log('SUCCESS!!');
return response.data;
})
.catch(function (response) {
console.log(response);
console.log('FAILURE!!');
});
}*/
return global.fetch(new URL(id, entryPoint), options).then(response => {
if (response.ok) return response;
return response.json().then(
json => {
return response.json().then(json => {
const error =
json['hydra:description'] ||
json['hydra:title'] ||
@ -80,5 +142,5 @@ export default function(id, options = {}) {
throw new Error(response.statusText || 'An error occurred.');
}
);
});
}).catch(error => console.error('Error:', error));
}

@ -1,24 +0,0 @@
<template>
<v-container
class="fill-height"
fluid
/>
</template>
<script>
export default {
name: "Home",
data() {
return {
};
},
computed: {
},
created() {
},
methods: {
}
}
</script>

@ -1,118 +1,115 @@
<template>
<div class="mt-5 p-5">
<b-container
fluid
>
<b-row>
<b-col cols="4" />
<b-col cols="4">
<form
@submit="onSubmit"
>
<p class="h4 text-center mb-4">
{{ $t('Sign in') }}
</p>
<div class="grey-text">
<b-form-input
v-model="login"
:placeholder="$t('Login')"
icon="envelope"
type="text"
required
name="login"
/>
<b-form-input
v-model="password"
:placeholder=" $t('Password') "
icon="lock"
type="password"
name="password"
required
/>
</div>
<div class=" flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<!-- <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg" alt="Workflow" />-->
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Login To Your Account
</h2>
</div>
<form class="mt-8 space-y-6" @submit.prevent="onSubmit">
<input type="hidden" name="remember" value="true" />
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="email-address" class="sr-only">Username</label>
<input id="email-address" v-model="login" name="login" type="text" autocomplete="login" required="" class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Username" />
</div>
<div>
<label for="password" class="sr-only">Password</label>
<input id="password" v-model="password" name="password" type="password" autocomplete="current-password" required="" class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password" />
</div>
</div>
<div class="text-center">
<b-button
block
type="submit"
variant="primary"
>
{{ $t('Login') }}
</b-button>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input id="remember_me" name="remember_me" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
<label for="remember_me" class="ml-2 block text-sm text-gray-900">
Remember me
</label>
</div>
<div
v-if="isLoading"
class="row col"
>
<p><font-awesome-icon icon="spinner" /></p>
</div>
<div class="text-sm">
<a href="/main/auth/lostPassword.php" id="forgot" class="font-medium text-blue-600 hover:text-blue-500">
Forgot your password?
</a>
</div>
</div>
<div
v-else-if="hasError"
class="row col"
>
<error-message :error="error" />
</div>
<div>
<button
type="submit"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<a href="/main/auth/lostPassword.php" id="forgot">Forgot password?</a>
</form>
</b-col>
<b-col cols="4" />
</b-row>
</b-container>
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
<svg v-if="isLoading"
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<LockClosedIcon v-if="!isLoading" class="h-5 w-5 text-blue-500 group-hover:text-blue-400" aria-hidden="true" />
</span>
Sign in
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ErrorMessage from "../components/ErrorMessage";
import { mapGetters } from 'vuex';
import ErrorMessage from "../components/ErrorMessage.vue";
import { LockClosedIcon } from '@heroicons/vue/solid'
export default {
name: "Login",
components: {
ErrorMessage,
},
data() {
return {
login: "",
password: "",
};
},
computed: {
...mapGetters({
'isLoading': 'security/isLoading',
'hasError': 'security/hasError',
'error': 'security/error',
}),
export default {
name: "Login",
components: {
ErrorMessage,
LockClosedIcon
},
data() {
return {
login: "",
password: "",
};
},
computed: {
...mapGetters({
'isLoading': 'security/isLoading',
'hasError': 'security/hasError',
'error': 'security/error',
}),
},
created() {
let redirect = this.$route.query.redirect;
if (this.$store.getters["security/isAuthenticated"]) {
if (typeof redirect !== "undefined") {
this.$router.push({path: redirect});
} else {
this.$router.push({path: "/courses"});
}
}
},
methods: {
onSubmit(evt) {
evt.preventDefault()
this.performLogin();
},
created() {
async performLogin() {
let payload = {login: this.$data.login, password: this.$data.password};
let redirect = this.$route.query.redirect;
if (this.$store.getters["security/isAuthenticated"]) {
await this.$store.dispatch("security/login", payload);
if (!this.$store.getters["security/hasError"]) {
if (typeof redirect !== "undefined") {
this.$router.push({path: redirect});
} else {
this.$router.push({path: "/courses"});
}
}
},
methods: {
onSubmit(evt) {
evt.preventDefault()
this.performLogin();
},
async performLogin() {
let payload = {login: this.$data.login, password: this.$data.password};
let redirect = this.$route.query.redirect;
await this.$store.dispatch("security/login", payload);
if (!this.$store.getters["security/hasError"]) {
if (typeof redirect !== "undefined") {
this.$router.push({path: redirect});
} else {
this.$router.push({path: "/courses"});
}
}
}
}
}
}
</script>

@ -0,0 +1,274 @@
<template>
<div class="card">
<DataView :value="courses" :layout="layout" :paginator="true" :rows="9" :sortOrder="sortOrder" :sortField="sortField">
<template #header>
<div class="p-grid p-nogutter">
<div class="p-col-3" style="text-align: left">
<Dropdown
v-model="sortKey"
:options="sortOptions"
optionLabel="label"
placeholder="Sort By Title"
@change="onSortChange($event)"
/>
</div>
<!-- <div class="p-col-3" style="text-align: left">-->
<!-- <Dropdown-->
<!-- v-model="sortKey"-->
<!-- :options="sortOptions"-->
<!-- optionLabel="label"-->
<!-- placeholder="Categories"-->
<!-- @change="onSortChange($event)"-->
<!-- />-->
<!-- </div>-->
<!-- <div class="p-col-6" style="text-align: right">-->
<!-- <DataViewLayoutOptions v-model="layout" />-->
<!-- </div>-->
</div>
</template>
<template #list="slotProps">
<div class="p-col-12">
<div class="course-list-item">
<img src="/img/session_default.png" :alt="slotProps.data.title"/>
<div class="course-list-detail">
<div class="course-name">{{ slotProps.data.title }}</div>
<div class="course-description">{{ slotProps.data.description }}</div>
<!-- <Rating :modelValue="slotProps.data.rating" :readonly="true" :cancel="false"></Rating>-->
<span v-for="category in slotProps.data.categories">
<i class="pi pi-tag course-category-icon"></i>
<span class="course-category">{{ category.name }}</span>&nbsp;
</span>
</div>
<div class="course-list-action">
<!-- <span class="course-price">${{slotProps.data.price}}</span>-->
<!-- <Button icon="pi pi-shopping-cart" label="Add to Cart" :disabled="slotProps.data.inventoryStatus === 'OUTOFSTOCK'"></Button>-->
<!-- <span :class="'course-badge status-'+slotProps.data.inventoryStatus.toLowerCase()">{{slotProps.data.inventoryStatus}}</span>-->
</div>
</div>
</div>
</template>
<template #grid="slotProps">
<div class="p-col-12 p-md-4">
<div class="course-grid-item card">
<div class="course-grid-item-top">
<div>
<i class="pi pi-tag course-category-icon"></i>
<span class="course-category">{{ slotProps.data.title }}</span>
</div>
<!-- <span :class="'course-badge status-'+slotProps.data.inventoryStatus.toLowerCase()">{{slotProps.data.inventoryStatus}}</span>-->
</div>
<div class="course-grid-item-content">
<img src="/img/icons/64/course.png" :alt="slotProps.data.title"/>
<div class="course-name">{{ slotProps.data.title }}</div>
<div class="course-description">{{ slotProps.data.description }}</div>
<!-- <Rating :modelValue="slotProps.data.rating" :readonly="true" :cancel="false"></Rating>-->
</div>
<div class="course-grid-item-bottom">
<!-- <span class="course-price">${{slotProps.data.price}}</span>-->
<!-- <Button icon="pi pi-shopping-cart" :disabled="slotProps.data.inventoryStatus === 'OUTOFSTOCK'"></Button>-->
</div>
</div>
</div>
</template>
</DataView>
</div>
</template>
<style lang="scss" scoped>
.card {
background: #ffffff;
padding: 2rem;
box-shadow: 0 2px 1px -1px rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 1px 3px 0 rgba(0,0,0,.12);
border-radius: 4px;
margin-bottom: 2rem;
}
.p-dropdown {
width: 14rem;
font-weight: normal;
}
.course-name {
font-size: 1.5rem;
font-weight: 700;
}
.course-description {
margin: 0 0 1rem 0;
}
.course-category-icon {
vertical-align: middle;
margin-right: .5rem;
}
.course-category {
font-weight: 600;
vertical-align: middle;
}
::v-deep(.course-list-item) {
display: flex;
align-items: center;
padding: 1rem;
width: 100%;
img {
width: 150px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
margin-right: 2rem;
}
.course-list-detail {
flex: 1 1 0;
}
.p-rating {
margin: 0 0 .5rem 0;
}
.course-price {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: .5rem;
align-self: flex-end;
}
.course-list-action {
display: flex;
flex-direction: column;
}
.p-button {
margin-bottom: .5rem;
}
}
::v-deep(.course-grid-item) {
margin: .5rem;
border: 1px solid #dee2e6;
.course-grid-item-top,
.course-grid-item-bottom {
display: flex;
align-items: center;
justify-content: space-between;
}
img {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
margin: 2rem 0;
}
.course-grid-item-content {
text-align: center;
}
.course-price {
font-size: 1.5rem;
font-weight: 600;
}
}
@media screen and (max-width: 576px) {
.course-list-item {
flex-direction: column;
align-items: center;
img {
margin: 2rem 0;
}
.course-list-detail {
text-align: center;
}
.course-price {
align-self: center;
}
.course-list-action {
display: flex;
flex-direction: column;
}
.course-list-action {
margin-top: 2rem;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
}
}
}
</style>
<script>
import {ENTRYPOINT} from '../../config/entrypoint';
import axios from "axios";
import Dropdown from "primevue/dropdown";
import DataView from 'primevue/dataview';
import DataViewLayoutOptions from 'primevue/dataviewlayoutoptions';
export default {
name: 'Catalog',
components: {
DataView,
Dropdown,
DataViewLayoutOptions
},
data() {
return {
status: '',
courses: [],
layout: 'list',
sortKey: null,
sortOrder: null,
sortField: null,
sortOptions: [
{label: 'A-z', value: 'title'},
{label: 'Z-a', value: '!title'},
]
};
},
created: function () {
this.load();
},
mounted: function () {
},
methods: {
load: function () {
//this.status = 'Loading';
//let user = this.$store.getters['security/getUser'];
axios.get(ENTRYPOINT + 'courses.json').then(response => {
this.status = '';
if (Array.isArray(response.data)) {
this.courses = response.data;
}
}).catch(function (error) {
console.log(error);
});
},
onSortChange(event) {
const value = event.value.value;
const sortValue = event.value;
if (value.indexOf('!') === 0) {
this.sortOrder = -1;
this.sortField = value.substring(1, value.length);
this.sortKey = sortValue;
}
else {
this.sortOrder = 1;
this.sortField = value;
this.sortKey = sortValue;
}
}
}
};
</script>

@ -0,0 +1,72 @@
<template>
<div class="card">
Session catalog todo
</div>
</template>
<script>
import {ENTRYPOINT} from '../../config/entrypoint';
import axios from "axios";
import Dropdown from "primevue/dropdown";
import DataView from 'primevue/dataview';
import DataViewLayoutOptions from 'primevue/dataviewlayoutoptions';
export default {
name: 'Catalog',
components: {
DataView,
Dropdown,
DataViewLayoutOptions
},
data() {
return {
status: '',
courses: [],
layout: 'list',
sortKey: null,
sortOrder: null,
sortField: null,
sortOptions: [
{label: 'A-z', value: 'title'},
{label: 'Z-a', value: '!title'},
]
};
},
created: function () {
this.load();
},
mounted: function () {
},
methods: {
load: function () {
//this.status = 'Loading';
//let user = this.$store.getters['security/getUser'];
axios.get(ENTRYPOINT + 'courses.json').then(response => {
this.status = '';
if (Array.isArray(response.data)) {
this.courses = response.data;
}
}).catch(function (error) {
console.log(error);
});
},
onSortChange(event) {
const value = event.value.value;
const sortValue = event.value;
if (value.indexOf('!') === 0) {
this.sortOrder = -1;
this.sortField = value.substring(1, value.length);
this.sortKey = sortValue;
}
else {
this.sortOrder = 1;
this.sortField = value;
this.sortKey = sortValue;
}
}
}
};
</script>

@ -0,0 +1,75 @@
<template>
<div class="grid gap-4">
<q-card-section>
<div class="text-h6">{{ course.title }}</div>
<div class="text-subtitle2">{{ course.description }}</div>
</q-card-section>
<div v-for="categories in tools" class="grid gap-4 grid-cols-2 md:grid-cols-4 lg:grid-cols-6">
<div v-for="tool in categories" class="bg-gray-100 rounded-xl p-4 shadow-md">
<div class="flex flex-col flex-center">
<q-avatar rounded>
<img
:alt="tool.name"
:src="'/img/tools/' + tool.name + '.png'"
/>
</q-avatar>
<q-item-section>
<div class="row no-wrap items-center">
<a
:href="goToCourse(course, tool)"
>
{{ tool.nameToTranslate }}
</a>
</div>
</q-item-section>
</div>
</div>
</div>
</div>
</template>
<script>
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import isEmpty from 'lodash/isEmpty';
import { useRoute } from 'vue-router'
import axios from "axios";
import { ENTRYPOINT } from '../../config/entrypoint';
import { reactive, toRefs} from 'vue'
// @todo use suspense
// @
export default {
name: 'Home',
servicePrefix: 'Courses',
components: {
Loading,
Toolbar
},
setup() {
const state = reactive({course: [], tools: [], shortcuts:[]});
const route = useRoute()
let id = route.params.id;
axios.get(ENTRYPOINT + '../course/' + id + '/home.json').then(response => {
state.course = response.data.course;
state.tools = response.data.tools;
state.shortcuts = response.data.shortcuts;
}).catch(function (error) {
console.log(error);
});
return toRefs(state);
},
methods: {
goToCourse(course, tool) {
let sessionId = this.$route.query.sid ?? 0;
let url = '/course/' + course.id + '/tool/' + tool.name + '?sid=' + sessionId;
return url;
}
}
};
</script>

@ -1,183 +1,213 @@
<template>
<span class="documents-list">
<Toolbar
:handle-add="addHandler"
:handle-add-document="addDocumentHandler"
:handle-upload-document="uploadDocumentHandler"
:filters="filters"
:on-send-filter="onSendFilter"
:reset-filter="resetFilter"
/>
<br>
<b-row class="text-center">
<b-col>
<form class="form-inline">
<div class="form-group mb-2">
<b-form-select
id="perPageSelect"
v-model="options.itemsPerPage"
size="sm"
:options="pageOptions"
@input="onUpdateOptions(options)"
/>
</div>
</form>
</b-col>
<b-col />
<b-col>
<!-- <div v-if="this.selected.length > 0" >-->
<!-- <b-button variant="info" size="sm" @click="deleteSelected">-->
<!-- {{ $t('Info') }}-->
<!-- </b-button>-->
<!-- <b-button variant="secondary" size="sm" @click="deleteSelected">-->
<!-- {{ $t('Edit') }}-->
<!-- </b-button>-->
<!-- <b-button variant="danger" size="sm" @click="deleteSelected">-->
<!-- {{ $t('Delete') }}-->
<!-- </b-button>-->
<!-- </div>-->
<b-pagination
v-model="options.page"
:disabled.sync="isLoading"
:total-rows="totalItems"
:per-page="options.itemsPerPage"
aria-controls="documents"
align="right"
size="sm"
@input="onUpdateOptions(options)"
/>
</b-col>
</b-row>
<b-row>
<b-col>
<b-table
id="documents"
ref="selectableTable"
striped
hover
no-local-sorting
responsive="sm"
:per-page="0"
:fields="fields"
:items="items"
@row-selected="onRowSelected"
selectable
small
:current-page.sync="options.page"
:sort-by.sync="options.sortBy"
:sort-desc.sync="options.sortDesc"
:busy.sync="isLoading"
:filters="filters"
primary-key="iid"
@sort-changed="sortingChanged"
>
<template v-slot:table-busy>
<div class="text-center my-2">
<b-spinner class="align-middle" />
<strong>{{ $t('Loading ...') }}</strong>
</div>
</template>
<!-- <template v-slot:cell(selected)="{ rowSelected }">-->
<!-- <template v-if="rowSelected">-->
<!-- <span aria-hidden="true">&check;</span>-->
<!-- <span class="sr-only">{{ $t('Selected') }}</span>-->
<!-- </template>-->
<!-- <template v-else>-->
<!-- <span aria-hidden="true">&nbsp;</span>-->
<!-- <span class="sr-only">{{ $t('Not selected') }}</span>-->
<!-- </template>-->
<div>
<!-- <Toolbar-->
<!-- :handle-add="addHandler"-->
<!-- :handle-add-document="addDocumentHandler"-->
<!-- :handle-upload-document="uploadDocumentHandler"-->
<!-- :filters="filters"-->
<!-- :on-send-filter="onSendFilter"-->
<!-- :reset-filter="resetFilter"-->
<!-- />-->
<!-- <q-table-->
<!-- id="documents"-->
<!-- ref="selectableTable"-->
<!-- title="Documents"-->
<!-- :rows="items"-->
<!-- :columns="fields"-->
<!-- row-key="iid"-->
<!-- />-->
<!-- :pagination.sync="pagination"-->
<!-- @request="onRequest"-->
<!--:loading="isLoading"-->
<!-- <q-table-->
<!-- :rows="items"-->
<!-- :columns="columns"-->
<!-- row-key="iid"-->
<!-- @request="onRequest"-->
<!-- :no-data-label="$t('Data unavailable')"-->
<!-- :no-results-label="$t('No results')"-->
<!-- :loading-label="$t('Loading...')"-->
<!-- :rows-per-page-label="$t('Records per page:')"-->
<!-- :loading="isLoading"-->
<!-- >-->
<!-- { label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},-->
<!-- { label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', name: 'updatedAt', sortable: true},-->
<!-- { label: this.$i18n.t('Size'), field: 'resourceNode.resourceFile.size', name: 'size', sortable: true},-->
<!-- { label: this.$i18n.t('Actions'), name: 'action', sortable: false}-->
<Toolbar class="p-mb-4">
<template #left>
<Button label="New" icon="pi pi-plus" class="p-button-primary p-button-sm p-mr-2" @click="openNew" />
<!-- <Button label="New folder" icon="pi pi-plus" class="p-button-success p-mr-2" @click="addHandler()" />-->
<Button label="New document" icon="pi pi-plus" class="p-button-sm p-button-primary p-mr-2" @click="addDocumentHandler()" />
<Button label="Upload" icon="pi pi-plus" class="p-button-sm p-button-primary p-mr-2" @click="uploadDocumentHandler()" />
<Button label="Delete" icon="pi pi-trash" class="p-button-sm p-button-danger" @click="confirmDeleteSelected" :disabled="!selectedItems || !selectedItems.length" />
</template>
<!-- <template #right>-->
<!-- <FileUpload mode="basic" accept="image/*" :maxFileSize="1000000" label="Import" chooseLabel="Import" class="p-mr-2 p-d-inline-block" />-->
<!-- <Button label="Export" icon="pi pi-upload" class="p-button-help" @click="exportCSV($event)" />-->
<!-- </template>-->
</Toolbar>
<DataTable
:value="items"
v-model:selection="selectedItems"
dataKey="iid"
:filters="filters"
:lazy="true"
:paginator="true"
:rows="10"
:totalRecords="totalItems"
:loading="isLoading"
@page="onPage($event)"
@sort="sortingChanged($event)"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
:rowsPerPageOptions="[5, 10, 20, 50]"
responsiveLayout="scroll"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
>
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
<Column field="resourceNode.title" :header="$t('Title')" :sortable="true"></Column>
<Column field="resourceNode.updatedAt" :header="$t('Modified')" :sortable="true">
<template #body="slotProps">
{{$luxonDateTime.fromISO(slotProps.data.resourceNode.updatedAt).toRelative() }}
</template>
</Column>
<Column field="resourceNode.resourceFile.size" :header="$t('Size')" :sortable="true">
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile ? $filters.prettyBytes(slotProps.data.resourceNode.resourceFile.size) : ''
}}
</template>
</Column>
<Column :exportable="false">
<template #body="slotProps">
<Button label="Show" class="p-button-sm p-button p-button-success p-mr-2" @click="showHandler(slotProps.data)" />
<Button label="Edit" icon="pi pi-pencil" class="p-button-sm p-button p-button-success p-mr-2" @click="editHandler(slotProps.data)" />
<Button label="Delete" icon="pi pi-trash" class="p-button-sm p-button p-button-danger" @click="confirmDeleteSelected(slotProps.data)" />
</template>
</Column>
<template #paginatorLeft>
<Button type="button" icon="pi pi-refresh" class="p-button-text" />
</template>
<template #paginatorRight>
<Button type="button" icon="pi pi-cloud" class="p-button-text" />
</template>
</DataTable>
<Dialog v-model:visible="itemDialog" :style="{width: '450px'}" :header="$t('New folder')" :modal="true" class="p-fluid">
<div class="p-field">
<label for="name">{{ $t('Name') }}</label>
<InputText
autocomplete="off"
id="title"
v-model.trim="item.title"
required="true"
autofocus
:class="{'p-invalid': submitted && !item.title}"
/>
<small class="p-error" v-if="submitted && !item.title">$t('Title is required')</small>
</div>
<template #footer>
<Button label="Cancel" icon="pi pi-times" class="p-button-text" @click="hideDialog"/>
<Button label="Save" icon="pi pi-check" class="p-button-text" @click="saveItem" />
</template>
</Dialog>
<Dialog v-model:visible="deleteItemDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem" />
<span v-if="item">Are you sure you want to delete <b>{{item.title}}</b>?</span>
</div>
<template #footer>
<Button label="No" icon="pi pi-times" class="p-button-text" @click="deleteItemDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteItem" />
</template>
</Dialog>
<Dialog v-model:visible="deleteItemsDialog" :style="{width: '450px'}" header="Confirm" :modal="true">
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem" />
<span v-if="item">Are you sure you want to delete the selected products?</span>
</div>
<template #footer>
<Button label="No" icon="pi pi-times" class="p-button-text" @click="deleteItemsDialog = false"/>
<Button label="Yes" icon="pi pi-check" class="p-button-text" @click="deleteSelectedItems" />
</template>
</Dialog>
<!-- <template v-slot:body="props">-->
<!-- <q-tr :props="props">-->
<!-- <q-td key="title" :props="props">-->
<!-- {{ props.row.title }}-->
<!-- </q-td>-->
<!-- <q-td key="resourceNode.updatedAt" :props="props">-->
<!-- {{ props.row.resourceNode.updatedAt }}-->
<!-- </q-td>-->
<!-- <q-td key="resourceNode.resourceFile.size" :props="props">-->
<!-- {{ props.row.resourceNode.resourceFile.size }}-->
<!-- </q-td>-->
<!-- </q-tr>-->
<!-- </template>-->
<template
v-slot:cell(resourceNode.title)="row"
>
<div v-if="row.item['resourceNode']['resourceFile']">
<a
data-fancybox="gallery"
:href="row.item['contentUrl'] "
>
<ResourceFileIcon :file="row.item['resourceNode']['resourceFile']" />
{{ row.item['resourceNode']['title'] }}
</a>
</div>
<div v-else>
<a @click="handleClick(row.item)">
<font-awesome-icon icon="folder" />
{{ row.item['resourceNode']['title'] }}
</a>
</div>
</template>
<template
v-slot:cell(resourceNode.updatedAt)="row"
>
{{ row.item.resourceNode.updatedAt | moment("from", "now") }}
</template>
<template
v-slot:cell(resourceNode.resourceFile.size)="row"
>
<span
v-if="row.item['resourceNode']['resourceFile']"
>
{{ row.item.resourceNode.resourceFile.size | prettyBytes }}
</span>
</template>
<template
v-if="isAuthenticated && (isCurrentTeacher || isAdmin)"
v-slot:cell(action)="row"
>
<ActionCell
slot="action"
:row="row"
:handle-show="() => showHandler(row.item)"
:handle-edit="() => editHandler(row.item)"
:handle-delete="() => deleteHandler(row.item)"
/>
</template>
<template
v-else
v-slot:cell(action)="row"
>
<ActionCell
slot="action"
:row="row"
:handle-show="() => showHandler(row.item)"
/>
</template>
</b-table>
<p>
<b-button size="sm" @click="selectAllRows">{{ $t('Select all') }}</b-button>
<b-button size="sm" @click="clearSelected">{{ $t('Clear selected') }}</b-button>
<b-button v-if="this.selected.length > 0" variant="danger" size="sm" @click="deleteSelected">
{{ $t('Delete') }}
</b-button>
</p>
</b-col>
</b-row>
</span>
<!-- <template v-slot:body-cell-updatedAt="props">-->
<!-- <q-td slot="body-cell-updatedAt" auto-width>-->
<!-- {{-->
<!-- moment(props.row.resourceNode.updatedAt).fromNow()-->
<!-- }}-->
<!-- </q-td>-->
<!-- </template>-->
<!-- <template v-slot:body-cell-size="props">-->
<!-- <q-td slot="body-cell-updatedAt" auto-width>-->
<!-- <span v-if="props.row.resourceNode.resourceFile">-->
<!-- {{ $filters.prettyBytes(props.row.resourceNode.resourceFile.size) }}-->
<!-- </span>-->
<!-- </q-td>-->
<!-- </template>-->
<!-- <template v-slot:body-cell-action="props">-->
<!-- <ActionCell-->
<!-- slot="body-cell-action"-->
<!-- slot-scope="props"-->
<!-- :handle-show="() => showHandler(props.row)"-->
<!-- :handle-edit="() => editHandler(props.row)"-->
<!-- :handle-delete="() => deleteHandler(props.row)"-->
<!-- />-->
<!-- </template>-->
<!-- </q-table>-->
</div>
</template>
<script>
//import { list } from '../../utils/vuexer';
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ListMixin from '../../mixins/ListMixin';
import ActionCell from '../../components/ActionCell';
import Toolbar from '../../components/Toolbar';
import ResourceFileIcon from './ResourceFileIcon';
import ActionCell from '../../components/ActionCell.vue';
import Toolbar from '../../components/Toolbar.vue';
import ResourceFileIcon from './ResourceFileIcon.vue';
import { useRoute } from 'vue-router'
import moment from 'moment'
//import { useToast } from 'primevue/usetoast';
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
/*const servicePrefix = 'documents';
const { getters, actions } = list(servicePrefix);*/
import isEmpty from 'lodash/isEmpty';
export default {
name: 'DocumentsList',
@ -192,21 +222,38 @@ export default {
return {
sortBy: 'title',
sortDesc: false,
fields: [
{label: this.$i18n.t('Title'), key: 'resourceNode.title', sortable: true},
{label: this.$i18n.t('Modified'), key: 'resourceNode.updatedAt', sortable: true},
{label: this.$i18n.t('Size'), key: 'resourceNode.resourceFile.size', sortable: true},
{label: this.$i18n.t('Actions'), key: 'action', sortable: false}
columns: [
//{ name: 'action' },
//{ name: 'id', field: '@id', label: this.$t('iid') },
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', name: 'updatedAt', sortable: true},
{ label: this.$i18n.t('Size'), field: 'resourceNode.resourceFile.size', name: 'size', sortable: true},
{ label: this.$i18n.t('Actions'), name: 'action', sortable: false}
],
pageOptions: [5, 10, 15, 20, this.$i18n.t('All')],
selected: [],
selectMode: 'multi',
isBusy: false
isBusy: false,
options: [],
// prime vue
itemDialog: false,
deleteItemDialog: false,
deleteItemsDialog: false,
item: {},
selectedItems: null,
filters: {},
submitted: false,
};
},
created() {
let nodeId = this.$route.params['node'];
this.findResourceNode('/api/resource_nodes/' + nodeId);
console.log('vue/views/documents/List.vue');
this.moment = moment;
const route = useRoute()
let nodeId = route.params['node'];
if (!isEmpty(nodeId)) {
this.findResourceNode('/api/resource_nodes/' + nodeId);
}
this.options.getPage = this.getPage;
this.onUpdateOptions(this.options);
},
mounted() {
@ -218,13 +265,10 @@ export default {
this.onScroll();
}
});*/
//const tableScrollBody = this.$refs['selectableTable'].$el;
/* Consider debouncing the event call */
//tableScrollBody.addEventListener("scroll", this.onScroll);
//window.addEventListener('scroll', this.onScroll)
window.addEventListener('scroll', () =>{
/*if(window.top.scrollY > window.outerHeight){
if (!this.isBusy) {
@ -244,12 +288,13 @@ export default {
resourceNode: 'getResourceNode'
}),
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'isAdmin': 'security/isAdmin',
'isCurrentTeacher': 'security/isCurrentTeacher',
}),
//...getters
// From ListMixin
...mapFields('documents', {
deletedItem: 'deleted',
@ -259,9 +304,96 @@ export default {
totalItems: 'totalItems',
view: 'view'
}),
},
methods: {
onPage(event) {
console.log(event);
console.log(event.page);
console.log(event.sortField);
console.log(event.sortOrder);
this.options.itemsPerPage = event.rows;
this.options.page = event.page + 1;
this.options.sortBy = event.sortField;
this.options.sortDesc = event.sortOrder === -1;
this.onUpdateOptions(this.options);
},
openNew() {
this.item = {};
this.submitted = false;
this.itemDialog = true;
},
hideDialog() {
this.itemDialog = false;
this.submitted = false;
},
saveItem() {
this.submitted = true;
if (this.item.title.trim()) {
if (this.item.id) {
//this.products[this.findIndexById(this.product.id)] = this.product;
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Product Updated', life: 3000});
} else {
//this.products.push(this.product);
this.item.parentResourceNodeId = this.$route.params.node;
this.item.resourceLinkList = JSON.stringify([{
gid: this.$route.query.gid,
sid: this.$route.query.sid,
c_id: this.$route.query.cid,
visibility: 2, // visible by default
}]);
this.create(this.item);
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Product Created', life: 3000});
this.showMessage('Saved');
}
this.itemDialog = false;
this.item = {};
}
},
editItem(product) {
this.product = {...product};
this.itemDialog = true;
},
confirmDeleteItem(product) {
this.product = product;
this.deleteProductDialog = true;
},
deleteItem() {
console.log('deleteItem');
this.deleteItemAction(this.item);
this.items = this.items.filter(val => val.iid !== this.item.iid);
this.deleteProductDialog = false;
this.item = {};
},
findIndexById(id) {
let index = -1;
for (let i = 0; i < this.products.length; i++) {
if (this.products[i].id === id) {
index = i;
break;
}
}
return index;
},
exportCSV() {
this.$refs.dt.exportCSV();
},
confirmDeleteSelected() {
this.deleteItemsDialog = true;
},
deleteSelectedItems() {
this.deleteMultipleAction(this.selectedItems);
this.onUpdateOptions(this.options);
/*this.products = this.products.filter(val => !this.selectedProducts.includes(val));*/
this.deleteItemsDialog = false;
this.selectedItems = null;
//this.$toast.add({severity:'success', summary: 'Successful', detail: 'Products Deleted', life: 3000});*/
},
async fetchItems() {
console.log('fetchItems');
/* No need to call if all items retrieved */
@ -314,7 +446,7 @@ export default {
this.deleteItem(item);
}*/
this.deleteMultipleItem(this.selected);
this.deleteMultipleAction(this.selected);
this.onUpdateOptions(this.options);
/*const promises = this.selected.map(async item => {
@ -332,21 +464,24 @@ export default {
//this.onUpdateOptions(this.options);
}
*/
console.log('end -- deleteSelected');
},
sortingChanged(ctx) {
this.options.sortDesc = ctx.sortDesc;
this.options.sortBy = ctx.sortBy;
sortingChanged(event) {
console.log('sortingChanged');
this.options.sortBy = event.sortField;
this.options.sortDesc = event.sortOrder === -1;
this.onUpdateOptions(this.options);
// ctx.sortBy ==> Field key for sorting by (or null for no sorting)
// ctx.sortDesc ==> true if sorting descending, false otherwise
},
//...actions,
// From ListMixin
...mapActions('documents', {
getPage: 'fetchAll',
deleteItem: 'del',
deleteMultipleItem: 'delMultiple'
create: 'create',
deleteItemAction: 'del',
deleteMultipleAction: 'delMultiple'
}),
...mapActions('resourcenode', {
findResourceNode: 'findResourceNode',

@ -13,14 +13,13 @@
<!-- </v-toolbar-title>-->
</template>
</Toolbar>
<br />
<div
v-if="item"
class="table-documents-show"
>
<h2>
<h5>
{{ item['title'] }}
</h2>
</h5>
<div v-if="item['resourceLinkListFromEntity']">
<ul>
<li
@ -39,8 +38,7 @@
</li>
</ul>
</div>
<b-table-simple>
<template slot="default">
<q-markup-table>
<tbody>
<tr>
<td><strong>{{ $t('Author') }}</strong></td>
@ -59,14 +57,14 @@
<tr>
<td><strong>{{ $t('Created at') }}</strong></td>
<td>
{{ item['resourceNode'] && item['resourceNode'].createdAt | moment("from", "now") }}
{{ item['resourceNode'] ? moment(item['resourceNode'].createdAt).fromNow() : ''}}
</td>
<td />
</tr>
<tr>
<td><strong>{{ $t('Updated at') }}</strong></td>
<td>
{{ item['resourceNode'] && item['resourceNode'].updatedAt | moment("from", "now") }}
{{ item['resourceNode'] ? moment(item['resourceNode'].updatedAt).fromNow() : ''}}
</td>
<td />
</tr>
@ -74,7 +72,7 @@
<td><strong>{{ $t('File') }}</strong></td>
<td>
<div>
<b-img
<q-img
v-if="item['resourceNode']['resourceFile']['image']"
:src="item['contentUrl'] + '?w=300'"
/>
@ -84,20 +82,19 @@
</video>
</span>
<span v-else>
<b-btn
<q-btn
variant="primary"
:href="item['downloadUrl']"
>
{{ $t('Download file') }}
</b-btn>
</q-btn>
</span>
</div>
</td>
<td />
</tr>
</tbody>
</template>
</b-table-simple>
</q-markup-table>
</div>
<Loading :visible="isLoading" />
</div>
@ -106,10 +103,10 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import Loading from '../../components/Loading';
import Loading from '../../components/Loading.vue';
import ShowMixin from '../../mixins/ShowMixin';
import Toolbar from '../../components/Toolbar';
import Toolbar from '../../components/Toolbar.vue';
import moment from 'moment'
const servicePrefix = 'Documents';
export default {
@ -119,6 +116,9 @@ export default {
Loading,
Toolbar
},
created: function () {
this.moment = moment;
},
mixins: [ShowMixin],
computed: {
...mapFields('documents', {

@ -6,7 +6,9 @@
:parentResourceNodeId="parentResourceNodeId"
:resourceLinkList="resourceLinkList"
:errors="violations"
:process-files="processFiles"
/>
<Toolbar
:handle-submit="onUploadForm"
/>
@ -17,10 +19,11 @@
<script>
import { mapActions } from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import DocumentsForm from '../../components/documents/FormUpload';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import DocumentsForm from '../../components/documents/FormUpload.vue';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import UploadMixin from '../../mixins/UploadMixin';
import { ref, onMounted } from 'vue'
const servicePrefix = 'Documents';
@ -37,6 +40,13 @@ export default {
Toolbar,
DocumentsForm
},
setup() {
const createForm = ref(null);
return {
createForm
}
},
mixins: [UploadMixin],
data() {
return {
@ -60,7 +70,57 @@ export default {
this.files = [];
},
methods: {
...mapActions('documents', ['uploadMany', 'create'])
async processFiles(files) {
/*this.files = [
...this.files,
...map(files, file => ({
title: file.name,
name: file.name,
size: file.size,
type: file.type,
filetype: 'file',
parentResourceNodeId: this.parentResourceNodeId,
resourceLinkList: this.resourceLinkList,
uploadFile: file,
invalidMessage: this.validate(file),
}))
];*/
return new Promise((resolve) => {
for (let i = 0; i < files.length; i++) {
files[i].title = files[i].name;
files[i].parentResourceNodeId = this.parentResourceNodeId;
files[i].resourceLinkList = this.resourceLinkList;
files[i].uploadFile = files[i];
this.createFile(files[i]);
}
resolve(files);
/*console.log(file);
file.title = file.name;
file.parentResourceNodeId = this.parentResourceNodeId;
file.resourceLinkList = this.resourceLinkList;
file.uploadFile = file;
this.create(file);
resolve(file);*/
/*for (let i = 0; i < this.files.length; i++) {
this.create(this.files[i]);
}
resolve(true);*/
}).then(() => {
this.files = [];
});
},
validate(file) {
if (file) {
return '';
}
return 'error';
},
...mapActions('documents', ['uploadMany', 'create', 'createFile'])
}
};
</script>

@ -1,30 +1,50 @@
<template>
<b-row no-gutters>
<b-col md="2">
<b-card-img
src="/img/icons/64/course.png"
alt="Image"
img-left
class="rounded-0"
/>
</b-col>
<b-col md="10">
<b-card-body :title="course.title">
<!-- <b-card-text>-->
<!-- Course description-->
<!-- </b-card-text>-->
<b-button
:href=" '/course/' + course.id + '/home'"
variant="primary"
>
Go
</b-button>
</b-card-body>
</b-col>
</b-row>
<!-- <q-card class="my-card">-->
<!-- <q-img src="https://cdn.quasar.dev/img/parallax2.jpg">-->
<!-- <div class="absolute-bottom text-h6">-->
<!-- Title-->
<!-- </div>-->
<!-- </q-img>-->
<!-- <q-card-section>-->
<!-- {{ lorem }}-->
<!-- </q-card-section>-->
<!-- </q-card>-->
<q-card class="my-card">
<img src="/img/session_default.png" />
<q-card-section>
<div class="text-h7">
<router-link :to="{ name: 'CourseHome', params: {id: course.id, course: course}}">
{{ course.title }}
</router-link>
</div>
</q-card-section>
<q-card-section class="q-pt-none">
{{ course.description }}
</q-card-section>
<!-- <q-card-actions>-->
<!-- <q-btn-->
<!-- type="a"-->
<!-- :to="{ name: 'CourseHome', params: {id: course.id, course: course}}"-->
<!-- text-color="white"-->
<!-- color="primary"-->
<!-- label="Go"-->
<!-- />-->
<!-- </q-card-actions>-->
</q-card>
</template>
<style scoped>
.my-card {
width: 100%;
max-width: 370px;
}
</style>
<script>
export default {
name: 'CourseCard',
props: {

@ -1,58 +1,17 @@
<template>
<span>
<b-button-group>
<b-button
rounded
:variant="isList()"
@click="changeLayout"
>
<font-awesome-icon icon="bars" />
</b-button>
<b-button
:variant="isDeck()"
rounded
@click="changeLayout"
>
<font-awesome-icon icon="th-large" />
</b-button>
</b-button-group>
<b-card-group
v-if="deck"
columns
<div
v-for="card in courses"
:key="card.course.id"
>
<b-card
v-for="card in courses"
:key="card.course.id"
no-body
class="overflow-hidden"
style="max-width: 540px;"
>
<CourseCard
:course="card.course"
/>
</b-card>
</b-card-group>
<span v-else>
<b-card
v-for="card in courses"
:key="card.course.id"
no-body
class="overflow-hidden"
style="max-width: 540px;"
>
<CourseCard
<CourseCard
:course="card.course"
/>
</b-card>
<span />
</span>
</span>
/>
</div>
</template>
<script>
import CourseCard from './CourseCard';
import CourseCard from './CourseCard.vue';
export default {
name: 'CourseCardList',
components: {

@ -1,34 +1,37 @@
<template>
<div class="course-list">
{{ status }}
<CourseCardList
:courses="courses"
/>
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-2">
{{ status }}
<CourseCardList
:courses="courses"
/>
</div>
</template>
<script>
import CourseCardList from './CourseCardList';
import ListMixin from '../../../mixins/ListMixin';
import CourseCardList from './CourseCardList.vue';
import {ENTRYPOINT} from '../../../config/entrypoint';
import axios from "axios";
export default {
name: 'CourseList',
servicePrefix: 'Course',
components: {
CourseCardList
CourseCardList,
},
mixins: [ListMixin],
data() {
return {
status: '',
courses: []
courses: [],
layout: 'list',
sortKey: null,
sortOrder: null,
sortField: null,
};
},
created: function () {
this.load();
},
mounted: function () {
},
methods: {
load: function () {
this.status = 'Loading';
@ -45,7 +48,7 @@ export default {
} else {
this.status = '';
}
}
},
}
};
</script>

@ -0,0 +1,47 @@
<template>
<div class="card">
<h6>Hello {{ currentUser.username }}</h6>
<q-tabs align="left" dense inline-label no-caps>
<q-route-tab to="/courses" label="Inbox" />
<q-route-tab to="/courses" label="Posts" />
<q-route-tab to="/courses" label="Friends" />
<q-route-tab to="/" label="Posts" />
<q-route-tab to="/sessions" label="My files" />
</q-tabs>
</div>
</template>
<script>
import { useRoute } from 'vue-router'
import axios from "axios";
import { ENTRYPOINT } from '../../../config/entrypoint';
import { reactive, toRefs} from 'vue'
import {mapGetters} from "vuex";
export default {
name: 'Home',
components: {
},
setup() {
const state = reactive({user: []});
const route = useRoute()
//let id = route.params.id;
// axios.get(ENTRYPOINT + '../user/' + id + '.json').then(response => {
// state.user = response.data.user;
// }).catch(function (error) {
// console.log(error);
// });
return toRefs(state);
},
computed: {
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'currentUser': 'security/getUser',
}),
},
methods: {
},
};
</script>

@ -1,20 +1,17 @@
<template>
<div class="course-list">
<div class="grid">
{{ status }}
<SessionCardList :sessions="sessions"></SessionCardList>
<SessionCardList :sessions="sessions"/>
</div>
</template>
<script>
import SessionCardList from './SessionCardList';
import ListMixin from '../../../mixins/ListMixin';
import SessionCardList from './SessionCardList.vue';
import { ENTRYPOINT } from '../../../config/entrypoint';
import axios from "axios";
export default {
name: 'SessionList',
servicePrefix: 'Course',
mixins: [ListMixin],
components: {
SessionCardList
},

@ -1,39 +1,32 @@
<template>
<b-row no-gutters>
<b-col md="2">
<b-card-img
src="/img/icons/64/session.png"
alt="Image"
class="mb-3"
img-left
/>
</b-col>
<b-col md="10">
<b-card-body :title="sessionRelUser.session.name">
<b-card
v-for="course in sessionRelUser.courses"
:key="course.id"
no-body
class="overflow-hidden"
style="max-width: 540px;"
>
<b-card-body :title="course.course.title">
<!-- <b-card-text>-->
<!-- Course description-->
<!-- </b-card-text>-->
<b-button
:href=" '/course/' + course.course.id + '/home' + '?sid=' + sessionRelUser.session.id"
variant="primary"
>
Go
</b-button>
</b-card-body>
</b-card>
</b-card-body>
</b-col>
</b-row>
<div class="text-h6 mt-4">{{ sessionRelUser.session.name }}</div>
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
<div
v-for="course in sessionRelUser.courses"
:key="course.id"
no-body
style="max-width: 540px;"
>
<q-card class="my-card">
<img src="/img/session_default.png" />
<q-card-section>
<div class="text-h7">
<!-- <router-link :to="'/course/' + course.course.id + '/home' + '?sid=' + sessionRelUser.session.id">-->
<a :href="'/course/' + course.course.id + '/home' + '?sid=' + sessionRelUser.session.id">
{{ course.course.title }}
</a>
</div>
</q-card-section>
</q-card>
</div>
</div>
</template>
<style scoped>
.my-card {
width: 100%;
max-width: 370px;
}
</style>
<script>
export default {
name: 'SessionCard',

@ -1,58 +1,17 @@
<template>
<span>
<b-button-group>
<b-button
rounded
:variant="isList()"
@click="changeLayout"
>
<font-awesome-icon icon="bars" />
</b-button>
<b-button
:variant="isDeck()"
rounded
@click="changeLayout"
>
<font-awesome-icon icon="th-large" />
</b-button>
</b-button-group>
<b-card-group
v-if="deck"
columns
>
<b-card
<div
v-for="card in sessions"
:key="card.session.id"
no-body
class="overflow-hidden"
style="max-width: 540px;"
>
<SessionCard
:session="card"
/>
</b-card>
</b-card-group>
<span v-else>
<b-card
v-for="card in sessions"
:key="card.session.id"
no-body
class="overflow-hidden"
style="max-width: 540px;"
>
<SessionCard
>
<SessionCard
:sessionRelUser="card"
/>
</b-card>
<span />
</span>
</span>
/>
</div>
</template>
<script>
import SessionCard from './SessionCard';
import SessionCard from './SessionCard.vue';
export default {
name: 'SessionCardList',
components: {

@ -2,15 +2,27 @@
"name": "chamilo",
"version": "2.0.0",
"description": "E-learning and collaboration software",
"license": "GPL-3.0",
"scripts": {
"start": "vue-cli-service serve --open"
},
"dependencies": {
"@babel/plugin-transform-runtime": "^7.9.6",
"@fancyapps/fancybox": "^3.5.7",
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
"@fortawesome/vue-fontawesome": "^2.0",
"@tinymce/tinymce-vue": "^3.0",
"@fortawesome/vue-fontawesome": "prerelease",
"@headlessui/vue": "^1.0.0",
"@heroicons/vue": "^1.0.1",
"@popperjs/core": "^2.9.2",
"@quasar/extras": "latest",
"@tailwindcss/forms": "^0.3.2",
"@tailwindcss/ui": "^0.7.2",
"@tinymce/tinymce-vue": "^4.0",
"@types/lodash": "^4.14.168",
"@vue/cli": "^4.5",
"@vuelidate/core": "^2.0.0-alpha.15",
"@vuelidate/validators": "^2.0.0-alpha.13",
"autoprefixer": "latest",
"axios": "^0.21",
"babel": "^6.23.0",
"babel-plugin-transform-builtin-extend": "^1.1.2",
@ -20,26 +32,26 @@
"blueimp-load-image": "^5.14.0",
"bootstrap-daterangepicker": "^3.0",
"bootstrap-select": "^1.13.17",
"bootstrap-vue": "^2.20",
"chart.js": "^2.9.3",
"chart.js": "^3.0",
"ckeditor4": "^4.16",
"create-nuxt-app": "^3.5.2",
"cropper": "^4.0",
"datepair.js": "^0.4.16",
"dotenv": "^8.2.0",
"dropzone": "^5.5.1",
"dropzone": "^5.8",
"easy-pie-chart": "^2.1.7",
"easytimer": "^1.1.1",
"easytimer.js": "^1.1.1",
"eslint": "^7.20.0",
"eslint-plugin-vue": "^7.6.0",
"eslint-plugin-vue": "^7.8",
"file-loader": "^6.0",
"flag-icon-css": "^3.0",
"fullcalendar": "^5.5.1",
"flag-icon-css": "^3.5",
"full-icu": "^1.3.4",
"fullcalendar": "^5.6",
"glob-all": "^3.2.1",
"highlight.js": "^10.0",
"hljs": "^6.2",
"html2canvas": "^1.0.0-rc.7",
"image-map-resizer": "^1.0.10",
"jquery": "^3.5.0",
"jquery": "^3.6.0",
"jquery-sortablejs": "^1.0.1",
"jquery-ui": "^1.12.1",
"jquery-ui-timepicker-addon": "^1.6.3",
@ -48,68 +60,84 @@
"jspdf": "^2.3",
"jsplumb": "^2.12",
"linkifyjs": "^2.1",
"luxon": "^1.26.0",
"mathjax": "^2.7",
"mediaelement": "^4.2",
"mediaelement-plugins": "https://github.com/chamilo/mediaelement-plugins",
"moment": "^2.25.3",
"mini-css-extract-plugin": "1.3.9",
"multiselect-two-sides": "^2.5.5",
"mxgraph": "^4.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"path": "^0.12.7",
"perfect-scrollbar": "^1.4",
"portal-vue": "^2.1.7",
"postcss": "^8.2.10",
"postcss-loader": "^5.2.0",
"postcss-prefix-selector": "^1.9.0",
"pretty-bytes": "^5.6",
"pretty-checkbox": "^3.0.3",
"primeflex": "^2.0.0",
"primeicons": "^4.1.0",
"primevue": "^3.3.5",
"purgecss-webpack-plugin": "^4.0.3",
"pwstrength-bootstrap": "^3.0",
"qtip2": "^3.0.3",
"quasar": "next",
"readmore-js": "^2.2.1",
"router": "^1.3.5",
"select2": "^4.0",
"signature_pad": "^3.0.0-beta.4",
"sortablejs": "^1.13.0",
"sweetalert2": "^10.0",
"tailwindcss": "latest",
"tempusdominus-bootstrap-4": "^5.39.0",
"tempusdominus-core": "^5.19.0",
"textcomplete": "^0.18.1",
"timeago": "^1.6.7",
"timepicker": "^1.13",
"tinymce": "^5.6",
"tinymce": "^5.7",
"ts-loader": "^8.1.0",
"video.js": "^7.11.4",
"vue": "^2.6.12",
"vue-flatpickr-component": "^8.0",
"vue-i18n": "^8.17.4",
"vue-loader": "^15.7.0",
"vue-moment": "^4.1.0",
"vue": "^3.0",
"vue-eslint-parser": "^7.6.0",
"vue-flatpickr-component": "^9.0",
"vue-i18n": "^9.0.0",
"vue-loader": "^16.0",
"vue-perfect-scrollbar": "^0.2.1",
"vue-router": "^3.5",
"vue-router": "^4.0",
"vue-sidebar-menu": "^4.7.1",
"vue-template-compiler": "^2.6.10",
"vue-toastification": "^1.7.7",
"vuelidate": "^0.7.5",
"vuex": "^3.6",
"vue-toastification": "^2.0.0-rc.1",
"vuetify": "3.0.0-alpha.2",
"vuex": "^4.0.0",
"vuex-composition-helpers": "next",
"vuex-map-fields": "^1.4.0",
"vuex-persistedstate": "^3.0.1",
"vuex-persistedstate": "4.0.0-beta.3",
"webcamjs": "^1.0",
"webpack-jquery-ui": "^2.0.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.0",
"@fortawesome/fontawesome-free": "^5.11",
"@symfony/webpack-encore": "^0.33",
"@mdi/font": "^5.9.55",
"@symfony/webpack-encore": "^1.1.1",
"@vue/cli-plugin-babel": "~4.5",
"@vue/cli-plugin-eslint": "~4.5",
"@vue/cli-service": "~4.5",
"babel-preset-react": "^6.24.1",
"bootstrap": "^4.5",
"copy-webpack-plugin": "^6.0",
"@vue/compiler-sfc": "^3.0.10",
"bootstrap": "^5.0.0-beta3",
"copy-webpack-plugin": "^8.0",
"deepmerge": "^4.2.2",
"fibers": "^5.0.0",
"filemanager-webpack-plugin": "^3.0",
"filemanager-webpack-plugin": "^4.0",
"free-jqgrid": "https://github.com/chamilo/jqGrid.git#bs4",
"node-sass": "^4.0",
"node-sass": "^5.0",
"popper.js": "^1.14.7",
"sass": "^1.26.5",
"sass-loader": "^9.0",
"postcss-rtl": "^1.2.3",
"sass": "^1.29.0",
"sass-loader": "^10.1",
"typescript": "^3.8.3",
"vue-cli-plugin-quasar": "next",
"vue-cli-plugin-vuetify": "~2.3.1",
"webpack-notifier": "^1.13"
}
},
"license": "GPL-3.0"
}

@ -781,6 +781,8 @@ $poweredBy = 'Powered by <a href="http://www.chamilo.org" target="_blank"> Chami
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="../../build/css/app.css">
<link rel="stylesheet" href="../../build/vue.css">
<link rel="stylesheet" href="../../build/css/bootstrap.css">
<script type="text/javascript" src="../../../build/runtime.js"></script>
<script type="text/javascript" src="../../../build/app.js"></script>
<script>
@ -864,67 +866,63 @@ $poweredBy = 'Powered by <a href="http://www.chamilo.org" target="_blank"> Chami
}
</script>
</head>
<body class="bg-chamilo bg-install container">
<div class="row justify-content-md-center">
<div class="col col-md-12">
<div class="install-box">
<div class="row">
<div class="col-md-4">
<div class="logo-install">
<img src="../../build/css/themes/chamilo/images/header-logo.png"
class="img-fluid" alt="Chamilo" />
</div>
<div class="install-steps">
<ol class="list-group">
<li class="list-group-item <?php step_active('1'); ?>">
<span class="number"> 1 </span>
<?php echo $translator->trans('Installation language'); ?>
</li>
<li class="list-group-item <?php step_active('2'); ?>">
<span class="number"> 2 </span>
<?php echo $translator->trans('Requirements'); ?>
</li>
<li class="list-group-item <?php step_active('3'); ?>">
<span class="number"> 3 </span>
<?php echo $translator->trans('Licence'); ?>
</li>
<li class="list-group-item <?php step_active('4'); ?>">
<span class="number"> 4 </span>
<?php echo $translator->trans('Database settings'); ?>
</li>
<li class="list-group-item <?php step_active('5'); ?>">
<span class="number"> 5 </span>
<?php echo $translator->trans('Config settings'); ?>
</li>
<li class="list-group-item <?php step_active('6'); ?>">
<span class="number"> 6 </span>
<?php echo $translator->trans('Show Overview'); ?>
</li>
<li class="list-group-item <?php step_active('7'); ?>">
<span class="number"> 7 </span>
<?php echo $translator->trans('Install'); ?>
</li>
</ol>
</div>
<div id="note">
<a class="btn btn-info btn-block" href="<?php echo $installationGuideLink; ?>" target="_blank">
<em class="fa fa-file-text-o"></em> <?php echo $translator->trans('Read the installation guide'); ?>
</a>
</div>
<body class="w-full justify-center bg-gradient-to-r from-blue-400 to-blue-600">
<div class="flex flex-col items-center justify-center ">
<div class="rounded p-4 m-8 w-3/5 bg-white flex">
<div class="w-1/3 p-6">
<div class="logo-install">
<img src="../../build/css/themes/chamilo/images/header-logo.png"
class="img-fluid" alt="Chamilo" />
</div>
<div class="col-md-8">
<form
class="form-horizontal" id="install_form" method="post"
action="<?php echo api_get_self(); ?>?running=1&amp;installType=<?php echo $installType; ?>&amp;updateFromConfigFile=<?php echo urlencode($updateFromConfigFile); ?>">
<?php echo $form; ?>
</form>
<div class="install-steps">
<ol class="list-group">
<li class="list-group-item <?php step_active('1'); ?>">
<span class="number"> 1 </span>
<?php echo $translator->trans('Installation language'); ?>
</li>
<li class="list-group-item <?php step_active('2'); ?>">
<span class="number"> 2 </span>
<?php echo $translator->trans('Requirements'); ?>
</li>
<li class="list-group-item <?php step_active('3'); ?>">
<span class="number"> 3 </span>
<?php echo $translator->trans('Licence'); ?>
</li>
<li class="list-group-item <?php step_active('4'); ?>">
<span class="number"> 4 </span>
<?php echo $translator->trans('Database settings'); ?>
</li>
<li class="list-group-item <?php step_active('5'); ?>">
<span class="number"> 5 </span>
<?php echo $translator->trans('Config settings'); ?>
</li>
<li class="list-group-item <?php step_active('6'); ?>">
<span class="number"> 6 </span>
<?php echo $translator->trans('Show Overview'); ?>
</li>
<li class="list-group-item <?php step_active('7'); ?>">
<span class="number"> 7 </span>
<?php echo $translator->trans('Install'); ?>
</li>
</ol>
</div>
<div id="note">
<a class="btn btn-info btn-block" href="<?php echo $installationGuideLink; ?>" target="_blank">
<em class="fa fa-file-text-o"></em> <?php echo $translator->trans('Read the installation guide'); ?>
</a>
</div>
</div>
<div class="w-2/3 p-6">
<form
class="form-horizontal" id="install_form" method="post"
action="<?php echo api_get_self(); ?>?running=1&amp;installType=<?php echo $installType; ?>&amp;updateFromConfigFile=<?php echo urlencode($updateFromConfigFile); ?>">
<?php echo $form; ?>
</form>
</div>
</div>
<footer class="install-footer">
<?php echo $poweredBy; ?>
</footer>
</div>
</div>
</body>
</html>

@ -1,3 +1,5 @@
{% import '@ChamiloCore/Macros/box.html.twig' as display %}
{% set admin_chamilo_announcements_disable = 'admin_chamilo_announcements_disable'|api_get_configuration_value %}
{% block content %}
@ -11,47 +13,37 @@
</div>
{% endif %}
<div id="settings" >
<div class="row">
<div id="settings" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 ">
{% for block_item in blocks_admin %}
<div id="tabs-{{ loop.index }}" class="col-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">{{ block_item.icon }} {{ block_item.label }}</h5>
</div>
<div class="card-body">
<div class="card-search">
{{ block_item.search_form }}
</div>
<div class="card-description mp-3 mb-3">
{% if block_item.description is defined %}
{{ block_item.description }}
{% endif %}
<div id="tabs-{{ loop.index }}" class="" >
{% set list %}
{% if block_item.description is defined %}
{{ block_item.description }}
{% endif %}
{% if block_item.items is not empty %}
<ul class="list-group">
{% for url in block_item.items %}
{% if url.url is not empty %}
<li class="list-group-item">
<a href="{{ url.url }}">
{{ url.label }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% if block_item.extra is not null %}
<div>
{{ block_item.extra }}
</div>
{% if block_item.items is not empty %}
<ul class="list-group">
{% for url in block_item.items %}
{% if url.url is not empty %}
<li class="list-group-item">
<a href="{{ url.url }}">
{{ url.label }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% endif %}
{% endset %}
{% if block_item.extra is not null %}
<div>
{{ block_item.extra }}
</div>
{% endif %}
</div>
</div>
{{ display.panel_box(loop.index, block_item.icon ~ block_item.label, block_item.search_form ~ list) }}
</div>
{% endfor %}
</div>
{# <div class="row">#}
{# {% for role in app.user.roles %}#}

@ -17,10 +17,10 @@
var region_value = '{{ region_value }}';
$(function() {
document.addEventListener('DOMContentLoaded', function() {
var cookieData = Cookies.getJSON('agenda_cookies');
var defaultView = (cookieData && cookieData.view) || '{{ default_view }}';
var defaultStartDate = (cookieData && cookieData.start) || moment.now();
var defaultStartDate = (cookieData && cookieData.start) || luxon.now().toString();
// Reset button.
$("button[type=reset]").click(function() {
@ -102,9 +102,12 @@
},
// Add event
select: function(info) {
var start = moment(info.start);
var end = moment(info.end);
var diffDays = moment(end).diff(start, 'days');
console.log(info);
let start = luxon.fromJSDate(info.start);
let end = luxon.fromJSDate(info.end);
let diffDays = end.diff(start, ["days"]).days;
//var diffDays = moment(end).diff(start, 'days');
var allDay = info.allDay;
var view = info.view;
@ -121,25 +124,26 @@
// Update chz-select
//$("#users_to_send").trigger("chosen:updated");
if ({{ can_add_events }} == 1) {
var startEn = start.clone().locale('en'),
endEn = end.clone().locale('en');
/*var startEn = start.clone().locale('en'),
endEn = end.clone().locale('en');*/
var url = '{{ web_agenda_ajax_url }}&a=add_event&start='+startEn.format("YYYY-MM-DD HH:mm:00")+'&end='+endEn.format("YYYY-MM-DD HH:mm:00")+'&all_day='+allDay+'&view='+view.name;
var start_date_value = start.format('{{ js_format_date }}');
var url = '{{ web_agenda_ajax_url }}&a=add_event&start='+start+'&end='+end+'&all_day='+allDay+'&view='+view.name;
var start_date_value = start.toFormat('{{ js_format_date }}');
$('#start_date').html(start_date_value);
if (diffDays > 1) {
if (diffDays > 0) {
$('#start_date').html('');
var end_date_value = '';
if (end) {
var clone = end.clone();
end_date_value = clone.subtract(1, 'days').format('{{ js_format_date }}');
//var clone = end.clone();
//end_date_value = clone.subtract(1, 'days').format('{{ js_format_date }}');
end_date_value = end.toFormat('{{ js_format_date }}');
}
$('#end_date').html(start_date_value + " - " + end_date_value);
} else if (diffDays == 0) {
var start_date_value = start.format('ll');
var startTime = start.format('LT');
var endTime = end.format('LT');
} else if (diffDays === 0) {
var start_date_value = start.toFormat('ll');
var startTime = start.toFormat('LT');
var endTime = end.toFormat('LT');
$('#start_date').html('');
$('#end_date').html(start_date_value + " (" + startTime + " - " + endTime+") ");
} else {
@ -214,21 +218,25 @@
eventClick: function(info) {
var event = info.event;
var view = info.view;
var start = event.start;
var end = event.end;
/*var start = event.start;
var end = event.end;*/
var momentStart = moment(start);
/*var momentStart = moment(start);
var momentEnd = moment(end);
var diffDays = moment(end).diff(start, 'days');*/
var diffDays = moment(end).diff(start, 'days');
var endDateMinusOne = '';
let start = luxon.fromJSDate(info.start);
let end = luxon.fromJSDate(info.end);
let diffDays = end.diff(start, ["days"]).days;
var endDateMinusOne = '';
if (end) {
//var clone = end.clone();
var endValue = moment(end);
endDateMinusOne = endValue.subtract(1, 'days').format('{{ js_format_date }}');
//var endValue = moment(end);
//endDateMinusOne = endValue.subtract(1, 'days').format('{{ js_format_date }}');
endDateMinusOne = end.toFormat('{{ js_format_date }}');
}
var startDateToString = moment(start).format("{{ js_format_date }}");
var startDateToString = start.toFormat("{{ js_format_date }}");
var calEvent = info.event.extendedProps;
var editable = calEvent.resourceEditable;
// Edit event.
@ -247,9 +255,9 @@
if (diffDays > 1) {
$('#simple_end_date').html(' - ' + endDateMinusOne);
} else if (diffDays == 0) {
var start_date_value = momentStart.format('ll');
var startTime = momentStart.format('LT');
var endTime = momentEnd.format('LT');
var start_date_value = start.toFormat('ll');
var startTime = start.toFormat('LT');
var endTime = start.toFormat('LT');
$('#simple_start_date').html('');
$('#simple_end_date').html(start_date_value + " (" + startTime + " - " + endTime+") ");
} else {
@ -318,10 +326,11 @@
calEvent.end = calEvent.end;
calEvent.allDay = calEvent.allDay;
calEvent.description = $("#content").val();
calendar.fullCalendar('updateEvent',
// @todo
/*calendar.fullCalendar('updateEvent',
calEvent,
true // make the event "stick"
);
);*/
//close modal
$('#calendarModal').modal('toggle');
}

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\EventListener;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
@ -19,12 +20,18 @@ class TwigListener
private SerializerInterface $serializer;
private Environment $twig;
private TokenStorageInterface $tokenStorage;
public function __construct(Environment $twig, SerializerInterface $serializer, TokenStorageInterface $tokenStorage)
{
private IllustrationRepository $illustrationRepository;
public function __construct(
Environment $twig,
SerializerInterface $serializer,
TokenStorageInterface $tokenStorage,
IllustrationRepository $illustrationRepository
) {
$this->twig = $twig;
$this->tokenStorage = $tokenStorage;
$this->serializer = $serializer;
$this->illustrationRepository = $illustrationRepository;
}
public function __invoke(RequestEvent $event): void
@ -32,17 +39,16 @@ class TwigListener
$request = $event->getRequest();
$token = $this->tokenStorage->getToken();
$user = null;
$data = null;
$avatar = null;
$isAuth = false;
if (null !== $token) {
$user = $token->getUser();
if ($user instanceof UserInterface) {
/** @var User $userClone */
$userClone = clone $user;
$userClone->setPassword('');
$data = $this->serializer->serialize($userClone, JsonEncoder::FORMAT);
$data = $this->serializer->serialize($userClone, 'jsonld', ['groups' => ['user_json:read']]);
$avatar = $this->illustrationRepository->getIllustrationUrl($user);
$isAuth = true;
}
}
@ -50,6 +56,7 @@ class TwigListener
//$this->twig->addGlobal('text_direction', api_get_text_direction());
$this->twig->addGlobal('from_vue', $request->request->get('from_vue') ? 1 : 0);
$this->twig->addGlobal('is_authenticated', json_encode($isAuth));
$this->twig->addGlobal('user_json', $data ?? json_encode($data));
$this->twig->addGlobal('user_json', $data ?? json_encode([]));
$this->twig->addGlobal('user_avatar', $avatar);
}
}

@ -0,0 +1,20 @@
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
fontFamily: {
sans: ['Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif']
}
},
},
variants: {
extend: {
opacity: ['disabled'],
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/ui'),
],
}

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
// this enables stricter inference for data properties on `this`
"strict": true,
"jsx": "preserve",
"moduleResolution": "node"
}
}

@ -1,4 +1,13 @@
var Encore = require('@symfony/webpack-encore');
const Encore = require('@symfony/webpack-encore');
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
const PurgeCssPlugin = require('purgecss-webpack-plugin');
const glob = require('glob-all');
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
Encore
@ -42,13 +51,18 @@ Encore
})
.enableSassLoader()
.enableVueLoader(() => {}, { runtimeCompilerBuild: false })
.enableTypeScriptLoader(function(tsConfig) {
tsConfig.transpileOnly = true;
//tsConfig.configFile = './tsconfig.json';
//tsConfig.exclude = ['/node_modules(?!\\/vuex-composition-helpers)/'];
})
.enableVueLoader(() => {}, { version: 3, runtimeCompilerBuild: false})
.autoProvidejQuery()
/*.enablePostCssLoader(function (options) {
options.config = {
.enablePostCssLoader(function(options) {
options.postcssOptions = {
path: 'postcss.config.js'
}
})*/
})
.copyFiles([
{
from: './node_modules/fullcalendar/',
@ -112,8 +126,39 @@ Encore
//
// },
// })
/*.addLoader({
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
exclude: /node_modules(?!\/vuex-composition-helpers)/,
})*/
/*.addLoader({
test: /\.vue$/,
loader: 'vue-loader'
})
.addLoader({
test: /\.ts?/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
exclude: /node_modules(?!\/vuex-composition-helpers)/,
})
.addLoader({
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
})*/
/*.addLoader({
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
exclude: /node_modules/,
})*/
;
//Encore.addPlugin(new VueLoaderPlugin);
Encore.addPlugin(new CopyPlugin({
patterns: [
{
@ -141,20 +186,18 @@ Encore.addPlugin(new CopyPlugin({
}
));
// Encore.addPlugin(new CopyPlugin([{
// from: 'assets/css/themes/' + theme + '/images',
// to: 'css/themes/' + theme + '/images'
// };
var themes = [
const themes = [
'chamilo'
];
// Add Chamilo themes
themes.forEach(function (theme) {
Encore.addStyleEntry('css/themes/' + theme + '/default', './assets/css/themes/' + theme + '/default.css');
// Copy images from themes into public/build
Encore.addPlugin(new CopyPlugin({
patterns: [{
@ -180,13 +223,25 @@ themes.forEach(function (theme) {
// }
// }));
/*Encore.configureLoaderRule('ts', rule => {
rule.exclude = '/node_modules(?!/vuex-composition-helpers)/'
});*/
const config = Encore.getWebpackConfig();
config.resolve.alias = {
/*config.resolve = {
extensions: [ '.ts', '.js' ],
extensions: ['.ts', '.js', '.vue', '.json'],
alias: {
'vue': '@vue/runtime-dom'
},
};*/
/*config.resolve.alias = {
// If using the runtime only build
vue$: 'vue/dist/vue.runtime.esm.js' // 'vue/dist/vue.runtime.common.js' for webpack 1
// Or if using full build of Vue (runtime + compiler)
// vue$: 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
};
};*/
module.exports = config;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save