Initial commit: Lash Vanshy - Complete project with admin panel, gallery, products, and contact
This commit is contained in:
18
.editorconfig
Executable file
18
.editorconfig
Executable file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
65
.env.example
Executable file
65
.env.example
Executable file
@@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
11
.gitattributes
vendored
Executable file
11
.gitattributes
vendored
Executable file
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
25
.gitignore
vendored
Executable file
25
.gitignore
vendored
Executable file
@@ -0,0 +1,25 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
_ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
352
PLAN_TECNICO.md
Executable file
352
PLAN_TECNICO.md
Executable file
@@ -0,0 +1,352 @@
|
||||
# PLAN DE DESARROLLO: Lash Vanshy
|
||||
|
||||
## 1. Estructura del Proyecto
|
||||
|
||||
```
|
||||
lash/
|
||||
├── app/
|
||||
│ ├── Http/
|
||||
│ │ ├── Controllers/
|
||||
│ │ │ ├── Frontend/
|
||||
│ │ │ │ ├── HomeController.php
|
||||
│ │ │ │ ├── GaleriaController.php # Modelos
|
||||
│ │ │ │ ├── ProductoController.php
|
||||
│ │ │ │ └── ContactoController.php
|
||||
│ │ │ └── Admin/
|
||||
│ │ │ ├── DashboardController.php
|
||||
│ │ │ ├── GaleriaController.php
|
||||
│ │ │ ├── ProductoController.php
|
||||
│ │ │ ├── ContactoController.php
|
||||
│ │ │ └── AdminUserController.php
|
||||
│ │ ├── Middleware/
|
||||
│ │ │ └── AdminAuth.php
|
||||
│ │ └── Requests/
|
||||
│ │ ├── GaleriaRequest.php
|
||||
│ │ ├── ProductoRequest.php
|
||||
│ │ └── AdminUserRequest.php
|
||||
│ ├── Models/
|
||||
│ │ ├── Galeria.php
|
||||
│ │ ├── Producto.php
|
||||
│ │ ├── Mensaje.php
|
||||
│ │ ├── Configuracion.php
|
||||
│ │ └── AdminUser.php
|
||||
│ └── Providers/
|
||||
├── database/
|
||||
│ └── migrations/
|
||||
├── public/
|
||||
│ ├── assets/
|
||||
│ │ ├── css/
|
||||
│ │ ├── js/
|
||||
│ │ └── images/
|
||||
│ └── uploads/
|
||||
│ ├── galeria/
|
||||
│ └── productos/
|
||||
├── resources/
|
||||
│ ├── views/
|
||||
│ │ ├── frontend/
|
||||
│ │ │ ├── layouts/
|
||||
│ │ │ ├── home/
|
||||
│ │ │ ├── galeria/
|
||||
│ │ │ ├── productos/
|
||||
│ │ │ └── contacto/
|
||||
│ │ └── admin/
|
||||
│ │ ├── layouts/
|
||||
│ │ ├── dashboard/
|
||||
│ │ ├── galeria/
|
||||
│ │ ├── productos/
|
||||
│ │ ├── mensajes/
|
||||
│ │ └── usuarios/
|
||||
│ └── assets/
|
||||
│ └── sass/
|
||||
├── routes/
|
||||
│ ├── web.php
|
||||
│ └── admin.php
|
||||
└── config/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Modelos de Base de Datos
|
||||
|
||||
### Tabla: admin_users
|
||||
```sql
|
||||
- id (bigint, PK)
|
||||
- name (varchar 255)
|
||||
- email (varchar 255, unique)
|
||||
- password (varchar 255)
|
||||
- rol (enum: 'super_admin', 'admin')
|
||||
- avatar (varchar 255, nullable)
|
||||
- created_at (timestamp)
|
||||
- updated_at (timestamp)
|
||||
```
|
||||
|
||||
### Tabla: galerias (Modelos - fotos/videos)
|
||||
```sql
|
||||
- id (bigint, PK)
|
||||
- titulo (varchar 255)
|
||||
- descripcion (text, nullable)
|
||||
- tipo (enum: 'imagen', 'video')
|
||||
- archivo (varchar 255)
|
||||
- thumbnail (varchar 255, nullable) - para videos
|
||||
- orden (int, default 0)
|
||||
- activo (boolean, default true)
|
||||
- created_at (timestamp)
|
||||
- updated_at (timestamp)
|
||||
```
|
||||
|
||||
### Tabla: productos
|
||||
```sql
|
||||
- id (bigint, PK)
|
||||
- nombre (varchar 255)
|
||||
- descripcion (text)
|
||||
- precio (decimal 10,2)
|
||||
- imagen (varchar 255, nullable)
|
||||
- categoria (varchar 100)
|
||||
- destacado (boolean, default false)
|
||||
- activo (boolean, default true)
|
||||
- orden (int, default 0)
|
||||
- created_at (timestamp)
|
||||
- updated_at (timestamp)
|
||||
```
|
||||
|
||||
### Tabla: mensajes (contacto)
|
||||
```sql
|
||||
- id (bigint, PK)
|
||||
- nombre (varchar 255)
|
||||
- email (varchar 255)
|
||||
- telefono (varchar 50, nullable)
|
||||
- mensaje (text)
|
||||
- leido (boolean, default false)
|
||||
- created_at (timestamp)
|
||||
- updated_at (timestamp)
|
||||
```
|
||||
|
||||
### Tabla: configuraciones
|
||||
```sql
|
||||
- id (bigint, PK)
|
||||
- clave (varchar 100, unique)
|
||||
- valor (text)
|
||||
- descripcion (varchar 255)
|
||||
- created_at (timestamp)
|
||||
- updated_at (timestamp)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Rutas y Endpoints
|
||||
|
||||
### Rutas Frontend (Públicas)
|
||||
| Método | URL | Controlador | Acción |
|
||||
|--------|-----|-------------|--------|
|
||||
| GET | / | HomeController | index |
|
||||
| GET | /modelos | GaleriaController | index |
|
||||
| GET | /productos | ProductoController | index |
|
||||
| GET | /contacto | ContactoController | index |
|
||||
| POST | /contacto | ContactoController | send |
|
||||
|
||||
### Rutas Admin
|
||||
| Método | URL | Controlador | Acción |
|
||||
|--------|-----|-------------|--------|
|
||||
| GET | /admin/login | AuthController | showLogin |
|
||||
| POST | /admin/login | AuthController | login |
|
||||
| POST | /admin/logout | AuthController | logout |
|
||||
| GET | /admin | DashboardController | index |
|
||||
| GET | /admin/modelos | GaleriaController | index |
|
||||
| GET | /admin/modelos/create | GaleriaController | create |
|
||||
| POST | /admin/modelos | GaleriaController | store |
|
||||
| GET | /admin/modelos/{id}/edit | GaleriaController | edit |
|
||||
| PUT | /admin/modelos/{id} | GaleriaController | update |
|
||||
| DELETE | /admin/modelos/{id} | GaleriaController | destroy |
|
||||
| GET | /admin/productos | ProductoController | index |
|
||||
| GET | /admin/productos/create | ProductoController | create |
|
||||
| POST | /admin/productos | ProductoController | store |
|
||||
| GET | /admin/productos/{id}/edit | ProductoController | edit |
|
||||
| PUT | /admin/productos/{id} | ProductoController | update |
|
||||
| DELETE | /admin/productos/{id} | ProductoController | destroy |
|
||||
| GET | /admin/mensajes | MensajeController | index |
|
||||
| GET | /admin/mensajes/{id} | MensajeController | show |
|
||||
| PUT | /admin/mensajes/{id}/leido | MensajeController | markRead |
|
||||
| DELETE | /admin/mensajes/{id} | MensajeController | destroy |
|
||||
| GET | /admin/usuarios | AdminUserController | index |
|
||||
| GET | /admin/usuarios/create | AdminUserController | create |
|
||||
| POST | /admin/usuarios | AdminUserController | store |
|
||||
| GET | /admin/usuarios/{id}/edit | AdminUserController | edit |
|
||||
| PUT | /admin/usuarios/{id} | AdminUserController | update |
|
||||
| DELETE | /admin/usuarios/{id} | AdminUserController | destroy |
|
||||
|
||||
---
|
||||
|
||||
## 4. Componentes Frontend
|
||||
|
||||
### Estructura Visual (Rosa Pastel + Blanco)
|
||||
|
||||
**Paleta de colores:**
|
||||
- Primario: Rosa pastel (#F8B4C4 o similar)
|
||||
- Fondo: Blanco (#FFFFFF)
|
||||
- Acento: Rosa más oscuro (#E89AAD)
|
||||
- Texto: Gris oscuro (#4A4A4A)
|
||||
- Secundario: Crema (#FFF5F7)
|
||||
|
||||
### Páginas Frontend
|
||||
|
||||
1. **Home** (`/`)
|
||||
- Hero con imagen de pestañas
|
||||
- Breve descripción del negocio
|
||||
- Servicios destacados
|
||||
- Call to action (reservar cita)
|
||||
- Testimonios (gestionado desde admin)
|
||||
- Footer con contacto básico
|
||||
|
||||
2. **Modelos (Galería)** (`/modelos`)
|
||||
- Grid de imágenes/videos de trabajos
|
||||
- Filtro por tipo (imagen/video)
|
||||
- Lightbox para ampliar imágenes
|
||||
- Videos con thumbnail reproducibles
|
||||
|
||||
3. **Productos** (`/productos`)
|
||||
- Lista de servicios/tratamientos
|
||||
- Precio visible
|
||||
- Descripción del servicio
|
||||
- Imagen representativa
|
||||
- NO tiene carrito, solo mostrar
|
||||
|
||||
4. **Contacto** (`/contacto`)
|
||||
- Formulario: nombre, email, teléfono, mensaje
|
||||
- Datos de contacto (teléfono, ubicación)
|
||||
- Mapa (opcional)
|
||||
- Horario de atención
|
||||
|
||||
---
|
||||
|
||||
## 5. Módulos de Administración
|
||||
|
||||
### Dashboard
|
||||
- Estadísticas básicas (mensajes nuevos, modelos, productos)
|
||||
- Acceso rápido a secciones
|
||||
|
||||
### Gestión Modelos (CRUD)
|
||||
- Listado con paginación
|
||||
- Crear: título, descripción, tipo, archivo, orden
|
||||
- Editar: todos los campos
|
||||
- Eliminar (soft delete)
|
||||
- Reordenar mediante drag & drop o campo orden
|
||||
|
||||
### Gestión Productos (CRUD)
|
||||
- Listado con paginación
|
||||
- Crear: nombre, descripción, precio, imagen, categoría, destacado
|
||||
- Editar: todos los campos
|
||||
- Eliminar
|
||||
- Filtrar por categoría
|
||||
- Marcar como destacado
|
||||
|
||||
### Gestión Mensajes
|
||||
- Listado de mensajes recibidos
|
||||
- Ver mensaje completo
|
||||
- Marcar como leído/no leído
|
||||
- Eliminar mensaje
|
||||
- Contador de mensajes sin leer
|
||||
|
||||
### Gestión Administradores
|
||||
- Listado de usuarios admin
|
||||
- Crear nuevo admin (solo super_admin)
|
||||
- Editar admin
|
||||
- Eliminar admin
|
||||
- Cambiar rol (admin/super_admin)
|
||||
|
||||
### Configuración
|
||||
- Datos del negocio (nombre, teléfono, email, dirección)
|
||||
- Redes sociales
|
||||
- Textos personalizados del Home
|
||||
|
||||
---
|
||||
|
||||
## 6. Dependencias Necessárias
|
||||
|
||||
### Composer (Laravel)
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"laravel/framework": "^10.0",
|
||||
"intervention/image": "^2.7", // Manejo de imágenes
|
||||
"laravel/sanctum": "^3.3" // Autenticación API
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPM (Frontend)
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"sass": "^1.69",
|
||||
"bootstrap": "^5.3",
|
||||
"axios": "^1.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Pasos de Implementación
|
||||
|
||||
### Fase 1: Configuración Inicial
|
||||
1. Crear proyecto Laravel nuevo
|
||||
2. Configurar conexión BD en `.env`
|
||||
3. Configurar Apache (VirtualHost)
|
||||
4. Instalar dependencias composer
|
||||
5. Configurar黏膜 Storage para uploads
|
||||
|
||||
### Fase 2: Base de Datos
|
||||
1. Crear migrations para todas las tablas
|
||||
2. Ejecutar migraciones
|
||||
3. Crear seeders con datos iniciales
|
||||
|
||||
### Fase 3: Modelos y Controladores Backend
|
||||
1. Crear modelos Eloquent
|
||||
2. Crear controladores de admin
|
||||
3. Crear requests para validación
|
||||
4. Implementar lógica CRUD
|
||||
|
||||
### Fase 4: Autenticación Admin
|
||||
1. Implementar login/logout
|
||||
2. Crear middleware de autenticación
|
||||
3. Proteger rutas de admin
|
||||
4. Manejo de sesiones seguras
|
||||
|
||||
### Fase 5: Vistas Frontend
|
||||
1. Crear layouts (header, footer)
|
||||
2. Implementar Home con diseño pastel
|
||||
3. Implementar Galería Modelos
|
||||
4. Implementar Productos
|
||||
5. Implementar Contacto
|
||||
|
||||
### Fase 6: Vistas Admin
|
||||
1. Crear layout admin
|
||||
2. Dashboard con estadísticas
|
||||
3. CRUD Modelos con upload de archivos
|
||||
4. CRUD Productos con upload de imágenes
|
||||
5. Gestión de Mensajes
|
||||
6. Gestión de Usuarios Admin
|
||||
|
||||
### Fase 7: Funcionalidades Extras
|
||||
1. Configuraciones editables
|
||||
2. Reordenar elementos (drag & drop)
|
||||
3. Exportar/marcar mensajes como leídos
|
||||
|
||||
### Fase 8: Seguridad y Optimización
|
||||
1. Validación CSRF en formularios
|
||||
2. Sanitización de inputs
|
||||
3. Headers de seguridad
|
||||
4. Optimización de imágenes
|
||||
5. Cache de rutas
|
||||
|
||||
---
|
||||
|
||||
## 8. Consideraciones de Diseño
|
||||
|
||||
- **Estilo**: Elegante, minimalista, profesional
|
||||
- **Colores**: Rosa pastel (#F8B4C4), Blanco, acentos en gris suave
|
||||
- **Tipografía**: Sans-serif limpia (ej: Poppins, Open Sans)
|
||||
- **Imágenes**: Alta calidad, optimizadas
|
||||
- **Responsive**: Mobile-first, funcional en todos dispositivos
|
||||
- **UX**: Navegación intuitiva, tiempos de carga rápidos
|
||||
58
README.md
Executable file
58
README.md
Executable file
@@ -0,0 +1,58 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||
|
||||
## Agentic Development
|
||||
|
||||
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||
|
||||
```bash
|
||||
composer require laravel/boost --dev
|
||||
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
83
app/Helpers/helpers.php
Executable file
83
app/Helpers/helpers.php
Executable file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Configuracion;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* Helper para obtener configuraciones del sitio
|
||||
*
|
||||
* @param string $clave La clave de configuración
|
||||
* @param mixed $default Valor por defecto si no existe
|
||||
* @return mixed
|
||||
*/
|
||||
if (! function_exists('config_site')) {
|
||||
function config_site(string $clave, $default = null)
|
||||
{
|
||||
return Configuracion::get($clave, $default);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para establecer una configuración
|
||||
*
|
||||
* @param string $clave La clave de configuración
|
||||
* @param mixed $valor El valor a guardar
|
||||
* @return Configuracion
|
||||
*/
|
||||
if (! function_exists('set_config_site')) {
|
||||
function set_config_site(string $clave, $valor): Configuracion
|
||||
{
|
||||
return Configuracion::set($clave, $valor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para obtener todas las configuraciones como array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
if (! function_exists('all_config_site')) {
|
||||
function all_config_site(): array
|
||||
{
|
||||
return Configuracion::allAsArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para verificar si el usuario actual es admin
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
if (! function_exists('is_admin')) {
|
||||
function is_admin(): bool
|
||||
{
|
||||
return Auth::guard('admin')->check();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para obtener el usuario admin actual
|
||||
*
|
||||
* @return AdminUser|null
|
||||
*/
|
||||
if (! function_exists('admin_user')) {
|
||||
function admin_user(): ?AdminUser
|
||||
{
|
||||
return Auth::guard('admin')->user();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para verificar si el usuario actual es super_admin
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
if (! function_exists('is_super_admin')) {
|
||||
function is_super_admin(): bool
|
||||
{
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
return $user && $user->isSuperAdmin();
|
||||
}
|
||||
}
|
||||
156
app/Http/Controllers/Admin/AdminUserController.php
Executable file
156
app/Http/Controllers/Admin/AdminUserController.php
Executable file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AdminUserRequest;
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AdminUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar lista de usuarios admin
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$users = AdminUser::orderBy('created_at', 'desc')->paginate(15);
|
||||
|
||||
return view('admin.usuarios.index', compact('users'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de creación
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.usuarios.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar nuevo usuario admin
|
||||
*/
|
||||
public function store(AdminUserRequest $request): RedirectResponse
|
||||
{
|
||||
// Verificar permisos - solo super_admin puede crear otros admins
|
||||
$currentUser = Auth::guard('admin')->user();
|
||||
|
||||
if (! $currentUser || ! $currentUser->isSuperAdmin()) {
|
||||
return back()->withErrors(['error' => 'No tienes permisos para crear administradores.']);
|
||||
}
|
||||
|
||||
// Si no es super_admin, no puede crear super_admin
|
||||
if ($request->rol === 'super_admin' && ! $currentUser->isSuperAdmin()) {
|
||||
return back()->withErrors(['error' => 'No tienes permisos para crear super administradores.']);
|
||||
}
|
||||
|
||||
$data = $request->validated();
|
||||
|
||||
// Manejar upload de avatar
|
||||
if ($request->hasFile('avatar')) {
|
||||
$data['avatar'] = $this->uploadFile($request->file('avatar'), 'avatars');
|
||||
}
|
||||
|
||||
AdminUser::create($data);
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', 'Usuario administrativo creado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de edición
|
||||
*/
|
||||
public function edit(AdminUser $admin_user): \Illuminate\Contracts\View\View|RedirectResponse
|
||||
{
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
// Solo super_admin puede editar otros usuarios
|
||||
// Un usuario puede editar su propio perfil
|
||||
if (! $user->isSuperAdmin() && $user->id !== $admin_user->id) {
|
||||
return back()->withErrors(['error' => 'No tienes permisos para editar este usuario.']);
|
||||
}
|
||||
|
||||
return view('admin.usuarios.edit', compact('admin_user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar usuario admin
|
||||
*/
|
||||
public function update(AdminUserRequest $request, AdminUser $admin_user): RedirectResponse
|
||||
{
|
||||
// Verificar permisos
|
||||
$currentUser = Auth::guard('admin')->user();
|
||||
|
||||
// Un usuario puede editar su propio perfil, pero no cambiar su rol
|
||||
if ($currentUser->id === $admin_user->id) {
|
||||
// No permitir cambiar rol de uno mismo
|
||||
if ($request->has('rol') && $request->rol !== $admin_user->rol) {
|
||||
return back()->withErrors(['error' => 'No puedes cambiar tu propio rol.']);
|
||||
}
|
||||
} elseif (! $currentUser->isSuperAdmin()) {
|
||||
return back()->withErrors(['error' => 'No tienes permisos para editar este usuario.']);
|
||||
}
|
||||
|
||||
// Si no es super_admin, no puede crear/asignar super_admin
|
||||
if ($request->rol === 'super_admin' && ! $currentUser->isSuperAdmin()) {
|
||||
return back()->withErrors(['error' => 'No tienes permisos para asignar rol de super administrador.']);
|
||||
}
|
||||
|
||||
$data = $request->validated();
|
||||
|
||||
// Si no se proporciona password, eliminar del array
|
||||
if (empty($data['password'])) {
|
||||
unset($data['password']);
|
||||
}
|
||||
|
||||
// Manejar upload de avatar
|
||||
if ($request->hasFile('avatar')) {
|
||||
// Eliminar avatar anterior
|
||||
if ($admin_user->avatar) {
|
||||
Storage::disk('public')->delete($admin_user->avatar);
|
||||
}
|
||||
$data['avatar'] = $this->uploadFile($request->file('avatar'), 'avatars');
|
||||
}
|
||||
|
||||
$admin_user->update($data);
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', 'Usuario administrativo actualizado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar usuario admin
|
||||
*/
|
||||
public function destroy(AdminUser $admin_user): RedirectResponse
|
||||
{
|
||||
// Verificar permisos - solo super_admin puede eliminar
|
||||
$currentUser = Auth::guard('admin')->user();
|
||||
|
||||
if (! $currentUser || ! $currentUser->isSuperAdmin()) {
|
||||
return back()->withErrors(['error' => 'No tienes permisos para eliminar administradores.']);
|
||||
}
|
||||
|
||||
// No permitir eliminarse a sí mismo
|
||||
if ($currentUser->id === $admin_user->id) {
|
||||
return back()->withErrors(['error' => 'No puedes eliminar tu propia cuenta.']);
|
||||
}
|
||||
|
||||
// Eliminar avatar
|
||||
if ($admin_user->avatar) {
|
||||
Storage::disk('public')->delete($admin_user->avatar);
|
||||
}
|
||||
|
||||
$admin_user->delete();
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', 'Usuario administrativo eliminado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Subir archivo a almacenamiento
|
||||
*/
|
||||
private function uploadFile($file, string $directory): string
|
||||
{
|
||||
return $file->store($directory, 'public');
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/Admin/AuthController.php
Executable file
68
app/Http/Controllers/Admin/AuthController.php
Executable file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\LoginRequest;
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar formulario de login
|
||||
*/
|
||||
public function showLogin(): \Illuminate\Contracts\View\View|RedirectResponse
|
||||
{
|
||||
if (Auth::guard('admin')->check()) {
|
||||
return redirect()->route('admin.dashboard');
|
||||
}
|
||||
|
||||
return view('admin.auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar login
|
||||
*/
|
||||
public function login(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$credentials = $request->only('email', 'password');
|
||||
|
||||
// Buscar usuario por email
|
||||
$adminUser = AdminUser::where('email', $credentials['email'])->first();
|
||||
|
||||
if (! $adminUser) {
|
||||
return back()->withErrors([
|
||||
'email' => 'Las credenciales no son válidas.',
|
||||
])->withInput($request->except('password'));
|
||||
}
|
||||
|
||||
// Verificar password
|
||||
if (! $adminUser->validatePassword($credentials['password'])) {
|
||||
return back()->withErrors([
|
||||
'email' => 'Las credenciales no son válidas.',
|
||||
])->withInput($request->except('password'));
|
||||
}
|
||||
|
||||
// Login manual
|
||||
Auth::guard('admin')->login($adminUser);
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('admin.dashboard'))->with('success', 'Bienvenido al panel de administración.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar sesión
|
||||
*/
|
||||
public function logout(): RedirectResponse
|
||||
{
|
||||
Auth::guard('admin')->logout();
|
||||
|
||||
request()->session()->invalidate();
|
||||
request()->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('admin.login')->with('success', 'Sesión cerrada correctamente.');
|
||||
}
|
||||
}
|
||||
56
app/Http/Controllers/Admin/ConfiguracionController.php
Executable file
56
app/Http/Controllers/Admin/ConfiguracionController.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Configuracion;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConfiguracionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar configuración
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$configuracion = Configuracion::allAsArray();
|
||||
|
||||
return view('admin.configuracion.index', compact('configuracion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar configuración
|
||||
*/
|
||||
public function update(): RedirectResponse
|
||||
{
|
||||
$fields = [
|
||||
'nombre_sitio',
|
||||
'telefono',
|
||||
'email',
|
||||
'direccion',
|
||||
'horario',
|
||||
'facebook',
|
||||
'instagram',
|
||||
'whatsapp',
|
||||
'tiktok',
|
||||
'youtube',
|
||||
'seo_titulo',
|
||||
'seo_descripcion',
|
||||
];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$value = request($field);
|
||||
|
||||
// Only save if the field has a value
|
||||
if ($value !== null && $value !== '') {
|
||||
Configuracion::set($field, $value);
|
||||
} else {
|
||||
// Optionally clear empty fields
|
||||
Configuracion::remove($field);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('admin.configuracion.index')->with('success', 'Configuración guardada correctamente.');
|
||||
}
|
||||
}
|
||||
32
app/Http/Controllers/Admin/DashboardController.php
Executable file
32
app/Http/Controllers/Admin/DashboardController.php
Executable file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Galeria;
|
||||
use App\Models\Mensaje;
|
||||
use App\Models\Producto;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar el dashboard principal
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
// Obtener estadísticas
|
||||
$stats = [
|
||||
'mensajes_no_leidos' => Mensaje::noLeidos()->count(),
|
||||
'total_modelos' => Galeria::count(),
|
||||
'total_productos' => Producto::count(),
|
||||
'productos_destacados' => Producto::where('destacado', true)->count(),
|
||||
'modelos_activos' => Galeria::where('activo', true)->count(),
|
||||
];
|
||||
|
||||
// Mensajes recientes
|
||||
$mensajes_recientes = Mensaje::orderBy('created_at', 'desc')->take(5)->get();
|
||||
|
||||
return view('admin.dashboard.index', compact('stats', 'mensajes_recientes'));
|
||||
}
|
||||
}
|
||||
113
app/Http/Controllers/Admin/GaleriaController.php
Executable file
113
app/Http/Controllers/Admin/GaleriaController.php
Executable file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\GaleriaRequest;
|
||||
use App\Models\Galeria;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GaleriaController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$galerias = Galeria::ordenado()->paginate(15);
|
||||
|
||||
return view('admin.galeria.index', compact('galerias'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.galeria.create');
|
||||
}
|
||||
|
||||
public function store(GaleriaRequest $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$data = $request->validated();
|
||||
Log::info('GaleriaRequest validated', $data);
|
||||
|
||||
if ($request->hasFile('archivo')) {
|
||||
Log::info('Has archivo file', ['file' => $request->file('archivo')->getClientOriginalName()]);
|
||||
$data['archivo'] = $this->uploadFile($request->file('archivo'), 'galeria');
|
||||
} else {
|
||||
Log::warning('No archivo file in request');
|
||||
Log::info('All files:', ['files' => $request->allFiles()]);
|
||||
}
|
||||
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
$data['thumbnail'] = $this->uploadFile($request->file('thumbnail'), 'galeria/thumbnails');
|
||||
}
|
||||
|
||||
$data['activo'] = $request->has('activo');
|
||||
$data['orden'] = $data['orden'] ?? 0;
|
||||
|
||||
Galeria::create($data);
|
||||
|
||||
return redirect()->route('admin.galeria.index')->with('success', 'Modelo registrado correctamente.');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error creating Galeria: '.$e->getMessage());
|
||||
|
||||
return redirect()->back()->with('error', 'Error al guardar: '.$e->getMessage())->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function edit(Galeria $galeria): View
|
||||
{
|
||||
return view('admin.galeria.edit', compact('galeria'));
|
||||
}
|
||||
|
||||
public function update(GaleriaRequest $request, Galeria $galeria): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$data = $request->validated();
|
||||
|
||||
if ($request->hasFile('archivo')) {
|
||||
if ($galeria->archivo) {
|
||||
Storage::disk('public')->delete($galeria->archivo);
|
||||
}
|
||||
$data['archivo'] = $this->uploadFile($request->file('archivo'), 'galeria');
|
||||
}
|
||||
|
||||
if ($request->hasFile('thumbnail')) {
|
||||
if ($galeria->thumbnail) {
|
||||
Storage::disk('public')->delete($galeria->thumbnail);
|
||||
}
|
||||
$data['thumbnail'] = $this->uploadFile($request->file('thumbnail'), 'galeria/thumbnails');
|
||||
}
|
||||
|
||||
$data['activo'] = $request->has('activo');
|
||||
$data['orden'] = $data['orden'] ?? $galeria->orden;
|
||||
|
||||
$galeria->update($data);
|
||||
|
||||
return redirect()->route('admin.galeria.index')->with('success', 'Modelo actualizado correctamente.');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating Galeria: '.$e->getMessage());
|
||||
|
||||
return redirect()->back()->with('error', 'Error al actualizar: '.$e->getMessage())->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(Galeria $galeria): RedirectResponse
|
||||
{
|
||||
if ($galeria->archivo) {
|
||||
Storage::disk('public')->delete($galeria->archivo);
|
||||
}
|
||||
if ($galeria->thumbnail) {
|
||||
Storage::disk('public')->delete($galeria->thumbnail);
|
||||
}
|
||||
|
||||
$galeria->delete();
|
||||
|
||||
return redirect()->route('admin.galeria.index')->with('success', 'Modelo eliminado correctamente.');
|
||||
}
|
||||
|
||||
private function uploadFile($file, string $directory): string
|
||||
{
|
||||
return $file->store($directory, 'public');
|
||||
}
|
||||
}
|
||||
70
app/Http/Controllers/Admin/MensajeController.php
Executable file
70
app/Http/Controllers/Admin/MensajeController.php
Executable file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Mensaje;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MensajeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar lista de mensajes
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$mensajes = Mensaje::orderBy('created_at', 'desc')->paginate(15);
|
||||
|
||||
return view('admin.mensajes.index', compact('mensajes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar mensaje específico
|
||||
*/
|
||||
public function show(Mensaje $mensaje): View
|
||||
{
|
||||
// Marcar como leído al ver
|
||||
if (! $mensaje->leido) {
|
||||
$mensaje->marcarLeido();
|
||||
}
|
||||
|
||||
return view('admin.mensajes.show', compact('mensaje'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternar estado de leído/no leído
|
||||
*/
|
||||
public function markRead(Mensaje $mensaje): RedirectResponse
|
||||
{
|
||||
if ($mensaje->leido) {
|
||||
$mensaje->marcarNoLeido();
|
||||
$message = 'Mensaje marcado como no leído.';
|
||||
} else {
|
||||
$mensaje->marcarLeido();
|
||||
$message = 'Mensaje marcado como leído.';
|
||||
}
|
||||
|
||||
return back()->with('success', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar mensaje
|
||||
*/
|
||||
public function destroy(Mensaje $mensaje): RedirectResponse
|
||||
{
|
||||
$mensaje->delete();
|
||||
|
||||
return redirect()->route('admin.mensajes.index')->with('success', 'Mensaje eliminado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar todos los mensajes como leídos
|
||||
*/
|
||||
public function markAllRead(): RedirectResponse
|
||||
{
|
||||
Mensaje::where('leido', false)->update(['leido' => true]);
|
||||
|
||||
return back()->with('success', 'Todos los mensajes marcados como leídos.');
|
||||
}
|
||||
}
|
||||
110
app/Http/Controllers/Admin/ProductoController.php
Executable file
110
app/Http/Controllers/Admin/ProductoController.php
Executable file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ProductoRequest;
|
||||
use App\Models\Producto;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProductoController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar lista de productos
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$productos = Producto::orderBy('orden', 'asc')->orderBy('created_at', 'desc')->paginate(15);
|
||||
|
||||
return view('admin.productos.index', compact('productos'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de creación
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.productos.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar nuevo registro
|
||||
*/
|
||||
public function store(ProductoRequest $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
// Manejar upload de imagen
|
||||
if ($request->hasFile('imagen')) {
|
||||
$data['imagen'] = $this->uploadFile($request->file('imagen'), 'productos');
|
||||
}
|
||||
|
||||
// Valores por defecto
|
||||
$data['destacado'] = $request->has('destacado');
|
||||
$data['activo'] = $request->has('activo');
|
||||
$data['orden'] = $data['orden'] ?? 0;
|
||||
|
||||
Producto::create($data);
|
||||
|
||||
return redirect()->route('admin.productos.index')->with('success', 'Producto registrado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de edición
|
||||
*/
|
||||
public function edit(Producto $producto): View
|
||||
{
|
||||
return view('admin.productos.edit', compact('producto'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar registro
|
||||
*/
|
||||
public function update(ProductoRequest $request, Producto $producto): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
// Manejar upload de imagen si se proporciona una nueva
|
||||
if ($request->hasFile('imagen')) {
|
||||
// Eliminar imagen anterior
|
||||
if ($producto->imagen) {
|
||||
Storage::disk('public')->delete($producto->imagen);
|
||||
}
|
||||
$data['imagen'] = $this->uploadFile($request->file('imagen'), 'productos');
|
||||
}
|
||||
|
||||
// Valores por defecto
|
||||
$data['destacado'] = $request->has('destacado');
|
||||
$data['activo'] = $request->has('activo');
|
||||
$data['orden'] = $data['orden'] ?? $producto->orden;
|
||||
|
||||
$producto->update($data);
|
||||
|
||||
return redirect()->route('admin.productos.index')->with('success', 'Producto actualizado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar registro
|
||||
*/
|
||||
public function destroy(Producto $producto): RedirectResponse
|
||||
{
|
||||
// Eliminar imagen
|
||||
if ($producto->imagen) {
|
||||
Storage::disk('public')->delete($producto->imagen);
|
||||
}
|
||||
|
||||
$producto->delete();
|
||||
|
||||
return redirect()->route('admin.productos.index')->with('success', 'Producto eliminado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Subir archivo a almacenamiento
|
||||
*/
|
||||
private function uploadFile($file, string $directory): string
|
||||
{
|
||||
return $file->store($directory, 'public');
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Executable file
8
app/Http/Controllers/Controller.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
36
app/Http/Controllers/Frontend/GaleriaController.php
Executable file
36
app/Http/Controllers/Frontend/GaleriaController.php
Executable file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Frontend;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Galeria;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GaleriaController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar galería pública
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$tipo = $request->get('tipo', 'todos');
|
||||
|
||||
$query = Galeria::activo()->ordenado();
|
||||
|
||||
if ($tipo !== 'todos') {
|
||||
$query->where('tipo', $tipo);
|
||||
}
|
||||
|
||||
$galeria = $query->paginate(12);
|
||||
|
||||
// Obtener estadísticas para los filtros
|
||||
$stats = [
|
||||
'total' => Galeria::activo()->count(),
|
||||
'imagenes' => Galeria::activo()->where('tipo', 'imagen')->count(),
|
||||
'videos' => Galeria::activo()->where('tipo', 'video')->count(),
|
||||
];
|
||||
|
||||
return view('frontend.galeria.index', compact('galeria', 'stats', 'tipo'));
|
||||
}
|
||||
}
|
||||
49
app/Http/Controllers/Frontend/HomeController.php
Executable file
49
app/Http/Controllers/Frontend/HomeController.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Frontend;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Configuracion;
|
||||
use App\Models\Galeria;
|
||||
use App\Models\Producto;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar página de inicio
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$configuracion = Configuracion::allAsArray();
|
||||
|
||||
// Productos destacados para el home
|
||||
$productosDestacados = Producto::activo()
|
||||
->where('destacado', true)
|
||||
->orderBy('orden', 'asc')
|
||||
->take(6)
|
||||
->get();
|
||||
|
||||
// Galería para mostrar en home (solo activos)
|
||||
$galeria = Galeria::activo()
|
||||
->ordenado()
|
||||
->take(8)
|
||||
->get();
|
||||
|
||||
return view('frontend.home.index', compact(
|
||||
'productosDestacados',
|
||||
'galeria',
|
||||
'configuracion'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar página de contacto
|
||||
*/
|
||||
public function contacto(): View
|
||||
{
|
||||
$configuracion = Configuracion::allAsArray();
|
||||
|
||||
return view('frontend.contacto.index', compact('configuracion'));
|
||||
}
|
||||
}
|
||||
48
app/Http/Controllers/Frontend/ProductoController.php
Executable file
48
app/Http/Controllers/Frontend/ProductoController.php
Executable file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Frontend;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Producto;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProductoController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar lista de productos/servicios
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$categoria = $request->get('categoria', 'todos');
|
||||
|
||||
$query = Producto::activo()->orderBy('orden', 'asc');
|
||||
|
||||
if ($categoria !== 'todos') {
|
||||
$query->where('categoria', $categoria);
|
||||
}
|
||||
|
||||
$productos = $query->paginate(12);
|
||||
|
||||
// Obtener categorías únicas
|
||||
$categorias = Producto::activo()
|
||||
->distinct()
|
||||
->pluck('categoria')
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
// Productos destacados
|
||||
$destacados = Producto::activo()
|
||||
->where('destacado', true)
|
||||
->orderBy('orden', 'asc')
|
||||
->take(4)
|
||||
->get();
|
||||
|
||||
return view('frontend.productos.index', compact(
|
||||
'productos',
|
||||
'categorias',
|
||||
'destacados',
|
||||
'categoria'
|
||||
));
|
||||
}
|
||||
}
|
||||
26
app/Http/Middleware/AdminAuth.php
Executable file
26
app/Http/Middleware/AdminAuth.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdminAuth
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* Verifica que el usuario esté autenticado en el guard 'admin'
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (! Auth::guard('admin')->check()) {
|
||||
// Si no está autenticado, redirigir al login
|
||||
return redirect()->route('admin.login')->with('error', 'Debes iniciar sesión para acceder.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
22
app/Http/Middleware/SecurityHeaders.php
Executable file
22
app/Http/Middleware/SecurityHeaders.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SecurityHeaders
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
$response->headers->set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' 'unsafe-inline' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com data:; img-src 'self' data: https:; connect-src 'self' https:;");
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
22
app/Http/Middleware/SuperAdminOnly.php
Executable file
22
app/Http/Middleware/SuperAdminOnly.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SuperAdminOnly
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
if (! $user || ! $user->isSuperAdmin()) {
|
||||
abort(403, 'Acceso restringido a super administrador.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
64
app/Http/Requests/AdminUserRequest.php
Executable file
64
app/Http/Requests/AdminUserRequest.php
Executable file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AdminUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:admin_users,email'],
|
||||
'password' => ['required', 'string', 'min:8', 'max:255'],
|
||||
'rol' => ['required', 'in:super_admin,admin'],
|
||||
'avatar' => ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:5120'],
|
||||
];
|
||||
|
||||
// En actualización, password no es obligatorio (puede mantener el actual)
|
||||
if ($this->isMethod('PATCH') || $this->isMethod('PUT')) {
|
||||
$rules['password'] = ['nullable', 'string', 'min:8', 'max:255'];
|
||||
$rules['email'] = ['required', 'string', 'email', 'max:255', 'unique:admin_users,email,'.$this->route('admin_user')];
|
||||
$rules['avatar'] = ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:5120'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El nombre es obligatorio.',
|
||||
'name.max' => 'El nombre no puede exceder 255 caracteres.',
|
||||
'email.required' => 'El email es obligatorio.',
|
||||
'email.email' => 'El email debe ser una dirección válida.',
|
||||
'email.max' => 'El email no puede exceder 255 caracteres.',
|
||||
'email.unique' => 'El email ya está en uso.',
|
||||
'password.required' => 'La contraseña es obligatoria.',
|
||||
'password.min' => 'La contraseña debe tener al menos 6 caracteres.',
|
||||
'password.max' => 'La contraseña no puede exceder 255 caracteres.',
|
||||
'rol.required' => 'El rol es obligatorio.',
|
||||
'rol.in' => 'El rol debe ser super_admin o admin.',
|
||||
'avatar.image' => 'El avatar debe ser una imagen.',
|
||||
'avatar.mimes' => 'El formato del avatar no es válido.',
|
||||
'avatar.max' => 'El avatar no puede exceder 5MB.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator instance.
|
||||
*/
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
// Validar que un admin no pueda crear/editar otro super_admin
|
||||
// Esta validación se maneja en el controlador
|
||||
});
|
||||
}
|
||||
}
|
||||
43
app/Http/Requests/GaleriaRequest.php
Executable file
43
app/Http/Requests/GaleriaRequest.php
Executable file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class GaleriaRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'titulo' => ['required', 'string', 'max:255'],
|
||||
'descripcion' => ['nullable', 'string', 'max:1000'],
|
||||
'tipo' => ['required', 'in:imagen,video'],
|
||||
'archivo' => ['required', 'file', 'max:102400'],
|
||||
'thumbnail' => ['nullable', 'image', 'max:10240'],
|
||||
'orden' => ['nullable', 'integer', 'min:0'],
|
||||
'activo' => ['nullable', 'boolean'],
|
||||
];
|
||||
|
||||
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
|
||||
$rules['archivo'] = ['nullable', 'file', 'max:102400'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'titulo.required' => 'El titulo es obligatorio.',
|
||||
'tipo.required' => 'Debes seleccionar el tipo (imagen o video).',
|
||||
'archivo.required' => 'Debes subir un archivo.',
|
||||
'archivo.mimes' => 'El formato del archivo no es valido.',
|
||||
'archivo.max' => 'El archivo no puede exceder 100MB.',
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/LoginRequest.php
Executable file
31
app/Http/Requests/LoginRequest.php
Executable file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email', 'max:255'],
|
||||
'password' => ['required', 'string', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => 'El email es obligatorio.',
|
||||
'email.email' => 'El email debe ser una dirección válida.',
|
||||
'email.max' => 'El email no puede exceder 255 caracteres.',
|
||||
'password.required' => 'La contraseña es obligatoria.',
|
||||
];
|
||||
}
|
||||
}
|
||||
42
app/Http/Requests/MensajeRequest.php
Executable file
42
app/Http/Requests/MensajeRequest.php
Executable file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class MensajeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255'],
|
||||
'telefono' => ['nullable', 'string', 'max:50', 'regex:/^[+]?[\d\s\-()]+$/'],
|
||||
'mensaje' => ['required', 'string', 'min:1', 'max:10000'],
|
||||
'leido' => ['nullable', 'boolean'],
|
||||
];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'nombre.required' => 'El nombre es obligatorio.',
|
||||
'nombre.max' => 'El nombre no puede exceder 255 caracteres.',
|
||||
'email.required' => 'El email es obligatorio.',
|
||||
'email.email' => 'El email debe ser una dirección válida.',
|
||||
'email.max' => 'El email no puede exceder 255 caracteres.',
|
||||
'telefono.max' => 'El teléfono no puede exceder 50 caracteres.',
|
||||
'telefono.regex' => 'El formato del teléfono no es válido.',
|
||||
'mensaje.required' => 'El mensaje es obligatorio.',
|
||||
'mensaje.min' => 'El mensaje no puede estar vacío.',
|
||||
'mensaje.max' => 'El mensaje no puede exceder 10000 caracteres.',
|
||||
];
|
||||
}
|
||||
}
|
||||
55
app/Http/Requests/ProductoRequest.php
Executable file
55
app/Http/Requests/ProductoRequest.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ProductoRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
'descripcion' => ['required', 'string', 'max:5000'],
|
||||
'precio' => ['required', 'numeric', 'min:0', 'regex:/^\d+(\.\d{1,2})?$/'],
|
||||
'imagen' => ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:10240'],
|
||||
'categoria' => ['required', 'string', 'max:100'],
|
||||
'destacado' => ['nullable', 'boolean'],
|
||||
'activo' => ['nullable', 'boolean'],
|
||||
'orden' => ['nullable', 'integer', 'min:0'],
|
||||
];
|
||||
|
||||
// En actualización, imagen no es obligatoria
|
||||
if ($this->isMethod('PATCH') || $this->isMethod('PUT')) {
|
||||
$rules['imagen'] = ['nullable', 'image', 'mimes:jpeg,png,gif,webp', 'max:10240'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'nombre.required' => 'El nombre es obligatorio.',
|
||||
'nombre.max' => 'El nombre no puede exceder 255 caracteres.',
|
||||
'descripcion.required' => 'La descripción es obligatoria.',
|
||||
'descripcion.max' => 'La descripción no puede exceder 5000 caracteres.',
|
||||
'precio.required' => 'El precio es obligatorio.',
|
||||
'precio.numeric' => 'El precio debe ser un número válido.',
|
||||
'precio.min' => 'El precio no puede ser negativo.',
|
||||
'precio.regex' => 'El precio debe tener como máximo 2 decimales.',
|
||||
'imagen.image' => 'La imagen debe ser un archivo de imagen.',
|
||||
'imagen.mimes' => 'El formato de imagen no es válido.',
|
||||
'imagen.max' => 'La imagen no puede exceder 10MB.',
|
||||
'categoria.required' => 'La categoría es obligatoria.',
|
||||
'categoria.max' => 'La categoría no puede exceder 100 caracteres.',
|
||||
'orden.integer' => 'El orden debe ser un número entero.',
|
||||
'orden.min' => 'El orden no puede ser negativo.',
|
||||
];
|
||||
}
|
||||
}
|
||||
81
app/Models/AdminUser.php
Executable file
81
app/Models/AdminUser.php
Executable file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdminUser extends Authenticatable
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'admin_users';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'rol',
|
||||
'avatar',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Validar password
|
||||
*/
|
||||
public function validatePassword(string $password): bool
|
||||
{
|
||||
return Hash::check($password, $this->password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password attribute - hash automatically
|
||||
*/
|
||||
public function setPasswordAttribute(string $value): void
|
||||
{
|
||||
$this->attributes['password'] = Hash::make($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar super admins
|
||||
*/
|
||||
public function scopeSuperAdmin(Builder $query): Builder
|
||||
{
|
||||
return $query->where('rol', 'super_admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar admins
|
||||
*/
|
||||
public function scopeAdmin(Builder $query): Builder
|
||||
{
|
||||
return $query->where('rol', 'admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si es super admin
|
||||
*/
|
||||
public function isSuperAdmin(): bool
|
||||
{
|
||||
return $this->rol === 'super_admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si tiene permiso para gestionar otros admins
|
||||
*/
|
||||
public function canManageAdmins(): bool
|
||||
{
|
||||
return $this->rol === 'super_admin';
|
||||
}
|
||||
}
|
||||
75
app/Models/Configuracion.php
Executable file
75
app/Models/Configuracion.php
Executable file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Configuracion extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'configuraciones';
|
||||
|
||||
protected $fillable = [
|
||||
'clave',
|
||||
'valor',
|
||||
'descripcion',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Obtener configuración por clave
|
||||
*
|
||||
* @param string $clave La clave de configuración
|
||||
* @param mixed $default Valor por defecto si no existe
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get(string $clave, $default = null)
|
||||
{
|
||||
$config = self::where('clave', $clave)->first();
|
||||
|
||||
if (! $config) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $config->valor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener todas las configuraciones como array asociativo
|
||||
*/
|
||||
public static function allAsArray(): array
|
||||
{
|
||||
return self::pluck('valor', 'clave')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Establecer una configuración
|
||||
*
|
||||
* @param string $clave La clave de configuración
|
||||
* @param mixed $valor El valor a guardar
|
||||
*/
|
||||
public static function set(string $clave, $valor): self
|
||||
{
|
||||
$config = self::where('clave', $clave)->firstOrNew(['clave' => $clave]);
|
||||
$config->valor = $valor;
|
||||
$config->save();
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar una configuración
|
||||
*
|
||||
* @param string $clave La clave de configuración
|
||||
*/
|
||||
public static function remove(string $clave): bool
|
||||
{
|
||||
return self::where('clave', $clave)->delete() > 0;
|
||||
}
|
||||
}
|
||||
71
app/Models/Galeria.php
Executable file
71
app/Models/Galeria.php
Executable file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Galeria extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'galerias';
|
||||
|
||||
protected $fillable = [
|
||||
'titulo',
|
||||
'descripcion',
|
||||
'tipo',
|
||||
'archivo',
|
||||
'thumbnail',
|
||||
'orden',
|
||||
'activo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'activo' => 'boolean',
|
||||
'orden' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope para filtrar registros activos
|
||||
*/
|
||||
public function scopeActivo(Builder $query): Builder
|
||||
{
|
||||
return $query->where('activo', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para ordenar por campo orden
|
||||
*/
|
||||
public function scopeOrdenado(Builder $query): Builder
|
||||
{
|
||||
return $query->orderBy('orden', 'asc')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener el tipo de archivo como clase CSS
|
||||
*/
|
||||
public function getTipoClaseAttribute(): string
|
||||
{
|
||||
return $this->tipo === 'video' ? 'video' : 'imagen';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si es video
|
||||
*/
|
||||
public function esVideo(): bool
|
||||
{
|
||||
return $this->tipo === 'video';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si es imagen
|
||||
*/
|
||||
public function esImagen(): bool
|
||||
{
|
||||
return $this->tipo === 'imagen';
|
||||
}
|
||||
}
|
||||
68
app/Models/Mensaje.php
Executable file
68
app/Models/Mensaje.php
Executable file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Mensaje extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'mensajes';
|
||||
|
||||
protected $fillable = [
|
||||
'nombre',
|
||||
'email',
|
||||
'telefono',
|
||||
'mensaje',
|
||||
'leido',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'leido' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope para filtrar mensajes no leídos
|
||||
*/
|
||||
public function scopeNoLeidos(Builder $query): Builder
|
||||
{
|
||||
return $query->where('leido', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar mensaje como leído
|
||||
*/
|
||||
public function marcarLeido(): bool
|
||||
{
|
||||
return $this->update(['leido' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar mensaje como no leído
|
||||
*/
|
||||
public function marcarNoLeido(): bool
|
||||
{
|
||||
return $this->update(['leido' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener iniciales del nombre
|
||||
*/
|
||||
public function getInicialesAttribute(): string
|
||||
{
|
||||
$nombres = explode(' ', $this->nombre);
|
||||
$iniciales = '';
|
||||
foreach ($nombres as $nombre) {
|
||||
if (! empty($nombre)) {
|
||||
$iniciales .= strtoupper($nombre[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return $iniciales;
|
||||
}
|
||||
}
|
||||
76
app/Models/Producto.php
Executable file
76
app/Models/Producto.php
Executable file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Producto extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'productos';
|
||||
|
||||
protected $fillable = [
|
||||
'nombre',
|
||||
'descripcion',
|
||||
'precio',
|
||||
'imagen',
|
||||
'categoria',
|
||||
'destacado',
|
||||
'activo',
|
||||
'orden',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'precio' => 'decimal:2',
|
||||
'destacado' => 'boolean',
|
||||
'activo' => 'boolean',
|
||||
'orden' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope para filtrar productos activos
|
||||
*/
|
||||
public function scopeActivo(Builder $query): Builder
|
||||
{
|
||||
return $query->where('activo', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar productos destacados
|
||||
*/
|
||||
public function scopeDestacado(Builder $query): Builder
|
||||
{
|
||||
return $query->where('destacado', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar por categoría
|
||||
*/
|
||||
public function scopePorCategoria(Builder $query, string $categoria): Builder
|
||||
{
|
||||
return $query->where('categoria', $categoria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener precio formateado
|
||||
*/
|
||||
public function getPrecioFormateadoAttribute(): string
|
||||
{
|
||||
$currency = config('currency');
|
||||
|
||||
return $currency['symbol'].number_format($this->precio, $currency['decimal_places'], $currency['decimal_separator'], $currency['thousands_separator']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si tiene imagen
|
||||
*/
|
||||
public function tieneImagen(): bool
|
||||
{
|
||||
return ! empty($this->imagen);
|
||||
}
|
||||
}
|
||||
32
app/Models/User.php
Executable file
32
app/Models/User.php
Executable file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Executable file
24
app/Providers/AppServiceProvider.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
18
artisan
Executable file
18
artisan
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
26
bootstrap/app.php
Executable file
26
bootstrap/app.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\AdminAuth;
|
||||
use App\Http\Middleware\SecurityHeaders;
|
||||
use App\Http\Middleware\SuperAdminOnly;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->alias([
|
||||
'admin.auth' => AdminAuth::class,
|
||||
'super_admin' => SuperAdminOnly::class,
|
||||
'security.headers' => SecurityHeaders::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Executable file
2
bootstrap/cache/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
7
bootstrap/providers.php
Executable file
7
bootstrap/providers.php
Executable file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
88
composer.json
Executable file
88
composer.json
Executable file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/tinker": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pint": "^1.27",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^12.5.12"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
},
|
||||
"files": [
|
||||
"app/Helpers/helpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install --ignore-scripts",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
8153
composer.lock
generated
Executable file
8153
composer.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
126
config/app.php
Executable file
126
config/app.php
Executable file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => env('APP_TIMEZONE', 'America/Mexico_City'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'es'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'es'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'es_MX'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
135
config/auth.php
Executable file
135
config/auth.php
Executable file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'admin' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'admin_users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
'admin_users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', AdminUser::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
|
||||
'admin_users' => [
|
||||
'provider' => 'admin_users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
130
config/cache.php
Executable file
130
config/cache.php
Executable file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
|
||||
];
|
||||
10
config/currency.php
Normal file
10
config/currency.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'code' => env('CURRENCY_CODE', 'MXN'),
|
||||
'symbol' => env('CURRENCY_SYMBOL', '$'),
|
||||
'name' => env('CURRENCY_NAME', 'Peso Mexicano'),
|
||||
'decimal_separator' => '.',
|
||||
'thousands_separator' => ',',
|
||||
'decimal_places' => 2,
|
||||
];
|
||||
184
config/database.php
Executable file
184
config/database.php
Executable file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
80
config/filesystems.php
Executable file
80
config/filesystems.php
Executable file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
132
config/logging.php
Executable file
132
config/logging.php
Executable file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'daily'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => base_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => base_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => base_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
118
config/mail.php
Executable file
118
config/mail.php
Executable file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||
],
|
||||
|
||||
];
|
||||
129
config/queue.php
Executable file
129
config/queue.php
Executable file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
38
config/services.php
Executable file
38
config/services.php
Executable file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
233
config/session.php
Executable file
233
config/session.php
Executable file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Serialization
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the serialization strategy for session data, which
|
||||
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||
| objects in the session but can make an application vulnerable to
|
||||
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||
|
|
||||
| Supported: "json", "php"
|
||||
|
|
||||
*/
|
||||
|
||||
'serialization' => 'json',
|
||||
|
||||
];
|
||||
1
database/.gitignore
vendored
Executable file
1
database/.gitignore
vendored
Executable file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
45
database/factories/UserFactory.php
Executable file
45
database/factories/UserFactory.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Executable file
49
database/migrations/0001_01_01_000000_create_users_table.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Executable file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Executable file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Executable file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
26
database/migrations/2024_01_01_000003_create_admin_users_table.php
Executable file
26
database/migrations/2024_01_01_000003_create_admin_users_table.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admin_users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->string('password');
|
||||
$table->enum('rol', ['super_admin', 'admin'])->default('admin');
|
||||
$table->string('avatar')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('admin_users');
|
||||
}
|
||||
};
|
||||
28
database/migrations/2024_01_01_000004_create_galerias_table.php
Executable file
28
database/migrations/2024_01_01_000004_create_galerias_table.php
Executable file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('galerias', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('titulo');
|
||||
$table->text('descripcion')->nullable();
|
||||
$table->enum('tipo', ['imagen', 'video'])->default('imagen');
|
||||
$table->string('archivo');
|
||||
$table->string('thumbnail')->nullable();
|
||||
$table->integer('orden')->default(0);
|
||||
$table->boolean('activo')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('galerias');
|
||||
}
|
||||
};
|
||||
29
database/migrations/2024_01_01_000005_create_productos_table.php
Executable file
29
database/migrations/2024_01_01_000005_create_productos_table.php
Executable file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('productos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nombre');
|
||||
$table->text('descripcion');
|
||||
$table->decimal('precio', 10, 2);
|
||||
$table->string('imagen')->nullable();
|
||||
$table->string('categoria', 100);
|
||||
$table->boolean('destacado')->default(false);
|
||||
$table->boolean('activo')->default(true);
|
||||
$table->integer('orden')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('productos');
|
||||
}
|
||||
};
|
||||
26
database/migrations/2024_01_01_000006_create_mensajes_table.php
Executable file
26
database/migrations/2024_01_01_000006_create_mensajes_table.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('mensajes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nombre');
|
||||
$table->string('email');
|
||||
$table->string('telefono', 50)->nullable();
|
||||
$table->text('mensaje');
|
||||
$table->boolean('leido')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mensajes');
|
||||
}
|
||||
};
|
||||
24
database/migrations/2024_01_01_000007_create_configuraciones_table.php
Executable file
24
database/migrations/2024_01_01_000007_create_configuraciones_table.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('configuraciones', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('clave', 100)->unique();
|
||||
$table->text('valor');
|
||||
$table->string('descripcion', 255)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('configuraciones');
|
||||
}
|
||||
};
|
||||
25
database/seeders/DatabaseSeeder.php
Executable file
25
database/seeders/DatabaseSeeder.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
package.json
Executable file
17
package.json
Executable file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": ">=1.11.0 <=1.14.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^3.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
36
phpunit.xml
Executable file
36
phpunit.xml
Executable file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
25
public/.htaccess
Executable file
25
public/.htaccess
Executable file
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
729
public/css/admin.css
Executable file
729
public/css/admin.css
Executable file
@@ -0,0 +1,729 @@
|
||||
/* ============================================
|
||||
Lash Vanshy - Estilos del Panel Admin
|
||||
Diseño: Rosa Pastel (#F8B4C4) + Blanco
|
||||
============================================ */
|
||||
|
||||
/* Variables CSS */
|
||||
:root {
|
||||
--primary: #F8B4C4;
|
||||
--primary-dark: #E89AAD;
|
||||
--secondary: #FFE4EC;
|
||||
--accent: #E89AAD;
|
||||
--background: #F8F9FA;
|
||||
--surface: #FFFFFF;
|
||||
--text: #4A4A4A;
|
||||
--text-light: #6c757d;
|
||||
--text-muted: #adb5bd;
|
||||
--border: #E9ECEF;
|
||||
--success: #28a745;
|
||||
--danger: #dc3545;
|
||||
--warning: #ffc107;
|
||||
--info: #17a2b8;
|
||||
--sidebar-width: 260px;
|
||||
--header-height: 70px;
|
||||
--shadow: rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 30px rgba(248, 180, 196, 0.3);
|
||||
--font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
color: var(--text);
|
||||
background-color: var(--background);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LAYOUT
|
||||
============================================ */
|
||||
.admin-wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SIDEBAR
|
||||
============================================ */
|
||||
.admin-sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: linear-gradient(180deg, #fff 0%, var(--secondary) 100%);
|
||||
box-shadow: 2px 0 20px var(--shadow);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar-header .brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-header .brand-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.sidebar-header .brand-text {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.sidebar-header .badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link:hover {
|
||||
background: rgba(248, 180, 196, 0.2);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link.active {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link.active i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-link .badge {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: transparent;
|
||||
border: 2px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: var(--danger);
|
||||
border-color: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MAIN CONTENT
|
||||
============================================ */
|
||||
.admin-main {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
height: var(--header-height);
|
||||
background: white;
|
||||
box-shadow: 0 2px 10px var(--shadow);
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-toggle-sidebar {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.current-date {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CARDS
|
||||
============================================ */
|
||||
.card-admin {
|
||||
background: white;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 5px 20px var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-admin .card-header {
|
||||
background: linear-gradient(135deg, var(--secondary), white);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1.25rem 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.card-admin .card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Stats Cards */
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 5px 20px var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon.primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon.success {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon.warning {
|
||||
background: linear-gradient(135deg, var(--warning), #fd7e14);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon.info {
|
||||
background: linear-gradient(135deg, var(--info), var(--primary-dark));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-card .stat-info h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-card .stat-info p {
|
||||
color: var(--text-light);
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TABLES
|
||||
============================================ */
|
||||
.table-admin {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-admin thead th {
|
||||
background: var(--secondary);
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
border-bottom: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.table-admin tbody td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-admin tbody tr:hover {
|
||||
background: rgba(248, 180, 196, 0.1);
|
||||
}
|
||||
|
||||
.table-admin .actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.table-admin .actions .btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FORMS
|
||||
============================================ */
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-family: var(--font-family);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(248, 180, 196, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-control.is-invalid {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: var(--danger);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Form switch */
|
||||
.form-switch {
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
width: 2rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input:checked {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BUTTONS
|
||||
============================================ */
|
||||
.btn-primary-admin {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary-admin:hover {
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-secondary-admin {
|
||||
background: white;
|
||||
border: 2px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary-admin:hover {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-danger-admin {
|
||||
background: var(--danger);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-danger-admin:hover {
|
||||
background: #c82333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ALERTS
|
||||
============================================ */
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, #d4edda, #c3e6cb);
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: linear-gradient(135deg, #fff3cd, #ffeeba);
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: linear-gradient(135deg, #d1ecf1, #bee5eb);
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PAGINATION
|
||||
============================================ */
|
||||
.pagination {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--text);
|
||||
border: 2px solid var(--border);
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITIES
|
||||
============================================ */
|
||||
.text-primary {
|
||||
color: var(--primary-dark) !important;
|
||||
}
|
||||
|
||||
.bg-primary-custom {
|
||||
background-color: var(--secondary) !important;
|
||||
}
|
||||
|
||||
.badge-admin {
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-admin.bg-success {
|
||||
background: linear-gradient(135deg, #28a745, #20c997) !important;
|
||||
}
|
||||
|
||||
.badge-admin.bg-warning {
|
||||
background: linear-gradient(135deg, var(--warning), #fd7e14) !important;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-admin.bg-danger {
|
||||
background: linear-gradient(135deg, var(--danger), #bd2130) !important;
|
||||
}
|
||||
|
||||
.badge-admin.bg-info {
|
||||
background: linear-gradient(135deg, var(--info), var(--primary-dark)) !important;
|
||||
}
|
||||
|
||||
/* Image preview */
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
border: 2px dashed var(--border);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
color: var(--border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SEARCH & FILTERS
|
||||
============================================ */
|
||||
.search-box {
|
||||
position: relative;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding-left: 2.5rem;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.filters-bar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
@media (max-width: 992px) {
|
||||
.admin-sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.admin-sidebar.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filters-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.table-admin {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table-admin thead th,
|
||||
.table-admin tbody td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MODAL
|
||||
============================================ */
|
||||
.modal-admin .modal-content {
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 60px var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-admin .modal-header {
|
||||
background: linear-gradient(135deg, var(--secondary), white);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-radius: 15px 15px 0 0;
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.modal-admin .modal-title {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-admin .modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-admin .modal-footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LOADING
|
||||
============================================ */
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
908
public/css/frontend.css
Executable file
908
public/css/frontend.css
Executable file
@@ -0,0 +1,908 @@
|
||||
/* ============================================
|
||||
Lash Vanshy - Estilos Frontend
|
||||
Diseño: Rosa Pastel (#F8B4C4) + Blanco
|
||||
============================================ */
|
||||
|
||||
/* Variables CSS */
|
||||
:root {
|
||||
--primary: #F8B4C4;
|
||||
--primary-dark: #E89AAD;
|
||||
--secondary: #FFE4EC;
|
||||
--accent: #E89AAD;
|
||||
--background: #FFFFFF;
|
||||
--text: #4A4A4A;
|
||||
--text-light: #7a7a7a;
|
||||
--border: #F0E6EA;
|
||||
--success: #4CAF50;
|
||||
--danger: #e74c3c;
|
||||
--shadow: rgba(248, 180, 196, 0.3);
|
||||
--font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
/* Reset y base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
color: var(--text);
|
||||
background-color: var(--background);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HEADER
|
||||
============================================ */
|
||||
.header-main {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, #fff 100%);
|
||||
box-shadow: 0 2px 15px var(--shadow);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.header-main .navbar {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.header-main .navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.header-main .nav-link {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem !important;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-main .nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.header-main .nav-link:hover,
|
||||
.header-main .nav-link.active {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.header-main .nav-link:hover::after,
|
||||
.header-main .nav-link.active::after {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.header-main .navbar-toggler {
|
||||
border: 2px solid var(--primary);
|
||||
background: transparent;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.header-main .navbar-toggler-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(248, 180, 196, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HERO SECTION
|
||||
============================================ */
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, var(--background) 50%, var(--secondary) 100%);
|
||||
padding: 80px 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23F8B4C4' fill-opacity='0.1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 1rem;
|
||||
animation: fadeInUp 1s ease;
|
||||
}
|
||||
|
||||
.hero-title span {
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 2rem;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
animation: fadeInUp 1s ease 0.2s both;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px var(--shadow);
|
||||
animation: fadeInUp 1s ease 0.4s both;
|
||||
}
|
||||
|
||||
.hero-btn {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||
color: white;
|
||||
padding: 1rem 2.5rem;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 10px 30px var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeInUp 1s ease 0.6s both;
|
||||
}
|
||||
|
||||
.hero-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 40px var(--shadow);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SECTIONS
|
||||
============================================ */
|
||||
.section-padding {
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-title h2 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-title h2 span {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.section-title p {
|
||||
color: var(--text-light);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Backgrounds */
|
||||
.bg-secondary {
|
||||
background-color: var(--secondary) !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CARDS
|
||||
============================================ */
|
||||
.card-custom {
|
||||
background: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-custom:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 20px 60px rgba(248, 180, 196, 0.4);
|
||||
}
|
||||
|
||||
.card-custom .card-img-top {
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
.card-custom .card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-custom .card-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-custom .card-text {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-custom .price {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.badge-destacado {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
GALLERY / GALERÍA
|
||||
============================================ */
|
||||
.gallery-section {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.gallery-filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
background: white;
|
||||
border: 2px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 30px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-btn:hover,
|
||||
.filter-btn.active {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
aspect-ratio: 4/3;
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.gallery-item:hover img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.gallery-item .overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, transparent 100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.gallery-item:hover .overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gallery-item .overlay-content {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gallery-item .overlay-content h4 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.gallery-item .overlay-content p {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.gallery-item .play-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: var(--primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.gallery-item:hover .play-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PRODUCTS / SERVICIOS
|
||||
============================================ */
|
||||
.products-section {
|
||||
background: linear-gradient(180deg, var(--secondary) 0%, var(--background) 100%);
|
||||
}
|
||||
|
||||
.category-filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px var(--shadow);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 20px 60px rgba(248, 180, 196, 0.4);
|
||||
}
|
||||
|
||||
.product-card img {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-card .product-info {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.product-card .product-category {
|
||||
font-size: 0.8rem;
|
||||
color: var(--primary-dark);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.product-card .product-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.product-card .product-description {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.product-card .product-price {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CONTACT FORM
|
||||
============================================ */
|
||||
.contact-section {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, var(--primary) 100%);
|
||||
padding: 3rem;
|
||||
color: var(--text);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.contact-info h3 {
|
||||
color: var(--text);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-info-item i {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary-dark);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-info-item div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contact-info-item strong {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-family: var(--font-family);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(248, 180, 196, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.btn-primary-custom {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary-custom:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px var(--shadow);
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #155724;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #721c24;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TESTIMONIALS / TESTIMONIOS
|
||||
============================================ */
|
||||
.testimonials-section {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.testimonial-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 40px var(--shadow);
|
||||
text-align: center;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.testimonial-card .quote-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--primary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.testimonial-card p {
|
||||
font-style: italic;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.testimonial-card .author {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FOOTER
|
||||
============================================ */
|
||||
.footer-main {
|
||||
background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%);
|
||||
color: white;
|
||||
padding: 60px 0 20px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footer-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-description {
|
||||
color: #aaa;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
background: var(--primary);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.footer-contact {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer-contact li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.footer-contact li i {
|
||||
color: var(--primary);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.footer-contact a {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.footer-contact a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.footer-schedule {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer-schedule li {
|
||||
color: #aaa;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
color: #777;
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
@media (max-width: 992px) {
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-title h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.contact-info-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.gallery-filters,
|
||||
.category-filters {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITIES
|
||||
============================================ */
|
||||
.text-primary {
|
||||
color: var(--primary-dark) !important;
|
||||
}
|
||||
|
||||
.bg-primary-custom {
|
||||
background-color: var(--secondary) !important;
|
||||
}
|
||||
|
||||
.placeholder-img {
|
||||
background: linear-gradient(135deg, var(--secondary), var(--primary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--text);
|
||||
border: 2px solid var(--border);
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
color: var(--border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h4 {
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
0
public/favicon.ico
Executable file
0
public/favicon.ico
Executable file
20
public/index.php
Executable file
20
public/index.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
2
public/robots.txt
Executable file
2
public/robots.txt
Executable file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
11
resources/css/app.css
Executable file
11
resources/css/app.css
Executable file
@@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
1
resources/js/app.js
Executable file
1
resources/js/app.js
Executable file
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
4
resources/js/bootstrap.js
vendored
Executable file
4
resources/js/bootstrap.js
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
263
resources/views/admin/auth/login.blade.php
Executable file
263
resources/views/admin/auth/login.blade.php
Executable file
@@ -0,0 +1,263 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Lash Vanshy Admin</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary: #F8B4C4;
|
||||
--primary-dark: #E89AAD;
|
||||
--secondary: #FFE4EC;
|
||||
--background: #F8F9FA;
|
||||
--text: #4A4A4A;
|
||||
--text-light: #6c757d;
|
||||
--border: #E9ECEF;
|
||||
--danger: #dc3545;
|
||||
--font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, var(--background) 50%, var(--secondary) 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(248, 180, 196, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
padding: 2.5rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-header .brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-header .brand-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.login-header .brand-text {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.login-body {
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 0.875rem 1rem;
|
||||
font-family: var(--font-family);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(248, 180, 196, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-control.is-invalid {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(248, 180, 196, 0.4);
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: transparent;
|
||||
border: 2px solid var(--border);
|
||||
border-left: none;
|
||||
border-radius: 0 12px 12px 0;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.input-group .form-control {
|
||||
border-right: none;
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
.input-group .form-control:focus + .input-group-text {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: var(--danger);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: #721c24;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.back-home {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.back-home a {
|
||||
color: var(--text-light);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.back-home a:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrapper">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<a href="{{ route('home') }}" class="brand">
|
||||
<span class="brand-icon">✨</span>
|
||||
<span class="brand-text">Lash Vanshy</span>
|
||||
</a>
|
||||
<p>Panel de Administración</p>
|
||||
</div>
|
||||
|
||||
<div class="login-body">
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
@if($errors->has('email'))
|
||||
{{ $errors->first('email') }}
|
||||
@else
|
||||
Los datos proporcionados no son correctos. Por favor, inténtalo de nuevo.
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Correo electrónico</label>
|
||||
<div class="input-group">
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="tu@email.com"
|
||||
required
|
||||
autofocus>
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Contraseña</label>
|
||||
<div class="input-group">
|
||||
<input type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
required>
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
</div>
|
||||
@error('password')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login">
|
||||
<i class="fas fa-sign-in-alt me-2"></i>Iniciar Sesión
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="back-home">
|
||||
<a href="{{ route('home') }}">
|
||||
<i class="fas fa-arrow-left me-1"></i>Volver al sitio
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
195
resources/views/admin/configuracion/index.blade.php
Executable file
195
resources/views/admin/configuracion/index.blade.php
Executable file
@@ -0,0 +1,195 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Configuración - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Configuración del Sitio')
|
||||
|
||||
@section('content')
|
||||
<!-- Header -->
|
||||
<div class="mb-4">
|
||||
<h2 class="mb-0">Configuración del Sitio</h2>
|
||||
<p class="text-muted mb-0">Administra la información pública del negocio</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.configuracion.update') }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Información del Negocio -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-store me-2"></i>Información del Negocio
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="nombre_sitio" class="form-label">Nombre del Sitio</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="nombre_sitio"
|
||||
name="nombre_sitio"
|
||||
value="{{ old('nombre_sitio', $configuracion['nombre_sitio'] ?? 'Lash Vanshy') }}"
|
||||
placeholder="Lash Vanshy">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="telefono" class="form-label">Teléfono</label>
|
||||
<input type="tel"
|
||||
class="form-control"
|
||||
id="telefono"
|
||||
name="telefono"
|
||||
value="{{ old('telefono', $configuracion['telefono'] ?? '') }}"
|
||||
placeholder="+34 000 000 000">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email', $configuracion['email'] ?? '') }}"
|
||||
placeholder="contacto@lashvanshy.com">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="direccion" class="form-label">Dirección</label>
|
||||
<textarea class="form-control"
|
||||
id="direccion"
|
||||
name="direccion"
|
||||
rows="2"
|
||||
placeholder="Tu dirección">{{ old('direccion', $configuracion['direccion'] ?? '') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="horario" class="form-label">Horario</label>
|
||||
<textarea class="form-control"
|
||||
id="horario"
|
||||
name="horario"
|
||||
rows="3"
|
||||
placeholder="Lunes - Viernes: 10:00 - 20:00 Sábado: 10:00 - 18:00">{{ old('horario', $configuracion['horario'] ?? '') }}</textarea>
|
||||
<small class="text-muted">Usa saltos de línea para cada día</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redes Sociales -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-share-alt me-2"></i>Redes Sociales
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="facebook" class="form-label">Facebook</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fab fa-facebook"></i></span>
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
id="facebook"
|
||||
name="facebook"
|
||||
value="{{ old('facebook', $configuracion['facebook'] ?? '') }}"
|
||||
placeholder="https://facebook.com/tupagina">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="instagram" class="form-label">Instagram</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fab fa-instagram"></i></span>
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
id="instagram"
|
||||
name="instagram"
|
||||
value="{{ old('instagram', $configuracion['instagram'] ?? '') }}"
|
||||
placeholder="https://instagram.com/tucuenta">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="whatsapp" class="form-label">WhatsApp</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fab fa-whatsapp"></i></span>
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
id="whatsapp"
|
||||
name="whatsapp"
|
||||
value="{{ old('whatsapp', $configuracion['whatsapp'] ?? '') }}"
|
||||
placeholder="https://wa.me/34600000000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tiktok" class="form-label">TikTok</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fab fa-tiktok"></i></span>
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
id="tiktok"
|
||||
name="tiktok"
|
||||
value="{{ old('tiktok', $configuracion['tiktok'] ?? '') }}"
|
||||
placeholder="https://tiktok.com/@tuusuario">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="youtube" class="form-label">YouTube</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fab fa-youtube"></i></span>
|
||||
<input type="url"
|
||||
class="form-control"
|
||||
id="youtube"
|
||||
name="youtube"
|
||||
value="{{ old('youtube', $configuracion['youtube'] ?? '') }}"
|
||||
placeholder="https://youtube.com/@tucanal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO -->
|
||||
<div class="col-12">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-search me-2"></i>Configuración SEO
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="seo_titulo" class="form-label">Título SEO</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="seo_titulo"
|
||||
name="seo_titulo"
|
||||
value="{{ old('seo_titulo', $configuracion['seo_titulo'] ?? 'Lash Vanshy - Extensiones de Pestañas Profesionales') }}"
|
||||
placeholder="Título para motores de búsqueda">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="seo_descripcion" class="form-label">Descripción SEO</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="seo_descripcion"
|
||||
name="seo_descripcion"
|
||||
value="{{ old('seo_descripcion', $configuracion['seo_descripcion'] ?? '') }}"
|
||||
placeholder="Descripción para motores de búsqueda">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Guardar Configuración
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
168
resources/views/admin/dashboard/index.blade.php
Executable file
168
resources/views/admin/dashboard/index.blade.php
Executable file
@@ -0,0 +1,168 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Dashboard - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Dashboard')
|
||||
|
||||
@section('content')
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['mensajes_no_leidos'] }}</h3>
|
||||
<p>Mensajes nuevos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-images"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['total_modelos'] }}</h3>
|
||||
<p>Total Modelos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-spa"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['total_productos'] }}</h3>
|
||||
<p>Total Servicios</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon info">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['productos_destacados'] }}</h3>
|
||||
<p>Servicios Destacados</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Recent Messages -->
|
||||
<div class="row g-4">
|
||||
<!-- Quick Actions -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-bolt me-2"></i>Accesos Rápidos
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<a href="{{ route('admin.galeria.create') }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Nuevo Modelo
|
||||
</a>
|
||||
<a href="{{ route('admin.productos.create') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Nuevo Servicio
|
||||
</a>
|
||||
<a href="{{ route('admin.mensajes.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-envelope me-2"></i>Ver Mensajes
|
||||
@if($stats['mensajes_no_leidos'] > 0)
|
||||
<span class="badge bg-danger ms-2">{{ $stats['mensajes_no_leidos'] }}</span>
|
||||
@endif
|
||||
</a>
|
||||
<a href="{{ route('home') }}" target="_blank" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-external-link-alt me-2"></i>Ver Sitio Web
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Messages -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="fas fa-envelope-open-text me-2"></i>Mensajes Recientes
|
||||
</span>
|
||||
<a href="{{ route('admin.mensajes.index') }}" class="btn btn-sm btn-primary-admin">
|
||||
Ver Todos
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($mensajes_recientes->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-admin mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Asunto</th>
|
||||
<th>Fecha</th>
|
||||
<th>Estado</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($mensajes_recientes as $mensaje)
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ $mensaje->nombre }}</strong>
|
||||
<br>
|
||||
<small class="text-muted">{{ $mensaje->email }}</small>
|
||||
</td>
|
||||
<td>{{ Str::limit($mensaje->mensaje, 40) }}</td>
|
||||
<td>{{ $mensaje->created_at->format('d/m/Y') }}</td>
|
||||
<td>
|
||||
@if($mensaje->leido)
|
||||
<span class="badge-admin bg-success">Leído</span>
|
||||
@else
|
||||
<span class="badge-admin bg-warning">Nuevo</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.mensajes.show', $mensaje) }}" class="btn btn-sm btn-primary-admin">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<h4>No hay mensajes</h4>
|
||||
<p>Los mensajes de contacto aparecerán aquí</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Site Preview -->
|
||||
<div class="row g-4 mt-2">
|
||||
<div class="col-12">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-eye me-2"></i>Vista Previa del Sitio
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<p class="mb-3">Accede al sitio público y revisa los cambios realizados</p>
|
||||
<a href="{{ route('home') }}" target="_blank" class="btn btn-primary-admin">
|
||||
<i class="fas fa-external-link-alt me-2"></i>Ver Sitio Web
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
223
resources/views/admin/galeria/create.blade.php
Executable file
223
resources/views/admin/galeria/create.blade.php
Executable file
@@ -0,0 +1,223 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Nuevo Modelo - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Nuevo Modelo')
|
||||
|
||||
@section('content')
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.galeria.index') }}">Galería</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Nuevo Modelo</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-plus-circle me-2"></i>Nuevo Modelo
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.galeria.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="titulo" class="form-label">Titulo *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('titulo') is-invalid @endif"
|
||||
id="titulo"
|
||||
name="titulo"
|
||||
value="{{ old('titulo') }}"
|
||||
placeholder="Ej: Extension de pestanas volumen russo"
|
||||
required>
|
||||
@error('titulo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="tipo" class="form-label">Tipo *</label>
|
||||
<select class="form-select @error('tipo') is-invalid @endif"
|
||||
id="tipo"
|
||||
name="tipo"
|
||||
required
|
||||
onchange="toggleFileInputs()">
|
||||
<option value="">Selecciona el tipo</option>
|
||||
<option value="imagen" {{ old('tipo') === 'imagen' ? 'selected' : '' }}>Imagen</option>
|
||||
<option value="video" {{ old('tipo') === 'video' ? 'selected' : '' }}>Video</option>
|
||||
</select>
|
||||
@error('tipo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="descripcion" class="form-label">Descripcion</label>
|
||||
<textarea class="form-control @error('descripcion') is-invalid @endif"
|
||||
id="descripcion"
|
||||
name="descripcion"
|
||||
rows="3"
|
||||
placeholder="Descripcion opcional del trabajo">{{ old('descripcion') }}</textarea>
|
||||
@error('descripcion')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="archivo" class="form-label">Archivo *</label>
|
||||
<input type="file"
|
||||
class="form-control @error('archivo') is-invalid @endif"
|
||||
id="archivo"
|
||||
name="archivo">
|
||||
<small class="text-muted" id="file-hint">Selecciona primero el tipo para ver los formatos validos</small>
|
||||
<div class="mt-2">
|
||||
<img id="file-preview" class="image-preview" style="max-width: 300px; display: none;">
|
||||
<video id="video-preview" class="video-preview" style="max-width: 300px; display: none;" controls></video>
|
||||
</div>
|
||||
@error('archivo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="thumbnail-container" style="display: none;">
|
||||
<label for="thumbnail" class="form-label">Miniatura (Thumbnail)</label>
|
||||
<input type="file"
|
||||
class="form-control @error('thumbnail') is-invalid @endif"
|
||||
id="thumbnail"
|
||||
name="thumbnail"
|
||||
accept="image/*">
|
||||
<small class="text-muted">Imagen de previsualizacion para el video</small>
|
||||
<div class="mt-2">
|
||||
<img id="thumbnail-preview" class="image-preview" style="max-width: 150px; display: none;">
|
||||
</div>
|
||||
@error('thumbnail')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="orden" class="form-label">Orden</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="orden"
|
||||
name="orden"
|
||||
value="{{ old('orden', 0) }}"
|
||||
min="0"
|
||||
placeholder="0">
|
||||
<small class="text-muted">Orden de visualizacion (menor = primero)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Estado</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="activo"
|
||||
name="activo"
|
||||
value="1"
|
||||
{{ old('activo', true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="activo">Activo (visible en web)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.galeria.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Guardar Modelo
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Informacion
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Los modelos son trabajos realizados que se mostraran en la galeria publica del sitio web.
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2">Puedes subir imagenes o videos de tus trabajos</li>
|
||||
<li class="mb-2">Los videos necesitan una miniatura para mostrarse en la galeria</li>
|
||||
<li>Usa el campo "orden" para controlar el orden de visualizacion</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function toggleFileInputs() {
|
||||
var tipo = document.getElementById('tipo').value;
|
||||
var archivo = document.getElementById('archivo');
|
||||
var fileHint = document.getElementById('file-hint');
|
||||
var thumbnailContainer = document.getElementById('thumbnail-container');
|
||||
|
||||
if (tipo === 'imagen') {
|
||||
archivo.accept = 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml';
|
||||
fileHint.textContent = 'Formatos: JPG, PNG, GIF, WebP, SVG. Maximo: 100MB';
|
||||
thumbnailContainer.style.display = 'none';
|
||||
} else if (tipo === 'video') {
|
||||
archivo.accept = 'video/mp4,video/webm,video/ogg';
|
||||
fileHint.textContent = 'Formatos: MP4, WebM, OGG. Maximo: 100MB';
|
||||
thumbnailContainer.style.display = 'block';
|
||||
} else {
|
||||
archivo.accept = '';
|
||||
fileHint.textContent = 'Selecciona primero el tipo para ver los formatos validos';
|
||||
thumbnailContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('archivo').addEventListener('change', function(e) {
|
||||
var file = e.target.files[0];
|
||||
var imagePreview = document.getElementById('file-preview');
|
||||
var videoPreview = document.getElementById('video-preview');
|
||||
|
||||
if (!file) {
|
||||
imagePreview.style.display = 'none';
|
||||
videoPreview.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.type.startsWith('image/')) {
|
||||
imagePreview.src = URL.createObjectURL(file);
|
||||
imagePreview.style.display = 'block';
|
||||
videoPreview.style.display = 'none';
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
videoPreview.src = URL.createObjectURL(file);
|
||||
videoPreview.style.display = 'block';
|
||||
imagePreview.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('thumbnail').addEventListener('change', function(e) {
|
||||
var file = e.target.files[0];
|
||||
var preview = document.getElementById('thumbnail-preview');
|
||||
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
preview.src = URL.createObjectURL(file);
|
||||
preview.style.display = 'block';
|
||||
} else {
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
toggleFileInputs();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
307
resources/views/admin/galeria/edit.blade.php
Executable file
307
resources/views/admin/galeria/edit.blade.php
Executable file
@@ -0,0 +1,307 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Editar Modelo - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Editar Modelo')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.galeria.index') }}">Galería</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Editar Modelo</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-edit me-2"></i>Editar Modelo
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.galeria.update', $galeria) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="titulo" class="form-label">Título *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('titulo') is-invalid @enderror"
|
||||
id="titulo"
|
||||
name="titulo"
|
||||
value="{{ old('titulo', $galeria->titulo) }}"
|
||||
placeholder="Ej: Extensión de pestañas volumen ruso"
|
||||
required>
|
||||
@error('titulo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="tipo" class="form-label">Tipo *</label>
|
||||
<select class="form-select @error('tipo') is-invalid @enderror"
|
||||
id="tipo"
|
||||
name="tipo"
|
||||
required
|
||||
onchange="toggleFileInputs()">
|
||||
<option value="">Selecciona el tipo</option>
|
||||
<option value="imagen" {{ $galeria->tipo === 'imagen' ? 'selected' : '' }}>Imagen</option>
|
||||
<option value="video" {{ $galeria->tipo === 'video' ? 'selected' : '' }}>Video</option>
|
||||
</select>
|
||||
@error('tipo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="descripcion" class="form-label">Descripción</label>
|
||||
<textarea class="form-control @error('descripcion') is-invalid @enderror"
|
||||
id="descripcion"
|
||||
name="descripcion"
|
||||
rows="3"
|
||||
placeholder="Descripción opcional del trabajo">{{ old('descripcion', $galeria->descripcion) }}</textarea>
|
||||
@error('descripcion')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Current File Info -->
|
||||
@if($galeria->tipo === 'imagen' && $galeria->archivo)
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Imagen Actual</label>
|
||||
<div class="mb-2">
|
||||
<img src="{{ asset('storage/' . $galeria->archivo) }}"
|
||||
alt="{{ $galeria->titulo }}"
|
||||
class="image-preview"
|
||||
style="max-height: 200px;">
|
||||
</div>
|
||||
<small class="text-muted">Deja este campo vacío para mantener la imagen actual</small>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($galeria->tipo === 'video')
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Video Actual</label>
|
||||
@if($galeria->archivo)
|
||||
<div class="mb-2">
|
||||
<video controls style="max-width: 100%; max-height: 200px; border-radius: 10px;">
|
||||
<source src="{{ asset('storage/' . $galeria->archivo) }}" type="video/mp4">
|
||||
Tu navegador no soporta videos.
|
||||
</video>
|
||||
</div>
|
||||
<small class="text-muted">Deja este campo vacío para mantener el video actual</small>
|
||||
@else
|
||||
<p class="text-muted">No hay video cargado</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Miniatura Actual</label>
|
||||
@if($galeria->thumbnail)
|
||||
<div class="mb-2">
|
||||
<img src="{{ asset('storage/' . $galeria->thumbnail) }}"
|
||||
alt="Thumbnail"
|
||||
class="image-preview"
|
||||
style="max-height: 150px;">
|
||||
</div>
|
||||
<small class="text-muted">Deja este campo vacío para mantener la miniatura actual</small>
|
||||
@else
|
||||
<p class="text-muted">No hay miniatura</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- New File Input -->
|
||||
@if($galeria->tipo === 'imagen')
|
||||
<div id="imagen-input" class="mb-3">
|
||||
<label for="archivo" class="form-label">Nueva Imagen</label>
|
||||
<input type="file"
|
||||
class="form-control @error('archivo') is-invalid @enderror"
|
||||
id="archivo"
|
||||
name="archivo"
|
||||
accept="image/*"
|
||||
onchange="previewImage(event, 'imagen-preview-edit')">
|
||||
<small class="text-muted">Formats: JPG, PNG, GIF. Max: 5MB</small>
|
||||
<div class="mt-2">
|
||||
<img id="imagen-preview-edit" class="image-preview" style="display: none;">
|
||||
</div>
|
||||
@error('archivo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@else
|
||||
<div id="imagen-input" class="mb-3" style="display: none;">
|
||||
<label for="archivo" class="form-label">Nueva Imagen</label>
|
||||
<input type="file"
|
||||
class="form-control @error('archivo') is-invalid @enderror"
|
||||
id="archivo"
|
||||
name="archivo"
|
||||
accept="image/*"
|
||||
onchange="previewImage(event, 'imagen-preview-edit')">
|
||||
<div class="mt-2">
|
||||
<img id="imagen-preview-edit" class="image-preview" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($galeria->tipo === 'video')
|
||||
<div id="video-inputs">
|
||||
<div class="mb-3">
|
||||
<label for="archivo_video" class="form-label">Nuevo Video</label>
|
||||
<input type="file"
|
||||
class="form-control @error('archivo') is-invalid @enderror"
|
||||
id="archivo_video"
|
||||
name="archivo"
|
||||
accept="video/*">
|
||||
<small class="text-muted">Formats: MP4, WebM. Max: 50MB</small>
|
||||
@error('archivo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="thumbnail_video" class="form-label">Nueva Miniatura</label>
|
||||
<input type="file"
|
||||
class="form-control @error('thumbnail') is-invalid @enderror"
|
||||
id="thumbnail_video"
|
||||
name="thumbnail"
|
||||
accept="image/*"
|
||||
onchange="previewImage(event, 'thumbnail-preview-edit')">
|
||||
<div class="mt-2">
|
||||
<img id="thumbnail-preview-edit" class="image-preview" style="display: none;">
|
||||
</div>
|
||||
@error('thumbnail')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div id="video-inputs" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="archivo_video" class="form-label">Nuevo Video</label>
|
||||
<input type="file"
|
||||
class="form-control @error('archivo') is-invalid @enderror"
|
||||
id="archivo_video"
|
||||
name="archivo"
|
||||
accept="video/*">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="thumbnail_video" class="form-label">Nueva Miniatura</label>
|
||||
<input type="file"
|
||||
class="form-control @error('thumbnail') is-invalid @enderror"
|
||||
id="thumbnail_video"
|
||||
name="thumbnail"
|
||||
accept="image/*"
|
||||
onchange="previewImage(event, 'thumbnail-preview-edit')">
|
||||
<div class="mt-2">
|
||||
<img id="thumbnail-preview-edit" class="image-preview" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="orden" class="form-label">Orden</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="orden"
|
||||
name="orden"
|
||||
value="{{ old('orden', $galeria->orden) }}"
|
||||
min="0"
|
||||
placeholder="0">
|
||||
<small class="text-muted">Orden de visualización (menor = primero)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Estado</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="activo"
|
||||
name="activo"
|
||||
value="1"
|
||||
{{ $galeria->activo ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="activo">Activo (visible en web)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.galeria.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Actualizar Modelo
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Los modelos son trabajos realizados que se mostrarán en la galería pública del sitio web.
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2">Puedes subir imágenes o videos de tus trabajos</li>
|
||||
<li class="mb-2">Los videos necesitan una miniatura para mostrarse en la galería</li>
|
||||
<li>Usa el campo "orden" para controlar el orden de visualización</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function toggleFileInputs() {
|
||||
const tipo = document.getElementById('tipo').value;
|
||||
const imagenInput = document.getElementById('imagen-input');
|
||||
const videoInputs = document.getElementById('video-inputs');
|
||||
|
||||
if (tipo === 'imagen') {
|
||||
imagenInput.style.display = 'block';
|
||||
videoInputs.style.display = 'none';
|
||||
} else if (tipo === 'video') {
|
||||
imagenInput.style.display = 'none';
|
||||
videoInputs.style.display = 'block';
|
||||
} else {
|
||||
imagenInput.style.display = 'none';
|
||||
videoInputs.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function previewImage(event, previewId) {
|
||||
const file = event.target.files[0];
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
preview.src = e.target.result;
|
||||
preview.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
toggleFileInputs();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
165
resources/views/admin/galeria/index.blade.php
Executable file
165
resources/views/admin/galeria/index.blade.php
Executable file
@@ -0,0 +1,165 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Galería - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Gestión de Galería')
|
||||
|
||||
@section('content')
|
||||
<!-- Header Actions -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-0">Modelos / Galería</h2>
|
||||
<p class="text-muted mb-0">Administra las imágenes y videos de tus trabajos</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.galeria.create') }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Nuevo Modelo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $galerias->total() }}</h3>
|
||||
<p>Total Modelos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $galerias->where('tipo', 'imagen')->count() }}</h3>
|
||||
<p>Imágenes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $galerias->where('tipo', 'video')->count() }}</h3>
|
||||
<p>Videos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Table -->
|
||||
<div class="card-admin">
|
||||
<div class="card-body">
|
||||
@if($galerias->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-admin">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Orden</th>
|
||||
<th>Vista Previa</th>
|
||||
<th>Título</th>
|
||||
<th>Tipo</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($galerias as $item)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge-admin bg-info">{{ $item->orden }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($item->tipo === 'video')
|
||||
@if($item->thumbnail)
|
||||
<img src="{{ asset('storage/' . $item->thumbnail) }}"
|
||||
alt="{{ $item->titulo }}"
|
||||
class="img-thumbnail"
|
||||
style="width: 60px; height: 40px; object-fit: cover;">
|
||||
@else
|
||||
<div class="placeholder-thumb">
|
||||
<i class="fas fa-video"></i>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
@if($item->archivo)
|
||||
<img src="{{ asset('storage/' . $item->archivo) }}"
|
||||
alt="{{ $item->titulo }}"
|
||||
class="img-thumbnail"
|
||||
style="width: 60px; height: 40px; object-fit: cover;">
|
||||
@else
|
||||
<div class="placeholder-thumb">
|
||||
<i class="fas fa-image"></i>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ $item->titulo }}</strong>
|
||||
@if($item->descripcion)
|
||||
<br><small class="text-muted">{{ Str::limit($item->descripcion, 40) }}</small>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge-admin {{ $item->tipo === 'video' ? 'bg-warning' : 'bg-info' }}">
|
||||
<i class="fas {{ $item->tipo === 'video' ? 'fa-video' : 'fa-image' }} me-1"></i>
|
||||
{{ ucfirst($item->tipo) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge-admin {{ $item->activo ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ $item->activo ? 'Activo' : 'Inactivo' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a href="{{ route('admin.galeria.edit', $item) }}"
|
||||
class="btn btn-sm btn-primary-admin"
|
||||
title="Editar">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.galeria.destroy', $item) }}"
|
||||
method="POST"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('¿Estás seguro de que deseas eliminar este modelo?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger-admin" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="d-flex justify-content-center">
|
||||
{{ $galerias->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-images"></i>
|
||||
<h4>No hay modelos</h4>
|
||||
<p>Comienza agregando tu primer modelo a la galería</p>
|
||||
<a href="{{ route('admin.galeria.create') }}" class="btn btn-primary-admin mt-3">
|
||||
<i class="fas fa-plus me-2"></i>Agregar Modelo
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.placeholder-thumb {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
background: var(--secondary);
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
161
resources/views/admin/layouts/master.blade.php
Executable file
161
resources/views/admin/layouts/master.blade.php
Executable file
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Lash Vanshy - Panel de Administración">
|
||||
<title>@yield('title', 'Dashboard - Lash Vanshy')</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||
|
||||
<!-- Estilos personalizados -->
|
||||
<link rel="stylesheet" href="{{ asset('css/admin.css') }}">
|
||||
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-wrapper">
|
||||
<!-- Sidebar -->
|
||||
<aside class="admin-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="{{ route('admin.dashboard') }}" class="brand">
|
||||
<span class="brand-icon">✨</span>
|
||||
<span class="brand-text">Lash Vanshy</span>
|
||||
</a>
|
||||
<span class="badge bg-danger">Admin</span>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="fas fa-home"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.galeria.index') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.galeria.*') ? 'active' : '' }}">
|
||||
<i class="fas fa-images"></i>
|
||||
<span>Galería / Modelos</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.productos.index') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.productos.*') ? 'active' : '' }}">
|
||||
<i class="fas fa-spa"></i>
|
||||
<span>Productos / Servicios</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.mensajes.index') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.mensajes.*') ? 'active' : '' }}">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>Mensajes</span>
|
||||
@php
|
||||
$noLeidos = \App\Models\Mensaje::noLeidos()->count();
|
||||
@endphp
|
||||
@if($noLeidos > 0)
|
||||
<span class="badge bg-danger ms-auto">{{ $noLeidos }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@if(Auth::guard('admin')->user()->rol === 'super_admin')
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.configuracion.index') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.configuracion.*') ? 'active' : '' }}">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Configuración</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
{{ strtoupper(substr(Auth::guard('admin')->user()->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="user-name">{{ Auth::guard('admin')->user()->name }}</span>
|
||||
<span class="user-role">{{ Auth::guard('admin')->user()->rol === 'super_admin' ? 'Super Admin' : 'Admin' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<form action="{{ route('admin.logout') }}" method="POST" class="mt-3">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-logout w-100">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>Cerrar Sesión
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="admin-main">
|
||||
<!-- Top Header -->
|
||||
<header class="admin-header">
|
||||
<div class="header-left">
|
||||
<button class="btn-toggle-sidebar d-lg-none" onclick="toggleSidebar()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title">@yield('page-title', 'Dashboard')</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="current-date">
|
||||
<i class="fas fa-calendar-alt me-2"></i>
|
||||
{{ now()->format('d/m/Y') }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="admin-content">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
{{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
@stack('scripts')
|
||||
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
document.querySelector('.admin-sidebar').classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
138
resources/views/admin/mensajes/index.blade.php
Executable file
138
resources/views/admin/mensajes/index.blade.php
Executable file
@@ -0,0 +1,138 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Mensajes - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Mensajes de Contacto')
|
||||
|
||||
@section('content')
|
||||
<!-- Header Actions -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-0">Mensajes</h2>
|
||||
<p class="text-muted mb-0">Gestiona los mensajes recibidos del formulario de contacto</p>
|
||||
</div>
|
||||
@if($mensajes->where('leido', false)->count() > 0)
|
||||
<form action="{{ route('admin.mensajes.leer-todos') }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-check-double me-2"></i>Marcar todos como leídos
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $mensajes->total() }}</h3>
|
||||
<p>Total Mensajes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $mensajes->where('leido', false)->count() }}</h3>
|
||||
<p>Sin Leer</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $mensajes->where('leido', true)->count() }}</h3>
|
||||
<p>Leídos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages Table -->
|
||||
<div class="card-admin">
|
||||
<div class="card-body">
|
||||
@if($mensajes->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-admin">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Email</th>
|
||||
<th>Mensaje</th>
|
||||
<th>Fecha</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($mensajes as $mensaje)
|
||||
<tr class="{{ !$mensaje->leido ? 'table-warning' : '' }}">
|
||||
<td>
|
||||
<strong>{{ $mensaje->nombre }}</strong>
|
||||
@if($mensaje->telefono)
|
||||
<br><small class="text-muted">{{ $mensaje->telefono }}</small>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<a href="mailto:{{ $mensaje->email }}">{{ $mensaje->email }}</a>
|
||||
</td>
|
||||
<td>{{ Str::limit($mensaje->mensaje, 50) }}</td>
|
||||
<td>
|
||||
<small>{{ $mensaje->created_at->format('d/m/Y H:i') }}</small>
|
||||
</td>
|
||||
<td>
|
||||
@if($mensaje->leido)
|
||||
<span class="badge-admin bg-success">Leído</span>
|
||||
@else
|
||||
<span class="badge-admin bg-warning">Nuevo</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a href="{{ route('admin.mensajes.show', $mensaje) }}"
|
||||
class="btn btn-sm btn-primary-admin"
|
||||
title="Ver mensaje">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
@if(!$mensaje->leido)
|
||||
<form action="{{ route('admin.mensajes.leido', $mensaje) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<button type="submit" class="btn btn-sm btn-secondary-admin" title="Marcar como leído">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ route('admin.mensajes.destroy', $mensaje) }}"
|
||||
method="POST"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('¿Estás seguro de que deseas eliminar este mensaje?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger-admin" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="d-flex justify-content-center">
|
||||
{{ $mensajes->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-envelope-open"></i>
|
||||
<h4>No hay mensajes</h4>
|
||||
<p>Los mensajes del formulario de contacto aparecerán aquí</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
119
resources/views/admin/mensajes/show.blade.php
Executable file
119
resources/views/admin/mensajes/show.blade.php
Executable file
@@ -0,0 +1,119 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Ver Mensaje - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Detalle del Mensaje')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.mensajes.index') }}">Mensajes</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Ver Mensaje</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="fas fa-envelope me-2"></i>Mensaje de {{ $mensaje->nombre }}
|
||||
</span>
|
||||
<span class="badge-admin {{ $mensaje->leido ? 'bg-success' : 'bg-warning' }}">
|
||||
{{ $mensaje->leido ? 'Leído' : 'Sin leer' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Message Info -->
|
||||
<div class="message-meta mb-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Nombre</label>
|
||||
<p class="mb-0 fw-bold">{{ $mensaje->nombre }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Email</label>
|
||||
<p class="mb-0">
|
||||
<a href="mailto:{{ $mensaje->email }}">{{ $mensaje->email }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@if($mensaje->telefono)
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Teléfono</label>
|
||||
<p class="mb-0">
|
||||
<a href="tel:{{ $mensaje->telefono }}">{{ $mensaje->telefono }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Fecha</label>
|
||||
<p class="mb-0">{{ $mensaje->created_at->format('d/m/Y H:i') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Message Content -->
|
||||
<div class="message-content">
|
||||
<label class="text-muted small d-block mb-2">Mensaje</label>
|
||||
<div class="p-3 bg-light rounded">
|
||||
{{ $mensaje->mensaje }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.mensajes.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
@if(!$mensaje->leido)
|
||||
<form action="{{ route('admin.mensajes.leido', $mensaje) }}" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-check me-2"></i>Marcar como leído
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ route('admin.mensajes.destroy', $mensaje) }}" method="POST" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este mensaje?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger-admin">
|
||||
<i class="fas fa-trash me-2"></i>Eliminar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Este mensaje fue enviado a través del formulario de contacto en el sitio web.
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2">Puedes responder directamente al email del cliente</li>
|
||||
<li class="mb-2">Los mensajes no leídos aparecen resaltados</li>
|
||||
<li>Puedes eliminar mensajes que ya no necesites</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="mailto:{{ $mensaje->email }}?subject=Re: Tu mensaje desde Lash Vanshy" class="btn btn-primary-admin w-100">
|
||||
<i class="fas fa-reply me-2"></i>Responder por Email
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
197
resources/views/admin/productos/create.blade.php
Executable file
197
resources/views/admin/productos/create.blade.php
Executable file
@@ -0,0 +1,197 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Nuevo Servicio - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Nuevo Servicio')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.productos.index') }}">Servicios</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Nuevo Servicio</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-plus-circle me-2"></i>Nuevo Servicio
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.productos.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nombre" class="form-label">Nombre del Servicio *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('nombre') is-invalid @enderror"
|
||||
id="nombre"
|
||||
name="nombre"
|
||||
value="{{ old('nombre') }}"
|
||||
placeholder="Ej: Extensión de Pestañas Clásicas"
|
||||
required>
|
||||
@error('nombre')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="descripcion" class="form-label">Descripción</label>
|
||||
<textarea class="form-control @error('descripcion') is-invalid @enderror"
|
||||
id="descripcion"
|
||||
name="descripcion"
|
||||
rows="4"
|
||||
placeholder="Describe el servicio, duración, técnicas utilizadas...">{{ old('descripcion') }}</textarea>
|
||||
@error('descripcion')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="precio" class="form-label">Precio ({{ config('currency.symbol') }}) *</label>
|
||||
<input type="number"
|
||||
class="form-control @error('precio') is-invalid @enderror"
|
||||
id="precio"
|
||||
name="precio"
|
||||
value="{{ old('precio') }}"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
required>
|
||||
@error('precio')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="categoria" class="form-label">Categoría *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('categoria') is-invalid @enderror"
|
||||
id="categoria"
|
||||
name="categoria"
|
||||
value="{{ old('categoria') }}"
|
||||
placeholder="Ej: Extensiones, Lifting, Otros"
|
||||
list="categorias-list"
|
||||
required>
|
||||
<datalist id="categorias-list">
|
||||
<option value="Extensiones">
|
||||
<option value="Lifting">
|
||||
<option value="Coloración">
|
||||
<option value="Tratamientos">
|
||||
<option value="Otros">
|
||||
</datalist>
|
||||
@error('categoria')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="imagen" class="form-label">Imagen del Servicio</label>
|
||||
<input type="file"
|
||||
class="form-control @error('imagen') is-invalid @enderror"
|
||||
id="imagen"
|
||||
name="imagen"
|
||||
accept="image/*"
|
||||
onchange="previewImage(event, 'imagen-preview')">
|
||||
<small class="text-muted">Formats: JPG, PNG, GIF. Max: 5MB. Opcional</small>
|
||||
<div class="mt-2">
|
||||
<img id="imagen-preview" class="image-preview" style="display: none;">
|
||||
</div>
|
||||
@error('imagen')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="orden" class="form-label">Orden</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="orden"
|
||||
name="orden"
|
||||
value="{{ old('orden', 0) }}"
|
||||
min="0"
|
||||
placeholder="0">
|
||||
<small class="text-muted">Orden de visualización (menor = primero)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Opciones</label>
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="destacado"
|
||||
name="destacado"
|
||||
value="1"
|
||||
{{ old('destacado') ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="destacado">⭐ Servicio destacado</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="activo"
|
||||
name="activo"
|
||||
value="1"
|
||||
{{ old('activo', true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="activo">Activo (visible en web)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.productos.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Guardar Servicio
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Los servicios son los tratamientos que ofreces en tu negocio. Cada servicio puede tener precio, descripción e imagen.
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2">Los servicios destacados aparecen en la página principal</li>
|
||||
<li class="mb-2">Las categorías te permiten filtrar los servicios</li>
|
||||
<li>Usa el campo "orden" para controlar el orden de visualización</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function previewImage(event, previewId) {
|
||||
const file = event.target.files[0];
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
preview.src = e.target.result;
|
||||
preview.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
214
resources/views/admin/productos/edit.blade.php
Executable file
214
resources/views/admin/productos/edit.blade.php
Executable file
@@ -0,0 +1,214 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Editar Servicio - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Editar Servicio')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.productos.index') }}">Servicios</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Editar Servicio</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-edit me-2"></i>Editar Servicio
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.productos.update', $producto) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nombre" class="form-label">Nombre del Servicio *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('nombre') is-invalid @enderror"
|
||||
id="nombre"
|
||||
name="nombre"
|
||||
value="{{ old('nombre', $producto->nombre) }}"
|
||||
placeholder="Ej: Extensión de Pestañas Clásicas"
|
||||
required>
|
||||
@error('nombre')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="descripcion" class="form-label">Descripción</label>
|
||||
<textarea class="form-control @error('descripcion') is-invalid @enderror"
|
||||
id="descripcion"
|
||||
name="descripcion"
|
||||
rows="4"
|
||||
placeholder="Describe el servicio, duración, técnicas utilizadas...">{{ old('descripcion', $producto->descripcion) }}</textarea>
|
||||
@error('descripcion')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="precio" class="form-label">Precio ({{ config('currency.symbol') }}) *</label>
|
||||
<input type="number"
|
||||
class="form-control @error('precio') is-invalid @enderror"
|
||||
id="precio"
|
||||
name="precio"
|
||||
value="{{ old('precio', $producto->precio) }}"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
required>
|
||||
@error('precio')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="categoria" class="form-label">Categoría *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('categoria') is-invalid @enderror"
|
||||
id="categoria"
|
||||
name="categoria"
|
||||
value="{{ old('categoria', $producto->categoria) }}"
|
||||
placeholder="Ej: Extensiones, Lifting, Otros"
|
||||
list="categorias-list"
|
||||
required>
|
||||
<datalist id="categorias-list">
|
||||
<option value="Extensiones">
|
||||
<option value="Lifting">
|
||||
<option value="Coloración">
|
||||
<option value="Tratamientos">
|
||||
<option value="Otros">
|
||||
</datalist>
|
||||
@error('categoria')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Image -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Imagen Actual</label>
|
||||
@if($producto->imagen)
|
||||
<div class="mb-2">
|
||||
<img src="{{ asset('storage/' . $producto->imagen) }}"
|
||||
alt="{{ $producto->nombre }}"
|
||||
class="image-preview"
|
||||
style="max-height: 200px;">
|
||||
</div>
|
||||
<small class="text-muted">Deja este campo vacío para mantener la imagen actual</small>
|
||||
@else
|
||||
<p class="text-muted">No hay imagen cargada</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="imagen" class="form-label">Nueva Imagen</label>
|
||||
<input type="file"
|
||||
class="form-control @error('imagen') is-invalid @enderror"
|
||||
id="imagen"
|
||||
name="imagen"
|
||||
accept="image/*"
|
||||
onchange="previewImage(event, 'imagen-preview-edit')">
|
||||
<small class="text-muted">Formats: JPG, PNG, GIF. Max: 5MB</small>
|
||||
<div class="mt-2">
|
||||
<img id="imagen-preview-edit" class="image-preview" style="display: none;">
|
||||
</div>
|
||||
@error('imagen')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="orden" class="form-label">Orden</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="orden"
|
||||
name="orden"
|
||||
value="{{ old('orden', $producto->orden) }}"
|
||||
min="0"
|
||||
placeholder="0">
|
||||
<small class="text-muted">Orden de visualización (menor = primero)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Opciones</label>
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="destacado"
|
||||
name="destacado"
|
||||
value="1"
|
||||
{{ $producto->destacado ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="destacado">⭐ Servicio destacado</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="activo"
|
||||
name="activo"
|
||||
value="1"
|
||||
{{ $producto->activo ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="activo">Activo (visible en web)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.productos.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Actualizar Servicio
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Los servicios son los tratamientos que ofreces en tu negocio. Cada servicio puede tener precio, descripción e imagen.
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2">Los servicios destacados aparecen en la página principal</li>
|
||||
<li class="mb-2">Las categorías te permiten filtrar los servicios</li>
|
||||
<li>Usa el campo "orden" para controlar el orden de visualización</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function previewImage(event, previewId) {
|
||||
const file = event.target.files[0];
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
preview.src = e.target.result;
|
||||
preview.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
preview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
158
resources/views/admin/productos/index.blade.php
Executable file
158
resources/views/admin/productos/index.blade.php
Executable file
@@ -0,0 +1,158 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Servicios - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Gestión de Servicios')
|
||||
|
||||
@section('content')
|
||||
<!-- Header Actions -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-0">Servicios / Productos</h2>
|
||||
<p class="text-muted mb-0">Administra los servicios que ofreces</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.productos.create') }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Nuevo Servicio
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $productos->total() }}</h3>
|
||||
<p>Total Servicios</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $productos->where('destacado', true)->count() }}</h3>
|
||||
<p>Destacados</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $productos->where('activo', true)->count() }}</h3>
|
||||
<p>Activos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products Table -->
|
||||
<div class="card-admin">
|
||||
<div class="card-body">
|
||||
@if($productos->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-admin">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Orden</th>
|
||||
<th>Imagen</th>
|
||||
<th>Nombre</th>
|
||||
<th>Categoría</th>
|
||||
<th>Precio</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($productos as $producto)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge-admin bg-info">{{ $producto->orden }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($producto->imagen)
|
||||
<img src="{{ asset('storage/' . $producto->imagen) }}"
|
||||
alt="{{ $producto->nombre }}"
|
||||
class="img-thumbnail"
|
||||
style="width: 60px; height: 40px; object-fit: cover;">
|
||||
@else
|
||||
<div class="placeholder-thumb">
|
||||
<i class="fas fa-spa"></i>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ $producto->nombre }}</strong>
|
||||
@if($producto->destacado)
|
||||
<br><span class="badge bg-warning badge-sm">⭐ Destacado</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge-admin bg-primary">{{ $producto->categoria }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ $producto->precio_formateado }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge-admin {{ $producto->activo ? 'bg-success' : 'bg-danger' }}">
|
||||
{{ $producto->activo ? 'Activo' : 'Inactivo' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a href="{{ route('admin.productos.edit', $producto) }}"
|
||||
class="btn btn-sm btn-primary-admin"
|
||||
title="Editar">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.productos.destroy', $producto) }}"
|
||||
method="POST"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('¿Estás seguro de que deseas eliminar este servicio?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger-admin" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="d-flex justify-content-center">
|
||||
{{ $productos->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-spa"></i>
|
||||
<h4>No hay servicios</h4>
|
||||
<p>Comienza agregando tu primer servicio</p>
|
||||
<a href="{{ route('admin.productos.create') }}" class="btn btn-primary-admin mt-3">
|
||||
<i class="fas fa-plus me-2"></i>Agregar Servicio
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.placeholder-thumb {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
background: var(--secondary);
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.badge-sm {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
125
resources/views/admin/usuarios/create.blade.php
Executable file
125
resources/views/admin/usuarios/create.blade.php
Executable file
@@ -0,0 +1,125 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Nuevo Usuario - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Nuevo Usuario')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.users.index') }}">Usuarios</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Nuevo Usuario</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-user-plus me-2"></i>Nuevo Usuario
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.users.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nombre *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('name') is-invalid @enderror"
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ old('name') }}"
|
||||
placeholder="Nombre completo"
|
||||
required>
|
||||
@error('name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email *</label>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="email@ejemplo.com"
|
||||
required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Contraseña *</label>
|
||||
<input type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
required>
|
||||
<small class="text-muted">Mínimo 8 caracteres</small>
|
||||
@error('password')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password_confirmation" class="form-label">Confirmar Contraseña *</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
placeholder="••••••••"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="rol" class="form-label">Rol *</label>
|
||||
<select class="form-select @error('rol') is-invalid @enderror"
|
||||
id="rol"
|
||||
name="rol"
|
||||
required>
|
||||
<option value="">Selecciona el rol</option>
|
||||
<option value="admin" {{ old('rol') === 'admin' ? 'selected' : '' }}>Admin</option>
|
||||
<option value="super_admin" {{ old('rol') === 'super_admin' ? 'selected' : '' }}>Super Admin</option>
|
||||
</select>
|
||||
<small class="text-muted">El Super Admin tiene acceso completo a todas las funciones</small>
|
||||
@error('rol')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Crear Usuario
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Los usuarios administradores pueden acceder al panel de gestión del sitio web.
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2"><strong>Admin:</strong> Puede gestionar galería, productos y mensajes</li>
|
||||
<li class="mb-2"><strong>Super Admin:</strong> Acceso completo incluyendo gestión de usuarios</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
126
resources/views/admin/usuarios/edit.blade.php
Executable file
126
resources/views/admin/usuarios/edit.blade.php
Executable file
@@ -0,0 +1,126 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Editar Usuario - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Editar Usuario')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.users.index') }}">Usuarios</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Editar Usuario</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-user-edit me-2"></i>Editar Usuario
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.users.update', $usuario) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nombre *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('name') is-invalid @enderror"
|
||||
id="name"
|
||||
name="name"
|
||||
value="{{ old('name', $usuario->name) }}"
|
||||
placeholder="Nombre completo"
|
||||
required>
|
||||
@error('name')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email *</label>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email', $usuario->email) }}"
|
||||
placeholder="email@ejemplo.com"
|
||||
required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Nueva Contraseña</label>
|
||||
<input type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="••••••••">
|
||||
<small class="text-muted">Deja vacío para mantener la contraseña actual. Mínimo 8 caracteres</small>
|
||||
@error('password')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password_confirmation" class="form-label">Confirmar Contraseña</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
placeholder="••••••••">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="rol" class="form-label">Rol *</label>
|
||||
<select class="form-select @error('rol') is-invalid @enderror"
|
||||
id="rol"
|
||||
name="rol"
|
||||
required
|
||||
{{ $usuario->id === Auth::guard('admin')->user()->id ? 'disabled' : '' }}>
|
||||
<option value="admin" {{ $usuario->rol === 'admin' ? 'selected' : '' }}>Admin</option>
|
||||
<option value="super_admin" {{ $usuario->rol === 'super_admin' ? 'selected' : '' }}>Super Admin</option>
|
||||
</select>
|
||||
@if($usuario->id === Auth::guard('admin')->user()->id)
|
||||
<small class="text-warning">No puedes cambiar tu propio rol</small>
|
||||
@endif
|
||||
@error('rol')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Actualizar Usuario
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Los usuarios administradores pueden acceder al panel de gestión del sitio web.
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2"><strong>Admin:</strong> Puede gestionar galería, productos y mensajes</li>
|
||||
<li class="mb-2"><strong>Super Admin:</strong> Acceso completo incluyendo gestión de usuarios</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
138
resources/views/admin/usuarios/index.blade.php
Executable file
138
resources/views/admin/usuarios/index.blade.php
Executable file
@@ -0,0 +1,138 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Usuarios Admin - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Gestión de Usuarios')
|
||||
|
||||
@section('content')
|
||||
<!-- Header Actions -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-0">Usuarios Administradores</h2>
|
||||
<p class="text-muted mb-0">Administra los usuarios del panel</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.users.create') }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Nuevo Usuario
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $usuarios->total() }}</h3>
|
||||
<p>Total Usuarios</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $usuarios->where('rol', 'super_admin')->count() }}</h3>
|
||||
<p>Super Admin</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="stat-card py-3">
|
||||
<div class="stat-info">
|
||||
<h3>{{ $usuarios->where('rol', 'admin')->count() }}</h3>
|
||||
<p>Admin</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="card-admin">
|
||||
<div class="card-body">
|
||||
@if($usuarios->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-admin">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Email</th>
|
||||
<th>Rol</th>
|
||||
<th>Fecha Alta</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($usuarios as $usuario)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="user-avatar-sm">
|
||||
{{ strtoupper(substr($usuario->name, 0, 1)) }}
|
||||
</div>
|
||||
<strong>{{ $usuario->name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ $usuario->email }}</td>
|
||||
<td>
|
||||
<span class="badge-admin {{ $usuario->rol === 'super_admin' ? 'bg-danger' : 'bg-primary' }}">
|
||||
{{ $usuario->rol === 'super_admin' ? 'Super Admin' : 'Admin' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ $usuario->created_at->format('d/m/Y') }}</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a href="{{ route('admin.users.edit', $usuario) }}"
|
||||
class="btn btn-sm btn-primary-admin"
|
||||
title="Editar">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
@if($usuario->id !== Auth::guard('admin')->user()->id)
|
||||
<form action="{{ route('admin.users.destroy', $usuario) }}"
|
||||
method="POST"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('¿Estás seguro de que deseas eliminar este usuario?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger-admin" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="d-flex justify-content-center">
|
||||
{{ $usuarios->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-users"></i>
|
||||
<h4>No hay usuarios</h4>
|
||||
<p>Comienza agregando tu primer usuario administrador</p>
|
||||
<a href="{{ route('admin.users.create') }}" class="btn btn-primary-admin mt-3">
|
||||
<i class="fas fa-plus me-2"></i>Agregar Usuario
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.user-avatar-sm {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
281
resources/views/frontend/contacto/index.blade.php
Executable file
281
resources/views/frontend/contacto/index.blade.php
Executable file
@@ -0,0 +1,281 @@
|
||||
@extends('frontend.layouts.main')
|
||||
|
||||
@section('title', 'Contacto - Lash Vanshy')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<section class="page-header">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Contáctanos</h1>
|
||||
<p>Estamos aquí para responder a todas tus preguntas</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section class="contact-section section-padding">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- Contact Info -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<div class="contact-info h-100">
|
||||
<h3 class="mb-4">Información de Contacto</h3>
|
||||
|
||||
<div class="contact-info-item">
|
||||
<i class="fas fa-phone-alt"></i>
|
||||
<div>
|
||||
<strong>Teléfono</strong>
|
||||
@if(!empty($configuracion['telefono']))
|
||||
<a href="tel:{{ $configuracion['telefono'] }}">{{ $configuracion['telefono'] }}</a>
|
||||
@else
|
||||
<span>+34 000 000 000</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info-item">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<div>
|
||||
<strong>Email</strong>
|
||||
@if(!empty($configuracion['email']))
|
||||
<a href="mailto:{{ $configuracion['email'] }}">{{ $configuracion['email'] }}</a>
|
||||
@else
|
||||
<span>contacto@lashvanshy.com</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<div>
|
||||
<strong>Dirección</strong>
|
||||
@if(!empty($configuracion['direccion']))
|
||||
<span>{{ $configuracion['direccion'] }}</span>
|
||||
@else
|
||||
<span>Tu dirección aquí</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-info-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<div>
|
||||
<strong>Horario</strong>
|
||||
@if(!empty($configuracion['horario']))
|
||||
<span>{!! nl2br(e($configuracion['horario'])) !!}</span>
|
||||
@else
|
||||
<span>Lunes - Viernes: 10:00 - 20:00</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h5 class="mb-3">Síguenos en redes sociales</h5>
|
||||
<div class="social-links">
|
||||
@if(!empty($configuracion['facebook']))
|
||||
<a href="{{ $configuracion['facebook'] }}" target="_blank" class="social-link" title="Facebook">
|
||||
<i class="fab fa-facebook-f"></i>
|
||||
</a>
|
||||
@endif
|
||||
@if(!empty($configuracion['instagram']))
|
||||
<a href="{{ $configuracion['instagram'] }}" target="_blank" class="social-link" title="Instagram">
|
||||
<i class="fab fa-instagram"></i>
|
||||
</a>
|
||||
@endif
|
||||
@if(!empty($configuracion['whatsapp']))
|
||||
<a href="{{ $configuracion['whatsapp'] }}" target="_blank" class="social-link" title="WhatsApp">
|
||||
<i class="fab fa-whatsapp"></i>
|
||||
</a>
|
||||
@endif
|
||||
@if(!empty($configuracion['tiktok']))
|
||||
<a href="{{ $configuracion['tiktok'] }}" target="_blank" class="social-link" title="TikTok">
|
||||
<i class="fab fa-tiktok"></i>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div class="col-lg-7">
|
||||
<div class="contact-card">
|
||||
<div class="contact-form">
|
||||
<h3 class="mb-4">Envíanos un Mensaje</h3>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
<ul class="mb-0">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('contacto.send') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nombre" class="form-label">Nombre completo *</label>
|
||||
<input type="text"
|
||||
class="form-control @error('nombre') is-invalid @enderror"
|
||||
id="nombre"
|
||||
name="nombre"
|
||||
value="{{ old('nombre') }}"
|
||||
placeholder="Tu nombre"
|
||||
required>
|
||||
@error('nombre')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="email" class="form-label">Email *</label>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="tu@email.com"
|
||||
required>
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="telefono" class="form-label">Teléfono (opcional)</label>
|
||||
<input type="tel"
|
||||
class="form-control @error('telefono') is-invalid @enderror"
|
||||
id="telefono"
|
||||
name="telefono"
|
||||
value="{{ old('telefono') }}"
|
||||
placeholder="Tu número de teléfono">
|
||||
@error('telefono')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="mensaje" class="form-label">Mensaje *</label>
|
||||
<textarea class="form-control @error('mensaje') is-invalid @enderror"
|
||||
id="mensaje"
|
||||
name="mensaje"
|
||||
rows="5"
|
||||
placeholder="¿En qué podemos ayudarte?"
|
||||
required>{{ old('mensaje') }}</textarea>
|
||||
@error('mensaje')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary-custom">
|
||||
<i class="fas fa-paper-plane me-2"></i>Enviar Mensaje
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, var(--primary) 100%);
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--text-light);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, var(--primary) 100%);
|
||||
padding: 3rem;
|
||||
border-radius: 20px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.contact-info h3 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.contact-info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-info-item i {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary-dark);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-info-item strong {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.contact-info-item a,
|
||||
.contact-info-item span {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.contact-form .form-label {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.contact-form .form-control {
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.contact-form .form-control:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(248, 180, 196, 0.2);
|
||||
}
|
||||
|
||||
.contact-form .form-control.is-invalid {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.contact-form .invalid-feedback {
|
||||
color: var(--danger);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
167
resources/views/frontend/galeria/index.blade.php
Executable file
167
resources/views/frontend/galeria/index.blade.php
Executable file
@@ -0,0 +1,167 @@
|
||||
@extends('frontend.layouts.main')
|
||||
|
||||
@section('title', 'Galería - Lash Vanshy')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<section class="page-header">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Nuestra Galería</h1>
|
||||
<p>Explora nuestros trabajos realizados y transforma tu mirada</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gallery Section -->
|
||||
<section class="gallery-section section-padding">
|
||||
<div class="container">
|
||||
<!-- Filters -->
|
||||
<div class="gallery-filters">
|
||||
<button class="filter-btn {{ $tipo === 'todos' ? 'active' : '' }}"
|
||||
onclick="filterGallery('todos')">
|
||||
<i class="fas fa-th-large me-2"></i>Todos ({{ $stats['total'] }})
|
||||
</button>
|
||||
<button class="filter-btn {{ $tipo === 'imagen' ? 'active' : '' }}"
|
||||
onclick="filterGallery('imagen')">
|
||||
<i class="fas fa-image me-2"></i>Imágenes ({{ $stats['imagenes'] }})
|
||||
</button>
|
||||
<button class="filter-btn {{ $tipo === 'video' ? 'active' : '' }}"
|
||||
onclick="filterGallery('video')">
|
||||
<i class="fas fa-video me-2"></i>Videos ({{ $stats['videos'] }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Grid -->
|
||||
@if($galeria->isNotEmpty())
|
||||
<div class="gallery-grid">
|
||||
@foreach($galeria as $item)
|
||||
<div class="gallery-item" data-type="{{ $item->tipo }}">
|
||||
@if($item->tipo === 'video')
|
||||
@if($item->thumbnail)
|
||||
<img src="{{ asset('storage/' . $item->thumbnail) }}"
|
||||
alt="{{ $item->titulo }}"
|
||||
loading="lazy">
|
||||
@elseif($item->archivo)
|
||||
<video poster="{{ asset('storage/' . $item->archivo . '/preview.jpg') }}">
|
||||
<source src="{{ asset('storage/' . $item->archivo) }}" type="video/mp4">
|
||||
</video>
|
||||
@else
|
||||
<div class="placeholder-img w-100 h-100">
|
||||
<i class="fas fa-video fa-2x"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="play-icon">
|
||||
<i class="fas fa-play"></i>
|
||||
</div>
|
||||
@else
|
||||
@if($item->archivo)
|
||||
<a href="{{ asset('storage/' . $item->archivo) }}"
|
||||
data-lightbox="gallery"
|
||||
data-title="{{ $item->titulo }}">
|
||||
<img src="{{ asset('storage/' . $item->archivo) }}"
|
||||
alt="{{ $item->titulo }}"
|
||||
loading="lazy">
|
||||
</a>
|
||||
@else
|
||||
<div class="placeholder-img w-100 h-100">
|
||||
<i class="fas fa-image fa-2x"></i>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div class="overlay">
|
||||
<div class="overlay-content">
|
||||
<h4>{{ $item->titulo }}</h4>
|
||||
@if($item->descripcion)
|
||||
<p>{{ Str::limit($item->descripcion, 60) }}</p>
|
||||
@endif
|
||||
<span class="badge bg-light text-dark mt-2">
|
||||
<i class="fas {{ $item->tipo === 'video' ? 'fa-video' : 'fa-image' }} me-1"></i>
|
||||
{{ $item->tipo === 'video' ? 'Video' : 'Imagen' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination-wrapper">
|
||||
{{ $galeria->appends(['tipo' => $tipo])->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-images"></i>
|
||||
<h4>No hay elementos en la galería</h4>
|
||||
<p>Próximamente subiremos más contenido. ¡Vuelve pronto!</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, var(--primary) 100%);
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--text-light);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--text);
|
||||
border: 2px solid var(--border);
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function filterGallery(tipo) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('tipo', tipo);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
// Configurar Lightbox
|
||||
lightbox.option({
|
||||
'resizeDuration': 200,
|
||||
'wrapAround': true,
|
||||
'albumLabel': 'Imagen %1 de %2',
|
||||
'fadeDuration': 300,
|
||||
'imageFadeDuration': 300
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
209
resources/views/frontend/home/index.blade.php
Executable file
209
resources/views/frontend/home/index.blade.php
Executable file
@@ -0,0 +1,209 @@
|
||||
@extends('frontend.layouts.main')
|
||||
|
||||
@section('title', 'Lash Vanshy - Extensiones de Pestañas Profesionales')
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6 order-2 order-lg-1">
|
||||
<div class="hero-content text-center text-lg-start">
|
||||
<h1 class="hero-title">
|
||||
<span>Lash Vanshy</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
Extensiones de pestañas profesionales con productos de la más alta calidad.
|
||||
Destaca tu belleza natural con nuestros servicios especializados y personalizados.
|
||||
</p>
|
||||
<a href="{{ route('productos') }}" class="hero-btn">
|
||||
Ver Servicios <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 order-1 order-lg-2 mb-4 mb-lg-0">
|
||||
<div class="hero-image-wrapper">
|
||||
<img src="https://images.unsplash.com/photo-1516975080664-ed2fc6a32937?w=600&h=500&fit=crop"
|
||||
alt="Extensiones de pestañas profesionales"
|
||||
class="hero-image img-fluid"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 400 300%22%3E%3Crect fill=%22%23FFE4EC%22 width=%22400%22 height=%22300%22/%3E%3Ctext x=%22200%22 y=%22150%22 text-anchor=%22middle%22 fill=%22%23E89AAD%22 font-size=%2240%22%3E%E2%9C%A8%3C/text%3E%3Ctext x=%22200%22 y=%22190%22 text-anchor=%22middle%22 fill=%22%234A4A4A%22 font-size=%2216%22%3ELash Vanshy%3C/text%3E%3C/svg%3E'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Servicios Destacados -->
|
||||
@if($productosDestacados->isNotEmpty())
|
||||
<section class="section-padding">
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>Servicios <span>Destacados</span></h2>
|
||||
<p>Descubre nuestros servicios más populares y solicitados por nuestras clientas.</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
@foreach($productosDestacados as $producto)
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card-custom h-100">
|
||||
<div class="position-relative">
|
||||
@if($producto->imagen)
|
||||
<img src="{{ asset('storage/' . $producto->imagen) }}"
|
||||
alt="{{ $producto->nombre }}"
|
||||
class="card-img-top">
|
||||
@else
|
||||
<div class="card-img-top placeholder-img">
|
||||
<i class="fas fa-eye"></i>
|
||||
</div>
|
||||
@endif
|
||||
<span class="badge-destacado">⭐ Destacado</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<span class="text-primary small text-uppercase fw-bold">{{ $producto->categoria }}</span>
|
||||
<h5 class="card-title mt-2">{{ $producto->nombre }}</h5>
|
||||
<p class="card-text">{{ Str::limit($producto->descripcion, 100) }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="price">{{ $producto->precio_formateado }}</span>
|
||||
<a href="{{ route('contacto') }}" class="btn btn-sm btn-outline-primary">
|
||||
Reservar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="{{ route('productos') }}" class="hero-btn">
|
||||
Ver Todos los Servicios <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<!-- Galería Preview -->
|
||||
@if($galeria->isNotEmpty())
|
||||
<section class="section-padding bg-secondary">
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>Nuestro <span>Trabajo</span></h2>
|
||||
<p>Mira algunos de nuestros trabajos realizados. Cada cliente es único y especial para nosotros.</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
@foreach($galeria as $item)
|
||||
<div class="col-6 col-md-4 col-lg-3">
|
||||
<div class="gallery-item">
|
||||
@if($item->tipo === 'video')
|
||||
@if($item->thumbnail)
|
||||
<img src="{{ asset('storage/' . $item->thumbnail) }}" alt="{{ $item->titulo }}">
|
||||
@else
|
||||
<div class="placeholder-img w-100 h-100">
|
||||
<i class="fas fa-play"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="play-icon">
|
||||
<i class="fas fa-play"></i>
|
||||
</div>
|
||||
@else
|
||||
@if($item->archivo)
|
||||
<img src="{{ asset('storage/' . $item->archivo) }}" alt="{{ $item->titulo }}">
|
||||
@else
|
||||
<div class="placeholder-img w-100 h-100">
|
||||
<i class="fas fa-image"></i>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<div class="overlay">
|
||||
<div class="overlay-content">
|
||||
<h4>{{ $item->titulo }}</h4>
|
||||
@if($item->descripcion)
|
||||
<p>{{ Str::limit($item->descripcion, 50) }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="{{ route('galeria') }}" class="hero-btn">
|
||||
Ver Galería Completa <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<!-- Por qué elegirnos -->
|
||||
<section class="section-padding">
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>¿Por qué <span>Elegirnos?</span></h2>
|
||||
<p>Nos comprometemos a darte los mejores resultados.</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center p-4">
|
||||
<div class="feature-icon mb-3">
|
||||
<i class="fas fa-gem"></i>
|
||||
</div>
|
||||
<h4>Productos de Calidad</h4>
|
||||
<p class="text-muted">Utilizamos solo productos premium y seguros para tus pestañas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center p-4">
|
||||
<div class="feature-icon mb-3">
|
||||
<i class="fas fa-user-tie"></i>
|
||||
</div>
|
||||
<h4>Profesionales Expertas</h4>
|
||||
<p class="text-muted">Nuestro equipo tiene años de experiencia en el sector.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center p-4">
|
||||
<div class="feature-icon mb-3">
|
||||
<i class="fas fa-heart"></i>
|
||||
</div>
|
||||
<h4>Atención Personalizada</h4>
|
||||
<p class="text-muted">Cada clienta recibe un tratamiento adaptado a sus necesidades.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Contacto -->
|
||||
<section class="section-padding bg-primary-custom">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 text-center">
|
||||
<h2 class="mb-3">¿Lista para transformar tu mirada?</h2>
|
||||
<p class="mb-4">Contáctanos hoy mismo y agenda tu cita. Estaremos encantadas de atenderte.</p>
|
||||
<a href="{{ route('contacto') }}" class="hero-btn">
|
||||
Contactar Ahora <i class="fas fa-envelope ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.feature-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--secondary), var(--primary));
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
144
resources/views/frontend/layouts/main.blade.php
Executable file
144
resources/views/frontend/layouts/main.blade.php
Executable file
@@ -0,0 +1,144 @@
|
||||
@php
|
||||
$configuracion = \App\Models\Configuracion::allAsArray();
|
||||
@endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Lash Vanshy - Extensiones de pestañas profesionales. Servicios de extensiones de pestañas, lift de pestañas y más.">
|
||||
<title>@yield('title', 'Lash Vanshy - Extensiones de Pestañas Profesionales')</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<!-- Lightbox CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/lightbox2@2.11.4/dist/css/lightbox.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Estilos personalizados -->
|
||||
<link rel="stylesheet" href="{{ asset('css/frontend.css') }}">
|
||||
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<header class="header-main">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ route('home') }}">
|
||||
<span class="brand-icon">✨</span>
|
||||
<span class="brand-text">Lash Vanshy</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ request()->routeIs('home') ? 'active' : '' }}" href="{{ route('home') }}">Inicio</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ request()->is('galeria*') ? 'active' : '' }}" href="{{ route('galeria') }}">Galería</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ request()->is('productos*') ? 'active' : '' }}" href="{{ route('productos') }}">Servicios</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ request()->is('contacto*') ? 'active' : '' }}" href="{{ route('contacto') }}">Contacto</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Contenido principal -->
|
||||
<main class="main-content">
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer-main">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 mb-4 mb-lg-0">
|
||||
<h5 class="footer-title">Lash Vanshy</h5>
|
||||
<p class="footer-description">
|
||||
Extensions de pestañas profesionales con productos de la más alta calidad. Destaca tu belleza natural con我们的 servicios especializados.
|
||||
</p>
|
||||
<div class="social-links">
|
||||
<a href="{{ $configuracion['facebook'] ?? '#' }}" target="_blank" class="social-link" title="Facebook">
|
||||
<i class="fab fa-facebook-f"></i>
|
||||
</a>
|
||||
<a href="{{ $configuracion['instagram'] ?? '#' }}" target="_blank" class="social-link" title="Instagram">
|
||||
<i class="fab fa-instagram"></i>
|
||||
</a>
|
||||
<a href="{{ $configuracion['whatsapp'] ?? '#' }}" target="_blank" class="social-link" title="WhatsApp">
|
||||
<i class="fab fa-whatsapp"></i>
|
||||
</a>
|
||||
<a href="{{ $configuracion['tiktok'] ?? '#' }}" target="_blank" class="social-link" title="TikTok">
|
||||
<i class="fab fa-tiktok"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 mb-4 mb-lg-0">
|
||||
<h5 class="footer-title">Contacto</h5>
|
||||
<ul class="footer-contact">
|
||||
@if(!empty($configuracion['telefono']))
|
||||
<li>
|
||||
<i class="fas fa-phone"></i>
|
||||
<a href="tel:{{ $configuracion['telefono'] }}">{{ $configuracion['telefono'] }}</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(!empty($configuracion['email']))
|
||||
<li>
|
||||
<i class="fas fa-envelope"></i>
|
||||
<a href="mailto:{{ $configuracion['email'] }}">{{ $configuracion['email'] }}</a>
|
||||
</li>
|
||||
@endif
|
||||
@if(!empty($configuracion['direccion']))
|
||||
<li>
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>{{ $configuracion['direccion'] }}</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<h5 class="footer-title">Horario</h5>
|
||||
<ul class="footer-schedule">
|
||||
@if(!empty($configuracion['horario']))
|
||||
<li><span>{!! nl2br(e($configuracion['horario'])) !!}</span></li>
|
||||
@else
|
||||
<li><span>Lunes - Viernes: 10:00 - 20:00</span></li>
|
||||
<li><span>Sábado: 10:00 - 18:00</span></li>
|
||||
<li><span>Domingo: Cerrado</span></li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© {{ date('Y') }} Lash Vanshy. Todos los derechos reservados.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Lightbox JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/lightbox2@2.11.4/dist/js/lightbox.min.js"></script>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
178
resources/views/frontend/productos/index.blade.php
Executable file
178
resources/views/frontend/productos/index.blade.php
Executable file
@@ -0,0 +1,178 @@
|
||||
@extends('frontend.layouts.main')
|
||||
|
||||
@section('title', 'Servicios - Lash Vanshy')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<section class="page-header">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Nuestros Servicios</h1>
|
||||
<p>Descubre todos los servicios que temos para ti</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Productos Section -->
|
||||
<section class="products-section section-padding">
|
||||
<div class="container">
|
||||
<!-- Category Filters -->
|
||||
<div class="category-filters">
|
||||
<button class="filter-btn {{ $categoria === 'todos' ? 'active' : '' }}"
|
||||
onclick="filterProducts('todos')">
|
||||
<i class="fas fa-th-large me-2"></i>Todos
|
||||
</button>
|
||||
@foreach($categorias as $cat)
|
||||
<button class="filter-btn {{ $categoria === $cat ? 'active' : '' }}"
|
||||
onclick="filterProducts('{{ $cat }}')">
|
||||
{{ $cat }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Featured Products -->
|
||||
@if($destacados->isNotEmpty() && $productos->currentPage() === 1)
|
||||
<div class="mb-5">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-star text-warning me-2"></i>Servicios Destacados
|
||||
</h3>
|
||||
<div class="products-grid">
|
||||
@foreach($destacados as $producto)
|
||||
<div class="product-card">
|
||||
@if($producto->imagen)
|
||||
<img src="{{ asset('storage/' . $producto->imagen) }}"
|
||||
alt="{{ $producto->nombre }}">
|
||||
@else
|
||||
<div class="placeholder-img" style="height: 250px;">
|
||||
<i class="fas fa-spa fa-2x"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="product-info">
|
||||
<span class="product-category">{{ $producto->categoria }}</span>
|
||||
<h3 class="product-title">{{ $producto->nombre }}</h3>
|
||||
<p class="product-description">{{ Str::limit($producto->descripcion, 120) }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="product-price">{{ $producto->precio_formateado }}</span>
|
||||
<a href="{{ route('contacto') }}" class="btn btn-sm btn-outline-primary rounded-pill">
|
||||
Reservar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- All Products -->
|
||||
<div>
|
||||
<h3 class="mb-4">Todos los Servicios</h3>
|
||||
@if($productos->isNotEmpty())
|
||||
<div class="products-grid">
|
||||
@foreach($productos as $producto)
|
||||
<div class="product-card">
|
||||
@if($producto->imagen)
|
||||
<img src="{{ asset('storage/' . $producto->imagen) }}"
|
||||
alt="{{ $producto->nombre }}">
|
||||
@else
|
||||
<div class="placeholder-img" style="height: 250px;">
|
||||
<i class="fas fa-spa fa-2x"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="product-info">
|
||||
<span class="product-category">{{ $producto->categoria }}</span>
|
||||
<h3 class="product-title">{{ $producto->nombre }}</h3>
|
||||
<p class="product-description">{{ Str::limit($producto->descripcion, 120) }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="product-price">{{ $producto->precio_formateado }}</span>
|
||||
<a href="{{ route('contacto') }}" class="btn btn-sm btn-outline-primary rounded-pill">
|
||||
Reservar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination-wrapper">
|
||||
{{ $productos->appends(['categoria' => $categoria])->links() }}
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-spa"></i>
|
||||
<h4>No hay servicios disponibles</h4>
|
||||
<p>Próximamente tendremos más servicios. ¡Vuelve pronto!</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, var(--primary) 100%);
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--text-light);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--text);
|
||||
border: 2px solid var(--border);
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function filterProducts(categoria) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('categoria', categoria);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
225
resources/views/welcome.blade.php
Executable file
225
resources/views/welcome.blade.php
Executable file
File diff suppressed because one or more lines are too long
79
routes/admin.php
Executable file
79
routes/admin.php
Executable file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\AdminUserController;
|
||||
use App\Http\Controllers\Admin\AuthController;
|
||||
use App\Http\Controllers\Admin\ConfiguracionController;
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Admin\GaleriaController;
|
||||
use App\Http\Controllers\Admin\MensajeController;
|
||||
use App\Http\Controllers\Admin\ProductoController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rutas del Panel de Administración
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Estas rutas se acceden desde /admin y tienen el prefijo 'admin.' en los nombres
|
||||
|
|
||||
*/
|
||||
|
||||
// Rutas de autenticación (públicas)
|
||||
Route::middleware('guest:admin')->group(function () {
|
||||
Route::get('/login', [AuthController::class, 'showLogin'])->name('admin.login');
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
});
|
||||
|
||||
// Rutas protegidas
|
||||
Route::middleware(['admin.auth', 'security.headers'])->group(function () {
|
||||
// Dashboard
|
||||
Route::get('/', [DashboardController::class, 'index'])->name('admin.dashboard');
|
||||
|
||||
// Cerrar sesión
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('admin.logout');
|
||||
|
||||
// Galería
|
||||
Route::prefix('galeria')->name('admin.galeria.')->group(function () {
|
||||
Route::get('/', [GaleriaController::class, 'index'])->name('index');
|
||||
Route::get('/create', [GaleriaController::class, 'create'])->name('create');
|
||||
Route::post('/', [GaleriaController::class, 'store'])->name('store');
|
||||
Route::get('/{galeria}/edit', [GaleriaController::class, 'edit'])->name('edit');
|
||||
Route::put('/{galeria}', [GaleriaController::class, 'update'])->name('update');
|
||||
Route::delete('/{galeria}', [GaleriaController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Productos
|
||||
Route::prefix('productos')->name('admin.productos.')->group(function () {
|
||||
Route::get('/', [ProductoController::class, 'index'])->name('index');
|
||||
Route::get('/create', [ProductoController::class, 'create'])->name('create');
|
||||
Route::post('/', [ProductoController::class, 'store'])->name('store');
|
||||
Route::get('/{producto}/edit', [ProductoController::class, 'edit'])->name('edit');
|
||||
Route::put('/{producto}', [ProductoController::class, 'update'])->name('update');
|
||||
Route::delete('/{producto}', [ProductoController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Mensajes
|
||||
Route::prefix('mensajes')->name('admin.mensajes.')->group(function () {
|
||||
Route::get('/', [MensajeController::class, 'index'])->name('index');
|
||||
Route::get('/{mensaje}', [MensajeController::class, 'show'])->name('show');
|
||||
Route::patch('/{mensaje}/leido', [MensajeController::class, 'markRead'])->name('leido');
|
||||
Route::post('/leer-todos', [MensajeController::class, 'markAllRead'])->name('leer-todos');
|
||||
Route::delete('/{mensaje}', [MensajeController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Usuarios Admin (solo super_admin)
|
||||
Route::prefix('usuarios')->name('admin.users.')->middleware('super_admin')->group(function () {
|
||||
Route::get('/', [AdminUserController::class, 'index'])->name('index');
|
||||
Route::get('/create', [AdminUserController::class, 'create'])->name('create');
|
||||
Route::post('/', [AdminUserController::class, 'store'])->name('store');
|
||||
Route::get('/{admin_user}/edit', [AdminUserController::class, 'edit'])->name('edit');
|
||||
Route::put('/{admin_user}', [AdminUserController::class, 'update'])->name('update');
|
||||
Route::delete('/{admin_user}', [AdminUserController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Configuración
|
||||
Route::prefix('configuracion')->name('admin.configuracion.')->group(function () {
|
||||
Route::get('/', [ConfiguracionController::class, 'index'])->name('index');
|
||||
Route::put('/', [ConfiguracionController::class, 'update'])->name('update');
|
||||
});
|
||||
});
|
||||
14
routes/api.php
Executable file
14
routes/api.php
Executable file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Aquí puedes agregar rutas de API si es necesario
|
||||
// Route::middleware('api')->group(function () {
|
||||
// // Rutas de API
|
||||
// });
|
||||
8
routes/console.php
Executable file
8
routes/console.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
47
routes/web.php
Executable file
47
routes/web.php
Executable file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Frontend\GaleriaController as FrontendGaleriaController;
|
||||
use App\Http\Controllers\Frontend\HomeController;
|
||||
use App\Http\Controllers\Frontend\ProductoController as FrontendProductoController;
|
||||
use App\Http\Requests\MensajeRequest;
|
||||
use App\Models\Mensaje;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rutas Web - Frontend y Admin
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// RUTAS DEL FRONTEND (PÚBLICAS)
|
||||
// ============================================
|
||||
|
||||
Route::middleware('security.headers')->group(function () {
|
||||
// Página principal
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
|
||||
// Galería
|
||||
Route::get('/galeria', [FrontendGaleriaController::class, 'index'])->name('galeria');
|
||||
|
||||
// Productos/Servicios
|
||||
Route::get('/productos', [FrontendProductoController::class, 'index'])->name('productos');
|
||||
|
||||
// Rutas de contacto
|
||||
Route::get('/contacto', [HomeController::class, 'contacto'])->name('contacto');
|
||||
|
||||
Route::post('/contacto', function (MensajeRequest $request) {
|
||||
Mensaje::create($request->validated());
|
||||
|
||||
return redirect()->route('contacto')->with('success', 'Tu mensaje ha sido enviado correctamente. Nos pondremos en contacto contigo pronto.');
|
||||
})->name('contacto.send');
|
||||
|
||||
// ============================================
|
||||
// RUTAS DEL PANEL DE ADMINISTRACIÓN
|
||||
// ============================================
|
||||
|
||||
// Incluir rutas de admin con prefijo /admin
|
||||
Route::prefix('admin')->group(function () {
|
||||
require __DIR__.'/admin.php';
|
||||
});
|
||||
});
|
||||
4
storage/app/.gitignore
vendored
Executable file
4
storage/app/.gitignore
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
2
storage/app/private/.gitignore
vendored
Executable file
2
storage/app/private/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/app/public/.gitignore
vendored
Executable file
2
storage/app/public/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user