Views
A continuación se desglosaran ciertas partes del código que nos ayudaran a entender como funcionan ciertos apartados del sitio en el backend, desarrollado con laravel.

Administrador Vistas
Crear Curso
Es una página para la creación de un curso en un panel de administración.
@extends('layouts.app')
@section('title', 'Crear curso')
@section('content')
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quill.js"></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/[email protected]/dist/alpine.min.js"></script>
<script src="https://cdn.filesizejs.com/filesize.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.1/css/all.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/quill.snow.css" rel="stylesheet">
<style>
.selected {
background-color: #d1fae5; /* Fondo verde claro */
}
.selected svg {
stroke: #34d399; /* Verde claro */
}
</style>
<div class="container mx-auto w-full p-4 flex flex-col justify-start gap-5">
<div class="flex justify-between items-center">
<div class="flex justify-between items-center w-full bg-gray-800 rounded p-6">
<h1 class="text-3xl text-bold text-white">Panel de Administración - Crear curso</h1>
<a href="" class="opacity-0 bg-gray-600 text-white px-4 py-2 rounded hover:bg-blue-600">Crear curso</a>
</div>
</div>
@if(session('error'))
<div class="bg-green-200 w-fit p-5 rounded mb-4">
{{ session('error') }}
</div>
@endif
<div class="shadow-2xl rounded flex flex-col justify-start bg-gray-50 ">
<form action="{{ route('crear.admin.curso') }}" method="POST" enctype="multipart/form-data" >
@csrf
<div class="w-full flex flex-col items-center content-center align-middle justify-center gap-1">
<div class="w-4/5 p-3">
<input type="hidden" name="id_usuario" value="{{ session()->get('usuario_id') }}">
<div class="gap-4">
<div class="space-y-1 mt-2">
<label for="titulo" class="block text-sm font-medium text-black">Título</label>
<input type="text" name="titulo" class="flex h-10 w-full rounded-md border-gray-400 border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" >
</div>
</div>
<div class="gap-4 grid grid-cols-2">
<div class="space-y-1 mt-2">
<label for="estado" class="block text-sm font-medium text-black">Estado</label>
<select name="estado" class="flex h-10 w-full rounded-md border-gray-400 border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" >
<option value="0">Abierto</option>
<option value="0">Cerrado</option>
</select>
</div>
<div class="space-y-1 mt-2">
<label for="precio" class="block text-sm font-medium text-black">Precio</label>
<input type="number" step="0.01" name="precio" class="flex h-10 w-full rounded-md border-gray-400 border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" >
</div>
</div>
<div class=" gap-4">
<div class="space-y-1 mt-2">
<label for="recursos" class="block text-sm font-medium text-black">Recursos</label>
<div class="flex flex-col flex-grow mb-3 w-full">
<div x-data="{ files: null }" id="FileUpload"
class="block w-full py-2 px-3 relative bg-white appearance-none border-2 border-gray-300 border-solid rounded-md hover:shadow-outline-gray">
<input type="file" name="recursos[]" multiple
class="absolute inset-0 z-50 m-0 p-0 w-full h-full outline-none opacity-0"
x-on:change="files = $event.target.files; console.log($event.target.files);"
x-on:dragover="$el.classList.add('active')" x-on:dragleave="$el.classList.remove('active')" x-on:drop="$el.classList.remove('active')"
>
<template x-if="files !== null">
<div class="flex flex-col space-y-1">
<template x-for="(_,index) in Array.from({ length: files.length })">
<div class="flex flex-row items-center space-x-2">
<template
x-if="files[index].type.includes('audio/')"><i class="far fa-file-audio fa-fw"></i></template>
<template
x-if="files[index].type.includes('application/')"><i class="far fa-file-alt fa-fw"></i></template>
<template
x-if="files[index].type.includes('image/')"><i class="far fa-file-image fa-fw"></i></template>
<template
x-if="files[index].type.includes('video/')"><i class="far fa-file-video fa-fw"></i></template>
<span class="font-medium text-gray-900" x-text="files[index].name">Cargando</span>
<span class="text-xs self-end text-gray-500" x-text="filesize(files[index].size)">...</span>
</div>
</template>
</div>
</template>
<template x-if="files === null">
<div class="flex flex-col space-y-2 p-6 items-center justify-center">
<i class="fas fa-cloud-upload-alt fa-3x text-currentColor"></i>
<p class="text-gray-700">Da clic para abrir el explorador de archivos.</p>
<a href="javascript:void(0)"
class="flex items-center mx-auto py-2 px-4 text-white text-center font-medium border border-transparent rounded-md outline-none bg-gray-700">Seleccionar archivos</a>
</div>
</template>
</div>
</div>
</div>
</div>
<div class="gap-4">
<div class="space-y-1 mt-2">
<label for="imagen" class="block text-sm font-medium text-black">Imagen</label>
<div id="file-preview" class="w-full min-h-[300px] flex items-center justify-center bg-gray-50 border-dashed border-2 border-gray-400 rounded-lg mx-auto text-center cursor-pointer relative overflow-hidden">
<input id="file-input" type="file" name="imagen" class="hidden" accept="image/*" />
<label for="file-input" class="absolute inset-0 flex flex-col items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 text-gray-950 mb-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
<h5 class="mb-2 text-xl font-bold tracking-tight text-gray-950">Cargar imagen</h5>
<p class="font-normal text-sm text-gray-900 md:px-6">Selecciona una imagen que no exceda los <b class="text-gray-900">2MB</b></p>
<p class="font-normal text-sm text-gray-900 md:px-6">y asegurate que sea alguno de los formatos: <b class="text-gray-900">JPG, JPEG o PNG</b></p>
<span id="filename" class="text-gray-900 bg-white z-50"></span>
</label>
<img id="image-preview" src="" alt="Vista previa" class="hidden max-h-[300px] object-cover" />
</div>
<button id="reset-button" class="mt-4 w-full px-4 py-2 bg-gray-700 text-white rounded">Limpiar</button>
<script>
const input = document.getElementById('file-input');
const filePreviewContainer = document.getElementById('file-preview');
const imagePreview = document.getElementById('image-preview');
const label = filePreviewContainer.querySelector('label');
const resetButton = document.getElementById('reset-button');
const previewPhoto = () => {
const file = input.files[0];
if (file) {
const fileReader = new FileReader();
fileReader.onload = function (event) {
imagePreview.src = event.target.result;
imagePreview.classList.remove('hidden');
label.classList.add('hidden');
}
fileReader.readAsDataURL(file);
}
}
input.addEventListener("change", (event) => {
if (event.target.files.length > 0) {
previewPhoto();
}
});
resetButton.addEventListener('click', () => {
input.value = '';
event.preventDefault();
imagePreview.classList.add('hidden');
label.classList.remove('hidden');
});
</script>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1 mt-2">
<label for="descripcion" class="block text-sm font-medium text-black">Descripción</label>
<textarea name="descripcion" class="h-[110px] flex h-10 w-full resize-none rounded-md border-gray-400 border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" ></textarea>
</div>
<div class="space-y-1 mt-2">
<label for="categoria" class="block text-sm font-medium text-black">Categoría</label>
<div class="flex flex-col content-center ">
<div class="flex items-center">
<input id="categoria" type="text" name="" placeholder="Agregar categoría" class="flex h-10 w-full rounded-md border-gray-400 border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" />
<button id="add-tag" class="ml-2 px-4 py-2 bg-gray-700 text-white rounded">Agregar</button>
</div>
<input type="hidden" id="categoria-hidden" name="categoria" />
<div id="tags-container" class="flex flex-wrap gap-2 mb-2">
</div>
</div>
</div>
<script>
const categoriaInput = document.getElementById('categoria');
const addTagButton = document.getElementById('add-tag');
const tagsContainer = document.getElementById('tags-container');
const categoriaHiddenInput = document.getElementById('categoria-hidden');
const updateHiddenInput = () => {
const tags = Array.from(tagsContainer.children)
.map(tag => tag.textContent.replace('✖', '').trim())
.join('|');
categoriaHiddenInput.value = `|${tags}|`;
}
const createTag = (text) => {
const tag = document.createElement('div');
tag.className = 'bg-gray-200 text-gray-800 rounded px-2 py-1 flex items-center';
tag.textContent = text;
const removeButton = document.createElement('button');
removeButton.textContent = '✖';
removeButton.className = 'ml-2 text-red-500';
removeButton.onclick = () => {
tagsContainer.removeChild(tag);
updateHiddenInput();
}
tag.appendChild(removeButton);
tagsContainer.appendChild(tag);
updateHiddenInput();
}
addTagButton.addEventListener('click', () => {
const value = categoriaInput.value.trim();
event.preventDefault();
if (value && !Array.from(tagsContainer.children).some(tag => tag.textContent.includes(value))) {
createTag(value);
categoriaInput.value = '';
}
});
categoriaInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTagButton.click();
}
});
</script>
</div>
<div class="gap-4">
<div class="space-y-1 mt-2">
<label for="contenidoCurso" class="block text-sm font-medium text-black">Contenido</label>
<input type="hidden" name="contenidoCurso" id="contenidoCurso">
<div id="editor-curso" class="min-h-[150px]">
<h2>Titulo de contenido</h2>
<p>Agrega <code>texto con formato</code> al contenido de tu curso.</p>
</div>
<script>
var quillCurso = new Quill('#editor-curso', {
theme: 'snow'
});
function updateCurso() {
var content = quillCurso.root.innerHTML;
document.getElementById('contenidoCurso').value = content;
}
quillCurso.on('text-change', updateCurso);
</script>
</div>
</div>
</div>
<div class="w-full p-3 overflow-auto hidden" id="capitulos-container">
</div>
<div id="preguntas-container" class="w-full mt-4 mb-5 grid p-6 grid-cols-2 gap-4"></div>
</div>
<div class="w-full flex grid grid-cols-3 justify-center p-3 gap-4">
<div class="mb-4">
<button type="button" class="w-full bg-gray-700 hover:bg-gray-950 text-white px-4 py-2 rounded-md" id="add-capitulo">Añadir Capítulo</button>
</div>
<div>
<button id="agregar-pregunta" type="button" class="w-full bg-gray-950 hover:bg-gray-950 text-white px-4 py-2 rounded-md">Agregar pregunta</button>
</div>
<div class="mb-4">
<button type="submit" class="w-full bg-green-900 hover:bg-green-800 text-white px-4 py-2 rounded-md">Guardar</button>
</div>
</div>
</form>
</div>
</div>
<script>
let numPregunta = 0; // Contador de preguntas
document.getElementById('agregar-pregunta').addEventListener('click', function() {
const preguntasContainer = document.getElementById('preguntas-container');
// Crear un nuevo elemento para la pregunta
const nuevaPregunta = document.createElement('div');
nuevaPregunta.className = 'bg-gray-200 p-4 rounded shadow space-y-2';
nuevaPregunta.innerHTML = `
<h1 class="text-black font-xl">Pregunta número ${numPregunta + 1}</h1>
<input type="text" name="preguntas[${numPregunta}][pregunta]" placeholder="Pregunta del examen" class="border border-gray-400 rounded p-2 w-full" />
<h2 class="text-black font-lg mt-2">Seleccione la respuesta correcta:</h2>
<div class="justify-between flex w-full gap-2 pb-3">
<div class="flex w-full flex-col space-y-2">
<input type="text" name="preguntas[${numPregunta}][opciones][]" placeholder="Respuesta 1" class="border border-gray-400 rounded p-2 w-full" />
<input type="text" name="preguntas[${numPregunta}][opciones][]" placeholder="Respuesta 2" class="border border-gray-400 rounded p-2 w-full" />
<input type="text" name="preguntas[${numPregunta}][opciones][]" placeholder="Respuesta 3" class="border border-gray-400 rounded p-2 w-full" />
<input type="text" name="preguntas[${numPregunta}][opciones][]" placeholder="Respuesta 4" class="border border-gray-400 rounded p-2 w-full" />
</div>
<div class="flex flex-col space-y-2 w-fit">
<label class="flex items-center border border-gray-400 rounded p-2 w-fit cursor-pointer bg-red-100" onclick="checkedQuestion(this, ${numPregunta})">
<input type="radio" name="preguntas[${numPregunta}][respuesta_correcta]" value="0" class="sr-only">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 stroke-red-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</label>
<label class="flex items-center border border-gray-400 rounded p-2 w-fit cursor-pointer bg-red-100" onclick="checkedQuestion(this, ${numPregunta})">
<input type="radio" name="preguntas[${numPregunta}][respuesta_correcta]" value="1" class="sr-only">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 stroke-red-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</label>
<label class="flex items-center border border-gray-400 rounded p-2 w-fit cursor-pointer bg-red-100" onclick="checkedQuestion(this, ${numPregunta})">
<input type="radio" name="preguntas[${numPregunta}][respuesta_correcta]" value="2" class="sr-only">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 stroke-red-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</label>
<label class="flex items-center border border-gray-400 rounded p-2 w-fit cursor-pointer bg-red-100" onclick="checkedQuestion(this, ${numPregunta})">
<input type="radio" name="preguntas[${numPregunta}][respuesta_correcta]" value="3" class="sr-only">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 stroke-red-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</label>
</div>
</div>
<button class="bg-red-400 w-full text-white font-semibold py-1 px-2 rounded hover:bg-red-300 transition duration-200 mt-6">
Eliminar Pregunta
</button>
`;
preguntasContainer.appendChild(nuevaPregunta);
numPregunta++; // Incrementar el contador
// Agregar la funcionalidad para eliminar la pregunta
nuevaPregunta.querySelector('button').addEventListener('click', function() {
preguntasContainer.removeChild(nuevaPregunta);
numPregunta--; // Decrementar el contador si se elimina una pregunta
});
});
function checkedQuestion(label, id) {
// Encuentra el contenedor de la pregunta usando el id proporcionado
const preguntaContainer = document.querySelector(`#preguntas-container > div:nth-child(${id + 1})`);
if (!preguntaContainer) return; // Si el contenedor de la pregunta no existe, no hacer nada
// Elimina la clase 'selected' de todos los labels y desmarca los radios dentro del contenedor de la pregunta específica
preguntaContainer.querySelectorAll('label').forEach(lbl => {
lbl.classList.remove('selected');
const input = lbl.querySelector('input[type="radio"]');
if (input) input.checked = false;
});
// Marca el label seleccionado y su radio dentro del contenedor de la pregunta específica
label.classList.add('selected');
const input = label.querySelector('input[type="radio"]');
if (input) input.checked = true;
}
document.getElementById('add-capitulo').addEventListener('click', function () {
let capitulosContainer = document.getElementById('capitulos-container');
capitulosContainer.classList.remove('hidden');
let capituloIndex = capitulosContainer.querySelectorAll('.capitulo').length;
let capituloHtml = `
<div class="flex grid grid-cols-2 gap-4 mb-4" id="CAP${capituloIndex}">
<div class="w-full sticky capitulo bg-gray-200 p-4 justify-center items-center content-center max-h-[700px] border rounded-md">
<div class="flex flex-row justify-between">
<div>
<h4 class="text-lg font-bold mb-2">Capítulo ${capituloIndex + 1}</h4>
</div>
<div>
<svg onclick="removeElement(element='CAP${capituloIndex}')" class="w-6 hidden h-6 text-gray-800 hover:cursor-pointer" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/>
</svg>
</div>
</div>
<div class="space-y-1 mt-2">
<label for="capitulos[${capituloIndex}][titulo]" class="block text-sm font-medium text-gray-900">Título</label>
<input type="text" name="capitulos[${capituloIndex}][titulo]" class="flex h-10 w-full rounded-md border-gray-400 border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" >
</div>
<div class="gap-4">
<div class="space-y-1">
<label for="contenidoCapitulo${capituloIndex}" class="block text-sm font-medium text-gray-900">Descripción</label>
<input type="hidden" name="capitulos[${capituloIndex}][descripcion]" id="contenidoCapitulo${capituloIndex}">
<div id="editor-capitulo-${capituloIndex}" class="min-h-[150px] bg-white">
<h2>Titulo de contenido</h2>
<p>Agrega <code>texto con formato</code> al contenido de tu curso.</p>
</div>
</div>
</div>
<div class="space-y-1 mt-2">
<div class="subcapitulos-container">
<h5 class="text-md font-bold mt-6">Subcapítulos</h5>
<button type="button" class="w-full bg-gray-500 hover:bg-gray-600 text-white px-4 mt-4 py-2 rounded-md add-subcapitulo" data-capitulo-index="${capituloIndex}">Añadir Subcapítulo</button>
</div>
</div>
</div>
<div class="flex flex-col overflow-auto bg-gray-300 rounded-md overflow max-h-[700px] gap-4 p-4" id="subcapitulos-container-${capituloIndex}">
</div>
</div>
`;
capitulosContainer.insertAdjacentHTML('beforeend', capituloHtml);
var quillCapitulo = new Quill(`#editor-capitulo-${capituloIndex}`, {
theme: 'snow'
});
function updateCapitulo() {
var content = quillCapitulo.root.innerHTML;
document.getElementById(`contenidoCapitulo${capituloIndex}`).value = content;
}
quillCapitulo.on('text-change', updateCapitulo);
});
document.addEventListener('click', function (e) {
if (e.target && e.target.classList.contains('add-subcapitulo')) {
let capituloIndex = event.target.getAttribute('data-capitulo-index');
let subcapitulosContainer = document.getElementById('subcapitulos-container-' + capituloIndex);
let subcapituloIndex = subcapitulosContainer.querySelectorAll('.subcapitulo').length;
let capituloNum = parseInt(capituloIndex)+1;
let subcapituloHtml = `
<div class="bg-gray-300 subcapitulo p-4 rounded-md" id="SUBCAP${capituloIndex}${subcapituloIndex}">
<div class="flex flex-row justify-between">
<div>
<h6 class="text-md text-black font-bold mb-2">Capítulo ${capituloNum}.${subcapituloIndex + 1}</h6>
</div>
<div>
<svg onclick="removeElement('SUBCAP${capituloIndex}${subcapituloIndex}')" class="hidden w-6 h-6 text-gray-800 hover:cursor-pointer" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/>
</svg>
</div>
</div>
<div class="space-y-1 mt-2">
<label for="capitulos[${capituloIndex}][subcapitulos][${subcapituloIndex}][titulo]" class="block text-sm font-medium text-black">Título</label>
<input type="text" name="capitulos[${capituloIndex}][subcapitulos][${subcapituloIndex}][titulo]" class="flex h-10 w-full rounded-md border-gray-400 border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" >
</div>
<div class="gap-4 text-black">
<div class="space-y-1">
<label for="capitulos[${capituloIndex}][subcapitulos][${subcapituloIndex}][contenido]" class="block text-sm font-medium text-gray-900">Contenido</label>
<input type="hidden" name="capitulos[${capituloIndex}][subcapitulos][${subcapituloIndex}][contenido]" id="contenidoSubCapitulo${capituloIndex}${subcapituloIndex}">
<div id="editor-subcapitulo-${capituloIndex}-${subcapituloIndex}" class="min-h-[150px] bg-white" >
<h2>Titulo de contenido</h2>
<p>Agrega <code>texto con formato</code> al contenido de tu curso.</p>
</div>
</div>
</div>
<div class="space-y-1 mt-2">
<label for="capitulos[${capituloIndex}][subcapitulos][${subcapituloIndex}][video]" class="block text-sm font-medium text-black">Video</label>
<div class="flex flex-col flex-grow mb-3 w-full">
<div x-data="{ files: null }" id="FileUpload"
class="block w-full py-2 px-3 relative bg-white appearance-none border-2 border-gray-300 border-solid rounded-md hover:shadow-outline-gray">
<input type="file" multiple name="capitulos[${capituloIndex}][subcapitulos][${subcapituloIndex}][video]"
class="absolute inset-0 z-50 m-0 p-0 w-full h-full outline-none opacity-0"
x-on:change="files = $event.target.files; console.log($event.target.files);"
x-on:dragover="$el.classList.add('active')" x-on:dragleave="$el.classList.remove('active')" x-on:drop="$el.classList.remove('active')"
>
<template x-if="files !== null">
<div class="flex flex-col space-y-1">
<template x-for="(_,index) in Array.from({ length: files.length })">
<div class="flex flex-row items-center space-x-2">
<template
x-if="files[index].type.includes('audio/')"><i class="far fa-file-audio fa-fw"></i></template>
<template
x-if="files[index].type.includes('application/')"><i class="far fa-file-alt fa-fw"></i></template>
<template
x-if="files[index].type.includes('image/')"><i class="far fa-file-image fa-fw"></i></template>
<template
x-if="files[index].type.includes('video/')"><i class="far fa-file-video fa-fw"></i></template>
<span class="font-medium text-gray-900" x-text="files[index].name">Cargando</span>
<span class="text-xs self-end text-gray-500" x-text="filesize(files[index].size)">...</span>
</div>
</template>
</div>
</template>
<template x-if="files === null">
<div class="flex flex-col space-y-2 p-6 items-center justify-center">
<i class="fas fa-cloud-upload-alt fa-3x text-currentColor"></i>
<p class="text-gray-700">Da clic para abrir el explorador de archivos.</p>
<a href="javascript:void(0)"
class="flex items-center mx-auto py-2 px-4 text-white text-center font-medium border border-transparent rounded-md outline-none bg-gray-700">Seleccionar archivos</a>
</div>
</template>
</div>
</div>
</div>
</div>
`;
let despuesDe = document.getElementById(`subcapitulos-container-${capituloIndex}`);
despuesDe.insertAdjacentHTML('beforeend', subcapituloHtml);
var quillSubCapitulo = new Quill(`#editor-subcapitulo-${capituloIndex}-${subcapituloIndex}`, {
theme: 'snow'
});
function updateSubCapitulo() {
var content = quillSubCapitulo.root.innerHTML;
document.getElementById(`contenidoSubCapitulo${capituloIndex}${subcapituloIndex}`).value = content;
}
quillSubCapitulo.on('text-change', updateSubCapitulo);
}
});
</script>
@endsection
Descripcion de Codigo
Encabezado y Navegación
Título de la página: "Panel de Administración - Crear curso".
Un enlace para crear el curso (actualmente con opacidad 0).
Mensajes de Error
Se muestra un mensaje de error si hay algún error en la sesión (
session('error')).
Formulario de Creación de Curso
Título del Curso: Campo de texto para ingresar el título.
Estado del Curso: Selección para definir el estado del curso (Abierto o Cerrado).
Precio del Curso: Campo numérico para ingresar el precio.
Recursos: Área para subir archivos con un área de arrastrar y soltar y vista previa de los archivos seleccionados.
Imagen: Sección para cargar una imagen con vista previa y botón para limpiar la selección.
Descripción: Área de texto para la descripción del curso.
Categoría: Campo para agregar categorías con un botón para añadirlas y visualización de las categorías agregadas.
Contenido: Editor de texto enriquecido (Quill) para agregar contenido al curso.
Capítulos y Preguntas: Espacios para agregar capítulos y preguntas del examen. Los capítulos se gestionan mediante un botón de añadir (aunque no se muestra completamente en el código) y las preguntas se añaden dinámicamente con un contador y opciones para respuestas correctas.
Botones de Acción
Añadir Capítulo: Botón para agregar capítulos al curso.
Agregar Pregunta: Botón para agregar preguntas al examen del curso.
Guardar: Botón para guardar el curso.
Scripts y Funcionalidades Adicionales
Carga de Imagen y Vista Previa: Script para mostrar la imagen seleccionada y para limpiar la selección.
Categorías Dinámicas: Script para añadir y eliminar categorías dinámicamente, actualizando un campo oculto con las categorías seleccionadas.
Editor de Contenido: Integración del editor de texto enriquecido Quill para el contenido del curso.
Preguntas Dinámicas: Script para agregar preguntas al examen, con opciones de respuesta y la capacidad de eliminar preguntas.
Selección de Respuestas Correctas: Función para seleccionar la respuesta correcta para cada pregunta del examen.
Diseño y EstiloCSS y Tailwind: Uso de clases de Tailwind CSS para el diseño y estilo de los elementos.
Alpine.js: Utilizado para manejar la carga de archivos y la gestión dinámica de categorías.
Usuario Vistas
Certificado
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Certificado de Curso</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
<style>
@page {
size: A4 landscape; /* Cambiado a horizontal */
margin: 0;
}
body {
margin: 0;
padding: 25px;
display: flex;
justify-content: center;
align-items: center;
height: 100vh; /* Altura máxima del viewport */
}
.certificate {
position: relative;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 0px 50px 0px 50px;
width: calc(100% - 100px);
background: url('{{ public_path('images/web/logo-dark.png') }}') no-repeat center center; /* Imagen de fondo */
background-size: contain;
background-color: rgb(199, 229, 255);
max-width: 1120px; /* Aumentado para mayor ancho en horizontal */
text-align: center;
display: flex; /* Flexbox para centrar contenido */
flex-direction: column; /* Orientación vertical */
justify-content: center; /* Centrado vertical */
align-items: center; /* Centrado horizontal */
max-height: calc(100vh - 100px); /* Limita la altura máxima */
}
.logo {
position: absolute; /* Posición absoluta para colocar el logo */
top: 5px; /* Ajusta la posición vertical */
right: 10px; /* Ajusta la posición horizontal a la derecha */
}
.laravel-logo {
position: absolute; /* Posición absoluta para colocar el logo de Laravel */
top: 30px; /* Ajusta la posición vertical */
left: 30px; /* Ajusta la posición horizontal a la izquierda */
}
h1 {
font-size: 2.5em;
color: #001437; /* text-gray-800 */
text-transform: uppercase;
margin-top: 21px;
font-family: 'Inter', sans-serif; /* Usando fuente de Tailwind */
}
h2 {
font-size: 2em;
color: #001433; /* text-gray-600 */
margin-bottom: 24px;
font-style: italic;
font-family: 'Inter', sans-serif; /* Usando fuente de Tailwind */
}
p {
font-size: 1.125em; /* text-lg */
color: #4A5568; /* text-gray-700 */
margin: 10px 0;
font-family: 'Inter', sans-serif; /* Usando fuente de Tailwind */
}
.issuer {
margin-top: 5px;
font-size: 1.125em; /* text-lg */
color: #000000; /* text-gray-600 */
font-family: 'Inter', sans-serif; /* Usando fuente de Tailwind */
}
.date {
margin-top: 5px;
padding-bottom: 40px;
font-size: 0.875em; /* text-sm */
color: #313131; /* text-gray-500 */
font-family: 'Inter', sans-serif; /* Usando fuente de Tailwind */
}
.signature {
margin-top: 130px; /* Mayor separación para la firma */
text-align: center; /* Centrar texto */
}
.signature-line {
border-top: 1px solid black; /* Línea para la firma */
width: 195px; /* Ancho de la línea */
margin: 0 auto; /* Centrar línea */
}
/* Ajustes de tabla */
.content-table {
width: 100%;
border-collapse: collapse;
margin-top: 110px;
}
.content-table td {
padding: 10px;
vertical-align: top;
font-family: 'Inter', sans-serif; /* Usando fuente de Tailwind */
text-align: left; /* Alinear texto a la izquierda */
}
</style>
</head>
<body>
<div class="certificate">
<div class="laravel-logo">
<img src="{{ public_path('images/web/certificado-button.png') }}" alt="Laravel Logo" style="width: 120px;"> <!-- Logo de Laravel en la parte izquierda -->
</div>
<div class="logo">
<img src="{{ public_path('images/web/logo-light.png') }}" alt="STCS Logo" style="width: 190px;"> <!-- Logo en la parte derecha -->
</div>
<h1>Certificado de Finalización</h1>
<h2>{{ $courseName }}</h2>
<table class="content-table">
<tr>
<td>Este certificado se otorga a <strong>{{ $userName }}</strong></td>
</tr>
<tr>
<td>Correo: <strong>{{ $userEmail }}</strong></td>
</tr>
<tr>
<td>Género: <strong>{{ $userGender }}</strong></td>
</tr>
<!--
<tr>
<td>Por haber completado el curso en un tiempo total de <strong>{{ $totalTime }} horas</strong></td>
</tr>
-->
<tr>
<td>Con una Acreditación del <strong>{{ $examScore }}</strong>%</td>
</tr>
<tr>
<td>Este curso está acreditado por STCS en México y se ha realizado en la plataforma de cursos web.</td>
</tr>
</table>
<div class="signature">
<div class="signature-line"></div> <!-- Línea para la firma -->
<p style="color: black">Firma del Instructor</p>
</div>
<div class="issuer">
STCS - México
</div>
<div class="date">
Fecha de emisión: {{ $date }}
</div>
</div>
</body>
</html>
Descripción de Código
A continuación se muestra la vista para el certificado de Termino de Curso.
Configuración de Página:
La página está configurada en formato horizontal A4 (landscape) con márgenes nulos para aprovechar todo el espacio disponible.
Cuerpo y Diseño General:
El cuerpo de la página se centra utilizando Flexbox para alinear el contenido vertical y horizontalmente en el centro del viewport.
El contenedor principal del certificado tiene un fondo con una imagen (logo de STCS) y un color de fondo azul claro (rgb(199, 229, 255)), con un sombreado y bordes redondeados para darle un efecto de tarjeta.
Elementos del Certificado:
Logos:
Logo de Laravel: Posicionado en la parte superior izquierda.
Logo STCS: Posicionado en la parte superior derecha.
Título: "Certificado de Finalización" en grande, centrado, en color oscuro.
Subtítulo: Nombre del curso en un tamaño ligeramente menor, en cursiva.
Tabla de Contenido:
Muestra información relevante sobre el destinatario del certificado, como el nombre, correo electrónico, género y puntuación del examen. La tabla también incluye una línea de firma y una sección para el nombre del emisor y la fecha de emisión.
Estilo de Texto:
Encabezados: Usan la fuente 'Inter' proporcionada por Tailwind CSS.
Párrafos y Datos: Usan una fuente legible y colores contrastantes para asegurar la claridad del texto.
Firma y Fecha:
Firma: Una línea para que se pueda añadir la firma del instructor.
Emisor: Información sobre el emisor del certificado (STCS - México).
Fecha: La fecha de emisión del certificado.
Ajustes de Estilo:
Se ha establecido un ancho máximo para el certificado para adaptarse bien al formato horizontal A4.
Se ha añadido un espaciado específico para la firma y la tabla de contenido para asegurar que el diseño se mantenga ordenado y profesional.
Last updated