Skip to content

Commit 9603ec0

Browse files
committed
Merge branch 'master' of github.com:chamilo/chamilo-lms
2 parents 60e4ba6 + 15d151f commit 9603ec0

114 files changed

Lines changed: 5787 additions & 3308 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

assets/css/scss/_chat.scss

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,131 @@
143143
.chd-bubble__ack{font-variant-numeric:tabular-nums}
144144
}
145145

146+
.course-tool-chat {
147+
/* ---------- Buttons ---------- */
148+
.btn{
149+
border-radius: 12px;
150+
padding: 8px 12px;
151+
font-size: 14px;
152+
font-weight: 600;
153+
border: 1px solid #E5E7EB;
154+
background:#fff;
155+
color:#374151;
156+
cursor: pointer;
157+
transition: background .15s ease, box-shadow .15s ease, border-color .15s ease, color .15s ease;
158+
}
159+
.btn:hover{ background:#F9FAFB; }
160+
.btn:disabled{ opacity:.5; cursor:not-allowed; }
161+
.btn-primary{ background:#4F46E5; border-color:#4F46E5; color:#fff; box-shadow: 0 2px 6px rgba(79,70,229,.25); }
162+
.btn-primary:hover{ background:#4338CA; }
163+
.btn-secondary{ color:#374151; }
164+
.btn-tertiary{ background:#fff; color:#4B5563; }
165+
.btn-danger-outline{ border-color:#EF4444; color:#B91C1C; background:#fff; }
166+
.btn-danger-outline:hover{ background:#FEE2E2; }
167+
168+
/* ---------- Textarea feel ---------- */
169+
.chat-writer{
170+
line-height: 1.35;
171+
white-space: pre-wrap;
172+
word-break: break-word;
173+
tab-size: 2;
174+
letter-spacing: normal;
175+
resize: vertical;
176+
}
177+
178+
/* ---------- Chat history container ---------- */
179+
.chat-history{
180+
background:#FAFAFA;
181+
border:1px solid #F3F4F6;
182+
border-radius: 16px;
183+
padding: 12px;
184+
min-height: 220px;
185+
max-height: 50vh;
186+
overflow-y: auto;
187+
}
188+
189+
/* ---------- Bubbles (match backend HTML structure) ---------- */
190+
.message-teacher,
191+
.message-student{
192+
display:flex;
193+
align-items:flex-end;
194+
gap:10px;
195+
margin:10px 0;
196+
}
197+
.message-teacher{ justify-content:flex-end; }
198+
.message-student{ justify-content:flex-start; }
199+
200+
.message-teacher .content-message,
201+
.message-student .content-message{
202+
max-width: 72%;
203+
padding: 10px 12px;
204+
border-radius: 14px;
205+
box-shadow: 0 1px 2px rgba(0,0,0,.04);
206+
}
207+
208+
.message-teacher .content-message{
209+
background:#4F46E5; color:#fff;
210+
border-top-right-radius: 4px;
211+
}
212+
.message-student .content-message{
213+
background:#F3F4F6; color:#111827;
214+
border-top-left-radius: 4px;
215+
}
216+
217+
.chat-image{
218+
width: 36px; height: 36px; border-radius: 9999px; object-fit: cover;
219+
border: 1px solid #E5E7EB;
220+
}
221+
.chat-message-block-name{
222+
font-weight: 600; font-size: 0.85rem; margin-bottom: 2px;
223+
color: currentColor;
224+
}
225+
.chat-message-block-content p{ margin: 0; }
226+
.chat-message-block-content p + p{ margin-top: .25rem; }
227+
.chat-message-block-content h1,
228+
.chat-message-block-content h2,
229+
.chat-message-block-content h3,
230+
.chat-message-block-content h4,
231+
.chat-message-block-content h5,
232+
.chat-message-block-content h6{ margin: .25rem 0; font-size: 1em; }
233+
.chat-message-block-content ul,
234+
.chat-message-block-content ol{ margin: .25rem 0; padding-left: 1.25rem; }
235+
.chat-message-block-content blockquote{ margin: .25rem 0; padding-left: .75rem; border-left: 3px solid #E5E7EB; color:#6B7280; }
236+
237+
.message-date{
238+
font-size: .7rem; opacity:.8; margin-top: 6px; text-align: right;
239+
color: #E0E7FF; /* light for teacher bubble */
240+
}
241+
.message-student .message-date{ color:#6B7280; text-align:left; }
242+
243+
/* Emoji popover */
244+
#emoji-popover.emoji-popover{
245+
position: fixed;
246+
z-index: 50;
247+
width: max-content;
248+
max-width: calc(100vw - 24px);
249+
max-height: 18rem;
250+
overflow: auto;
251+
padding: 8px;
252+
border: 1px solid #E5E7EB;
253+
border-radius: 16px;
254+
background: #fff;
255+
box-shadow: 0 10px 20px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.06);
256+
}
257+
#emoji-popover .emoji-btn{
258+
width: 2.4rem; height: 2.4rem; font-size: 1.25rem; line-height: 1;
259+
display: flex; align-items: center; justify-content: center;
260+
border-radius: 10px; border: none; background: transparent; cursor: pointer;
261+
}
262+
#emoji-popover .emoji-btn:hover{ background:#F3F4F6; }
263+
#emoji-popover .emoji-btn:focus{ outline:2px solid rgba(79,70,229,.6); outline-offset:2px; }
264+
265+
/* Emoji-friendly font stack */
266+
#chat-writer, .chat-history, #emoji-popover{
267+
font-family: Arial,sans-serif, "Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue";
268+
}
269+
}
270+
146271
@media (max-width: 720px) {
147272
.chd .chd-dock { width: 100%; right: 0; left: 0; bottom: 0; border-radius: 10px; }
148273
.chd .chd-body { grid-template-columns: 1fr; }

assets/css/scss/_survey.scss

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,79 @@
104104
}
105105
}
106106
}
107+
108+
.survey_question .field {
109+
@apply flex flex-col gap-3 w-full;
110+
}
111+
112+
.survey_question .radio-inline {
113+
@apply flex items-center gap-3 mb-2;
114+
}
115+
116+
.survey_question .p-radiobutton {
117+
@apply relative flex items-center justify-center w-5 h-5;
118+
}
119+
120+
.survey_question .p-radiobutton-box {
121+
@apply w-5 h-5 border border-gray-50 rounded-full flex items-center justify-center bg-white transition-all duration-150 ease-in-out;
122+
}
123+
124+
.survey_question .p-radiobutton-input {
125+
@apply absolute opacity-0 cursor-pointer inset-0 w-full h-full;
126+
}
127+
128+
.survey_question .p-radiobutton-checked .p-radiobutton-box {
129+
@apply border-primary bg-primary;
130+
}
131+
132+
.survey_question .p-radiobutton-icon {
133+
@apply w-2 h-2 bg-white rounded-full hidden;
134+
}
135+
136+
.survey_question .p-radiobutton-checked .p-radiobutton-icon {
137+
@apply block;
138+
}
139+
140+
.survey_question .radio-inline label {
141+
@apply flex items-center text-gray-90 text-base font-normal cursor-pointer select-none;
142+
}
143+
144+
.survey_question .radio-inline label p {
145+
@apply m-0 leading-tight;
146+
}
147+
148+
.survey_question .checkbox-inline {
149+
@apply flex items-center gap-3 mb-2;
150+
}
151+
152+
.survey_question .p-checkbox {
153+
@apply relative flex items-center justify-center w-5 h-5;
154+
}
155+
156+
.survey_question .p-checkbox-box {
157+
@apply w-5 h-5 border border-gray-50 rounded-md flex items-center justify-center bg-white transition-all duration-150 ease-in-out;
158+
}
159+
160+
.survey_question .p-checkbox-input {
161+
@apply absolute opacity-0 cursor-pointer inset-0 w-full h-full;
162+
}
163+
164+
.survey_question .p-checkbox-icon {
165+
@apply w-3 h-3 text-white hidden;
166+
}
167+
168+
.survey_question .p-checkbox-input:checked + .p-checkbox-box {
169+
@apply border-primary bg-primary;
170+
}
171+
172+
.survey_question .p-checkbox-input:checked + .p-checkbox-box .p-icon {
173+
@apply block;
174+
}
175+
176+
.survey_question .checkbox-inline label {
177+
@apply flex items-center text-gray-90 text-base font-normal cursor-pointer select-none;
178+
}
179+
180+
.survey_question .checkbox-inline label p {
181+
@apply m-0 leading-tight;
182+
}

assets/vue/App.vue

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@
1313
>
1414
<div class="flex items-center gap-4 rounded-2xl p-6 bg-warning text-white/80 shadow">
1515
<i class="mdi mdi-alert-outline text-4xl text-white"></i>
16-
<p class="font-extrabold text-xl text-white" v-text="forbiddenMsg" />
16+
<p
17+
class="font-extrabold text-xl text-white"
18+
v-text="forbiddenMsg"
19+
/>
1720
</div>
1821
</div>
1922

2023
<!-- Page content; optionally dim/disable when forbidden -->
2124
<div :class="{ 'opacity-50 pointer-events-none': !!forbiddenMsg }">
2225
<slot />
23-
<div id="legacy_content" ref="legacyContainer" />
26+
<div
27+
id="legacy_content"
28+
ref="legacyContainer"
29+
/>
2430
<ConfirmDialog />
2531
<AccessUrlChooser v-if="!showAccessUrlChosserLayout" />
2632
<DockedChat v-if="showGlobalChat" />
@@ -55,7 +61,7 @@
5561
</template>
5662

5763
<script setup>
58-
import { computed, onMounted, onUpdated, provide, ref, watch, watchEffect, defineAsyncComponent } from "vue"
64+
import { computed, defineAsyncComponent, onMounted, onUpdated, provide, ref, watch, watchEffect } from "vue"
5965
import { useRoute, useRouter } from "vue-router"
6066
import { DefaultApolloClient } from "@vue/apollo-composable"
6167
import axios from "axios"
@@ -135,7 +141,12 @@ const layout = computed(() => {
135141
})
136142
137143
const legacyContainer = ref(null)
138-
watch(() => route.name, () => { if (legacyContainer.value) legacyContainer.value.innerHTML = "" })
144+
watch(
145+
() => route.name,
146+
() => {
147+
if (legacyContainer.value) legacyContainer.value.innerHTML = ""
148+
},
149+
)
139150
watchEffect(() => {
140151
if (!legacyContainer.value) return
141152
const content = document.querySelector("#sectionMainContent")
@@ -183,21 +194,28 @@ axios.interceptors.response.use(
183194
if (s === 401) notification.showWarningNotification(error.response?.data?.error || "Unauthorized")
184195
else if (s === 500) notification.showWarningNotification(error.response?.data?.detail || "Server error")
185196
return Promise.reject(error)
186-
}
197+
},
187198
)
188199
189200
platformConfigurationStore.initialize()
190201
191202
// i18n sync
192-
watch(() => route.params, () => {
193-
const { appLocale } = useLocale()
194-
if (appLocale?.value && locale.value !== appLocale.value) setLocale(appLocale.value)
195-
}, { immediate: true })
196-
197-
watch(() => securityStore.user?.language, (lang) => {
198-
if (lang && locale.value !== lang) setLocale(lang)
199-
}, { immediate: true })
203+
watch(
204+
() => route.params,
205+
() => {
206+
const { appLocale } = useLocale()
207+
if (appLocale?.value && locale.value !== appLocale.value) setLocale(appLocale.value)
208+
},
209+
{ immediate: true },
210+
)
200211
212+
watch(
213+
() => securityStore.user?.language,
214+
(lang) => {
215+
if (lang && locale.value !== lang) setLocale(lang)
216+
},
217+
{ immediate: true },
218+
)
201219
202220
onMounted(async () => {
203221
const { loader } = useMediaElementLoader()
@@ -220,26 +238,22 @@ onMounted(async () => {
220238
const DockedChat = defineAsyncComponent(() => import("./components/chat/DockedChat.vue"))
221239
const allowGlobalChat = computed(() => {
222240
if (platformConfigurationStore.isLoading) {
223-
console.log("[CHAT] waiting settings... isLoading=true")
224241
return false
225242
}
226243
const val = platformConfigurationStore.getSetting?.("chat.allow_global_chat")
227-
console.log("[CHAT] getSetting('chat.allow_global_chat') ->", val)
228244
return String(val) === "true"
229245
})
230246
231247
const showGlobalChat = computed(() => {
232-
const visible = securityStore.isAuthenticated && allowGlobalChat.value
233-
console.log("[CHAT] showGlobalChat=", visible, "| isAuthenticated=", securityStore.isAuthenticated, "| allowGlobalChat=", allowGlobalChat.value)
234-
return visible
248+
return securityStore.isAuthenticated && allowGlobalChat.value
235249
})
236250
237251
watch(forbiddenMsg, (msg) => {
238252
if (msg) {
239-
const legacy = document.getElementById('legacy_content')
240-
if (legacy) legacy.innerHTML = ''
241-
const section = document.getElementById('sectionMainContent')
242-
if (section) section.innerHTML = ''
253+
const legacy = document.getElementById("legacy_content")
254+
if (legacy) legacy.innerHTML = ""
255+
const section = document.getElementById("sectionMainContent")
256+
if (section) section.innerHTML = ""
243257
}
244258
})
245259
</script>

assets/vue/components/Breadcrumb.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
v-bind="props.action"
1313
@click="handleBreadcrumbClick(item)"
1414
>
15-
{{ item.label }}
15+
{{ stripHtml(item.label) }}
1616
</BaseAppLink>
17-
<span v-else>{{ item.label }}</span>
17+
<span
18+
v-else
19+
v-text="stripHtml(item.label)"
20+
></span>
1821
</template>
1922

2023
<template #separator> /</template>
@@ -422,4 +425,9 @@ function handleBreadcrumbClick(item) {
422425
window.location.href = router.resolve(item.route).href
423426
}
424427
}
428+
429+
function stripHtml(value) {
430+
if (!value || typeof value !== "string") return ""
431+
return value.replace(/<[^>]*>?/gm, "").trim()
432+
}
425433
</script>

assets/vue/components/StudentViewButton.vue

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,27 @@ const isStudentView = computed({
3333
try {
3434
const resp = await permissionService.toogleStudentView()
3535
const mode = (typeof resp === "string" ? resp : resp?.data || "").toString().toLowerCase()
36-
const desired = mode.includes("student")
36+
const desired = mode === "studentview"
3737
38-
platformConfigStore.studentView = desired
38+
platformConfigStore.setStudentViewEnabled(desired)
3939
emit("change", desired)
4040
} catch (e) {
4141
console.warn("[SVB] toggle failed", e)
42-
const desired = !platformConfigStore.isStudentViewActive
43-
platformConfigStore.studentView = desired
44-
emit("change", desired)
42+
platformConfigStore.setStudentViewEnabled(!platformConfigStore.isStudentViewActive)
43+
emit("change", platformConfigStore.isStudentViewActive)
4544
}
4645
},
4746
get() {
48-
return platformConfigStore.isStudentViewActive
47+
return !!platformConfigStore.isStudentViewActive
4948
},
5049
})
5150
52-
const showButton = computed(() =>
53-
securityStore.isAuthenticated &&
54-
cidReqStore.course &&
55-
(securityStore.isCourseAdmin || securityStore.isAdmin || isCoach.value) &&
56-
platformConfigStore.getSetting("course.student_view_enabled") === "true"
51+
const showButton = computed(
52+
() =>
53+
securityStore.isAuthenticated &&
54+
cidReqStore.course &&
55+
(securityStore.isCourseAdmin || securityStore.isAdmin || isCoach.value) &&
56+
platformConfigStore.getSetting("course.student_view_enabled") === "true",
5757
)
5858
5959
const windowSize = ref(window.innerWidth)

0 commit comments

Comments
 (0)