first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
2025-04-23 00:14:33 +06:00
commit 356f56eebd
197 changed files with 21536 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
@props([
'on',
])
<div
x-data="{ shown: false, timeout: null }"
x-init="@this.on('{{ $on }}', () => { clearTimeout(timeout); shown = true; timeout = setTimeout(() => { shown = false }, 2000); })"
x-show.transition.out.opacity.duration.1500ms="shown"
x-transition:leave.opacity.duration.1500ms
style="display: none"
{{ $attributes->merge(['class' => 'text-sm']) }}
>
{{ $slot->isEmpty() ? __('Saved.') : $slot }}
</div>

View File

@@ -0,0 +1,16 @@
<!-- resources/views/components/activity-icon.blade.php -->
@props(['type'])
@php
$icons = [
'upload' => 'document-upload',
'approval' => 'shield-check',
'comment' => 'chat-alt',
'update' => 'pencil-alt',
'delete' => 'trash'
];
$defaultIcon = 'bell';
@endphp
<x-icons :icon="$icons[$type] ?? $defaultIcon" class="w-5 h-5 text-gray-400" />

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {{ $attributes }}>
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 714 B

View File

@@ -0,0 +1,6 @@
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
</div>
<div class="ms-1 grid flex-1 text-start text-sm">
<span class="mb-0.5 truncate leading-none font-semibold">Laravel Starter Kit</span>
</div>

View File

@@ -0,0 +1,9 @@
@props([
'title',
'description',
])
<div class="flex w-full flex-col text-center">
<flux:heading size="xl">{{ $title }}</flux:heading>
<flux:subheading>{{ $description }}</flux:subheading>
</div>

View File

@@ -0,0 +1,9 @@
@props([
'status',
])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
{{ $status }}
</div>
@endif

View File

@@ -0,0 +1,8 @@
<!-- resources/views/components/checkbox.blade.php -->
@props(['disabled' => false])
<input
type="checkbox"
{{ $disabled ? 'disabled' : '' }}
{!! $attributes->merge(['class' => 'rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50'])
!!}>

View File

@@ -0,0 +1,27 @@
<!-- resources/views/components/confirmation-modal.blade.php -->
@props(['show' => false])
<div x-data="{ show: @entangle($attributes->wire('model')) }"
x-show="show"
class="fixed inset-0 z-50 overflow-y-auto">
<!-- Fondo oscuro -->
<div class="fixed inset-0 bg-black opacity-50"></div>
<!-- Contenido del modal -->
<div class="min-h-screen flex items-center justify-center p-4">
<div @click.away="show = false"
class="bg-white rounded-lg overflow-hidden shadow-xl max-w-md w-full">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-medium text-gray-900">{{ $title }}</h3>
</div>
<div class="px-6 py-4">
{{ $content }}
</div>
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-4">
{{ $footer }}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,6 @@
@props(['href' => '#'])
<a {{ $attributes->merge(['class' => 'block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100']) }}
href="{{ $href }}">
{{ $slot }}
</a>

View File

@@ -0,0 +1,22 @@
@props(['folder', 'level' => 0])
<li class="pl-{{ $level * 4 }} group">
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded-lg cursor-pointer"
wire:click="$emit('folderSelected', {{ $folder->id }})">
<div class="flex items-center">
<x-icons icon="folder" class="w-5 h-5 mr-2 text-yellow-500" />
<span class="text-sm">{{ $folder->name }}</span>
</div>
@if($folder->children->isNotEmpty())
<x-icons icon="chevron-right" class="w-4 h-4 text-gray-400 transform group-hover:rotate-90 transition-transform" />
@endif
</div>
@if($folder->children->isNotEmpty())
<ul class="mt-1 space-y-1">
@foreach($folder->children as $child)
<x-folder-item :folder="$child" :level="$level + 1" />
@endforeach
</ul>
@endif
</li>

View File

@@ -0,0 +1,34 @@
@props(['folder', 'selectedFolderId', 'expandedFolders', 'level' => 0])
<li class="pl-{{ $level * 2 }}">
<div class="flex items-center justify-between p-2 rounded hover:bg-gray-100 {{ $folder->id == $selectedFolderId ? 'bg-blue-50 border border-blue-200' : '' }}">
<div class="flex items-center flex-1" wire:click="selectFolder({{ $folder->id }})">
<button wire:click.prevent="toggleFolder({{ $folder->id }})" class="mr-2">
@if(in_array($folder->id, $expandedFolders))
<x-icons.chevron-down class="w-4 h-4 text-gray-400" />
@else
<x-icons.chevron-right class="w-4 h-4 text-gray-400" />
@endif
</button>
<x-icons.folder class="w-5 h-5 mr-2 text-yellow-500" />
<span class="cursor-pointer">{{ $folder->name }}</span>
</div>
<span class="text-sm text-gray-500">
{{ $folder->documents_count }}
</span>
</div>
@if(in_array($folder->id, $expandedFolders))
<ul class="mt-2 space-y-2">
@foreach($folder->children as $child)
<x-folder-item
:folder="$child"
:selectedFolderId="$selectedFolderId"
:expandedFolders="$expandedFolders"
:level="$level + 1"
wire:key="folder-{{ $child->id }}"
/>
@endforeach
</ul>
@endif
</li>

View File

@@ -0,0 +1,94 @@
@props(['icon' => ''])
@if($icon === 'dashboard')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
@elseif($icon === 'folder')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
@elseif($icon === 'document')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
@elseif($icon === 'search')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
@elseif($icon === 'chevron-down')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
@elseif($icon === 'user')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
@elseif($icon === 'logout')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
@elseif($icon === 'eye')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
@elseif($icon === 'download')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
@elseif($icon === 'storage')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
@elseif($icon === 'arrow-up')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
@elseif($icon === 'clock')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@elseif($icon === 'trash')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
@elseif($icon === 'edit')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
@elseif($icon === 'check')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
@elseif($icon === 'x')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
@elseif($icon === 'plus')
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
@else
<!-- Icono por defecto (question mark circle) -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@endif

View File

@@ -0,0 +1,4 @@
<!-- resources/views/components/icons/upload.blade.php -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,5 @@
<x-layouts.app.sidebar :title="$title ?? null">
<flux:main>
{{ $slot }}
</flux:main>
</x-layouts.app.sidebar>

View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<a href="{{ route('dashboard') }}" class="ms-2 me-5 flex items-center space-x-2 rtl:space-x-reverse lg:ms-0" wire:navigate>
<x-app-logo />
</a>
<flux:navbar class="-mb-px max-lg:hidden">
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navbar.item>
</flux:navbar>
<flux:spacer />
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!">
<flux:tooltip :content="__('Search')" position="bottom">
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" />
</flux:tooltip>
<flux:tooltip :content="__('Repository')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="folder-git-2"
href="https://github.com/laravel/livewire-starter-kit"
target="_blank"
:label="__('Repository')"
/>
</flux:tooltip>
<flux:tooltip :content="__('Documentation')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="book-open-text"
href="https://laravel.com/docs/starter-kits"
target="_blank"
label="Documentation"
/>
</flux:tooltip>
</flux:navbar>
<!-- Desktop User Menu -->
<flux:dropdown position="top" align="end">
<flux:profile
class="cursor-pointer"
:initials="auth()->user()->initials()"
/>
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
<!-- Mobile Menu -->
<flux:sidebar stashable sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="ms-1 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
<x-app-logo />
</a>
<flux:navlist variant="outline">
<flux:navlist.group :heading="__('Platform')">
<flux:navlist.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist>
<flux:spacer />
<flux:navlist variant="outline">
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:navlist.item>
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits" target="_blank">
{{ __('Documentation') }}
</flux:navlist.item>
</flux:navlist>
</flux:sidebar>
{{ $slot }}
@fluxScripts
</body>
</html>

View File

@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
<x-app-logo />
</a>
<flux:navlist variant="outline">
<flux:navlist.group :heading="__('Platform')" class="grid">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
</flux:navlist>
<flux:spacer />
<flux:navlist variant="outline">
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:navlist.item>
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits" target="_blank">
{{ __('Documentation') }}
</flux:navlist.item>
</flux:navlist>
<!-- Desktop User Menu -->
<flux:dropdown position="bottom" align="start">
<flux:profile
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
icon-trailing="chevrons-up-down"
/>
<flux:menu class="w-[220px]">
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:sidebar>
<!-- Mobile User Menu -->
<flux:header class="lg:hidden">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<flux:spacer />
<flux:dropdown position="top" align="end">
<flux:profile
:initials="auth()->user()->initials()"
icon-trailing="chevron-down"
/>
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
{{ $slot }}
@fluxScripts
</body>
</html>

View File

@@ -0,0 +1,3 @@
<x-layouts.auth.simple :title="$title ?? null">
{{ $slot }}
</x-layouts.auth.simple>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-md flex-col gap-6">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8">{{ $slot }}</div>
</div>
</div>
</div>
</div>
@fluxScripts
</body>
</html>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-2">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
{{ $slot }}
</div>
</div>
</div>
@fluxScripts
</body>
</html>

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
<div class="absolute inset-0 bg-neutral-900"></div>
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
<span class="flex h-10 w-10 items-center justify-center rounded-md">
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
</span>
{{ config('app.name', 'Laravel') }}
</a>
@php
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
@endphp
<div class="relative z-20 mt-auto">
<blockquote class="space-y-2">
<flux:heading size="lg">&ldquo;{{ trim($message) }}&rdquo;</flux:heading>
<footer><flux:heading>{{ trim($author) }}</flux:heading></footer>
</blockquote>
</div>
</div>
<div class="w-full lg:p-8">
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
{{ $slot }}
</div>
</div>
</div>
@fluxScripts
</body>
</html>

View File

@@ -0,0 +1,40 @@
@props([
'options' => [],
'selected' => [],
'name' => 'multiselect',
'id' => 'multiselect-' . Str::random(5),
'wireModel' => null // Nuevo prop para el modelo Livewire
])
<div
x-data="{
open: false,
selected: @if($wireModel) @entangle($wireModel) @else {{ json_encode($selected) }} @endif
}"
class="relative"
<div
@click="open = !open"
class="cursor-pointer p-2 border rounded-lg flex items-center justify-between"
>
<span x-text="selected.length ? selected.join(', ') : 'Seleccione opciones'"></span>
<x-icons icon="chevron-down" class="w-4 h-4 ml-2" />
</div>
<div
x-show="open"
@click.away="open = false"
class="absolute z-50 w-full mt-1 bg-white border rounded-lg shadow-lg max-h-60 overflow-auto"
>
<template x-for="(option, index) in {{ json_encode($options) }}">
<label class="flex items-center px-4 py-2 hover:bg-gray-100">
<input
type="checkbox"
x-model="selected"
:value="option.value"
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500"
>
<span x-text="option.label" class="ml-2"></span>
</label>
</template>
</div>
</div>

View File

@@ -0,0 +1,9 @@
@props(['href', 'active' => false])
<a href="{{ $href }}" {{ $attributes->class([
'flex items-center px-4 py-2 text-sm transition-colors',
'bg-gray-900 text-white' => $active,
'text-gray-300 hover:bg-gray-700 hover:text-white' => !$active
]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,12 @@
@props([
'id' => uniqid(),
])
<svg {{ $attributes }} fill="none">
<defs>
<pattern id="pattern-{{ $id }}" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
</pattern>
</defs>
<rect stroke="none" fill="url(#pattern-{{ $id }})" width="100%" height="100%"></rect>
</svg>

View File

@@ -0,0 +1,6 @@
<!-- resources/views/components/select.blade.php -->
@props(['disabled' => false])
<select {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50']) !!}>
{{ $slot }}
</select>

View File

@@ -0,0 +1,20 @@
<div class="flex items-start max-md:flex-col">
<div class="me-10 w-full pb-4 md:w-[220px]">
<flux:navlist>
<flux:navlist.item :href="route('settings.profile')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
<flux:navlist.item :href="route('settings.password')" wire:navigate>{{ __('Password') }}</flux:navlist.item>
<flux:navlist.item :href="route('settings.appearance')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
</flux:navlist>
</div>
<flux:separator class="md:hidden" />
<div class="flex-1 self-stretch max-md:pt-6">
<flux:heading>{{ $heading ?? '' }}</flux:heading>
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
<div class="mt-5 w-full max-w-lg">
{{ $slot }}
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
@props([
'title',
'value',
'icon', // ← Añade esta línea
'color',
'trend' => null, // Valor por defecto: null
'trendColor' => 'text-gray-500' // Color por defecto
])
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">{{ $title }}</p>
<p class="text-3xl font-bold">{{ $value }}</p>
</div>
<div class="{{ $color }} p-3 rounded-full">
@if($icon === 'folder')
<x-icons icon="folder" class="w-8 h-8 text-white" />
@elseif($icon === 'document')
<x-icons icon = "document" class="w-8 h-8 text-white" />
@endif
</div>
</div>
<div class="mt-4">
<span class="text-sm {{ $trendColor }}">
<x-icons icon="arrow-up" class="w-4 h-4 inline" /> {{ $trend }}%
</span>
<span class="text-sm text-gray-500">desde el último mes</span>
</div>
</div>

View File

@@ -0,0 +1,14 @@
@props(['status'])
@php
$colors = [
'pending' => 'bg-yellow-100 text-yellow-800',
'approved' => 'bg-green-100 text-green-800',
'rejected' => 'bg-red-100 text-red-800',
'in_review' => 'bg-blue-100 text-blue-800'
];
@endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $colors[$status] }}">
{{ strtoupper($status) }}
</span>

View File

@@ -0,0 +1,4 @@
<!-- resources/views/components/textarea.blade.php -->
@props(['disabled' => false])
<textarea {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50']) !!}>{{ $slot }}</textarea>

View File

@@ -0,0 +1,62 @@
<!-- resources/views/components/toolbar.blade.php -->
<div class="flex items-center justify-between mb-4 space-x-4">
<!-- Grupo izquierdo: Acciones principales -->
<div class="flex items-center space-x-2">
<!-- Crear nueva carpeta -->
<button wire:click="$emit('openCreateFolderModal')"
class="px-3 py-2 text-sm text-white bg-blue-600 rounded-lg hover:bg-blue-700">
<x-icons icon="folder-add" class="w-5 h-5 mr-1 inline" />
Nueva Carpeta
</button>
<!-- Subir archivos -->
<div x-data="{ isUploading: false }"
x-on:livewire-upload-start="isUploading = true"
x-on:livewire-upload-finish="isUploading = false"
x-on:livewire-upload-error="isUploading = false">
<button @click="document.getElementById('file-input').click()"
class="px-3 py-2 text-sm text-white bg-green-600 rounded-lg hover:bg-green-700"
:disabled="isUploading">
<x-icons icon="upload" class="w-5 h-5 mr-1 inline" />
<span x-text="isUploading ? 'Subiendo...' : 'Subir Archivos'"></span>
</button>
<input type="file"
id="file-input"
wire:model="files"
multiple
class="hidden"
@change="$wire.uploadFiles()">
</div>
</div>
<!-- Grupo derecho: Acciones adicionales -->
<div class="flex items-center space-x-2">
<!-- Menú desplegable con más opciones -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open"
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg">
<x-icons icon="dots-vertical" class="w-5 h-5" />
</button>
<div x-show="open"
@click.away="open = false"
class="absolute right-0 z-10 w-48 bg-white rounded-lg shadow-lg">
<div class="p-2 space-y-1">
<button wire:click="$emit('openCreateSubfolderModal')"
class="flex items-center w-full px-2 py-1 text-sm hover:bg-gray-100">
<x-icons icon="folder-plus" class="w-4 h-4 mr-2" />
Nueva Subcarpeta
</button>
<button wire:click="$emit('openMultiUploadModal')"
class="flex items-center w-full px-2 py-1 text-sm hover:bg-gray-100">
<x-icons icon="upload-multiple" class="w-4 h-4 mr-2" />
Subida Masiva
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal para crear carpeta -->
@livewire('folder.create-modal')

View File

@@ -0,0 +1,176 @@
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- Cabecera estándar de Laravel -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-100">
<div x-data="{ sidebarOpen: false }" class="flex h-screen">
<!-- Sidebar -->
<aside class="bg-gray-800 text-white w-64 flex-shrink-0">
<div class="p-4">
<h1 class="text-2xl font-bold">mDMS</h1>
</div>
<nav class="mt-6">
<x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
<x-icons icon="dashboard" class="w-5 h-5 mr-3" />
Dashboard
</x-nav-link>
<x-nav-link href="{{ route('projects.index') }}" :active="request()->routeIs('projects.*')">
<x-icons icon="folder" class="w-5 h-5 mr-3" />
Proyectos
</x-nav-link>
<x-nav-link href="{{ route('documents.index') }}" :active="request()->routeIs('documents.*')">
<x-icons icon="document" class="w-5 h-5 mr-3" />
Documentos
</x-nav-link>
</nav>
</aside>
<!-- Contenido principal -->
<main class="flex-1 overflow-x-hidden overflow-y-auto">
<!-- Header -->
<header class="bg-white shadow">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex-1 max-w-2xl">
<div class="relative">
<input type="search" placeholder="Buscar documentos..."
class="w-full pl-10 pr-4 py-2 rounded-lg border focus:outline-none focus:border-blue-500">
<x-icons icon="search" class="w-5 h-5 absolute left-3 top-2.5 text-gray-400" />
</div>
</div>
<div class="flex items-center">
<x-dropdown align="right">
<x-slot name="trigger">
<button class="flex items-center space-x-2">
<span class="text-gray-700">{{ Auth::user()->name }}</span>
<x-icons icon="chevron-down" class="w-4 h-4" />
</button>
</x-slot>
<x-dropdown-link href="{{ route('profile.show') }}">
<x-icons icon="user" class="w-5 h-5 mr-2" /> Perfil
</x-dropdown-link>
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link href="{{ route('logout') }}"
onclick="event.preventDefault(); this.closest('form').submit();">
<x-icons icon="logout" class="w-5 h-5 mr-2" /> Cerrar sesión
</x-dropdown-link>
</form>
</x-dropdown>
</div>
</div>
</header>
<!-- Contenido del dashboard -->
<div class="p-6">
<!-- Estadísticas rápidas -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<x-stats-card title="Proyectos" value=" {{ $stats['projects_count'] }} " icon="folder" color="bg-blue-500"/>
<x-stats-card title="Documentos" value=" {{ $stats['documents_count'] }} " icon="document" color="bg-green-500" />
<x-stats-card title="Almacenamiento" value=" {{$stats['users_count'] }} " icon="storage" color="bg-purple-500" />
<x-stats-card title="Pendientes" value=" {{ $stats['storage_used'] }} " icon="clock" color="bg-yellow-500" />
</div>
<!-- Documentos recientes -->
<div class="bg-white rounded-lg shadow mb-8">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-semibold">Documentos recientes</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nombre</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tipo</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Estado</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Acciones</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($recentDocuments as $document)
<tr>
<td class="px-6 py-4">{{ $document->name }}</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ $document->type }}
</span>
</td>
<td class="px-6 py-4">
<x-status-badge :status="$document->status" />
</td>
<td class="px-6 py-4">
<x-dropdown>
<x-dropdown-link href="{{ route('documents.show', $document) }}">
<x-icons icon="eye" class="w-4 h-4 mr-2" /> Ver
</x-dropdown-link>
<x-dropdown-link href="#">
<x-icons icon="download" class="w-4 h-4 mr-2" /> Descargar
</x-dropdown-link>
</x-dropdown>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<!-- Actividad reciente -->
<div class="grid lg:grid-cols-2 gap-6">
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-semibold">Actividad reciente</h3>
</div>
<div class="p-6">
<div class="flow-root">
<ul class="-mb-4">
@foreach($recentActivities as $activity)
<li class="mb-4">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<x-activity-icon :type="$activity->type" />
</div>
<div class="flex-1">
<p class="text-sm text-gray-900">{{ $activity->description }}</p>
<p class="text-xs text-gray-500">{{ $activity->created_at->diffForHumans() }}</p>
</div>
</div>
</li>
@endforeach
</ul>
</div>
</div>
</div>
<!-- Uso de almacenamiento -->
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-semibold">Uso de almacenamiento</h3>
</div>
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<div>
<p class="text-2xl font-bold">{{ $stats['storage_used'] }}</p>
<p class="text-sm text-gray-500">de {{ $stats['storage_limit'] }} disponibles</p>
</div>
<x-icons icon="storage" class="w-12 h-12 text-purple-500" />
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
div class="bg-purple-500 rounded-full h-2"
style="width: {{ $stats['storage_percentage'] }}%"></div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
<x-layouts.app :title="__('Dashboard')">
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
</x-layouts.app>

View File

@@ -0,0 +1,47 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="M12 7v14" />
<path d="M16 12h2" />
<path d="M16 8h2" />
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
<path d="M6 12h2" />
<path d="M6 8h2" />
</svg>

View File

@@ -0,0 +1,43 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="m7 15 5 5 5-5" />
<path d="m7 9 5-5 5 5" />
</svg>

View File

@@ -0,0 +1,45 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5" />
<circle cx="13" cy="12" r="2" />
<path d="M18 19c-2.8 0-5-2.2-5-5v8" />
<circle cx="20" cy="19" r="2" />
</svg>

View File

@@ -0,0 +1,45 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')->add(
match ($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
},
);
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<rect width="7" height="7" x="3" y="3" rx="1" />
<rect width="7" height="7" x="14" y="3" rx="1" />
<rect width="7" height="7" x="14" y="14" rx="1" />
<rect width="7" height="7" x="3" y="14" rx="1" />
</svg>

View File

@@ -0,0 +1,51 @@
@props([
'expandable' => false,
'expanded' => true,
'heading' => null,
])
<?php if ($expandable && $heading): ?>
<ui-disclosure
{{ $attributes->class('group/disclosure') }}
@if ($expanded === true) open @endif
data-flux-navlist-group
>
<button
type="button"
class="group/disclosure-button mb-[2px] flex h-10 w-full items-center rounded-lg text-zinc-500 hover:bg-zinc-800/5 hover:text-zinc-800 lg:h-8 dark:text-white/80 dark:hover:bg-white/[7%] dark:hover:text-white"
>
<div class="ps-3 pe-4">
<flux:icon.chevron-down class="hidden size-3! group-data-open/disclosure-button:block" />
<flux:icon.chevron-right class="block size-3! group-data-open/disclosure-button:hidden" />
</div>
<span class="text-sm font-medium leading-none">{{ $heading }}</span>
</button>
<div class="relative hidden space-y-[2px] ps-7 data-open:block" @if ($expanded === true) data-open @endif>
<div class="absolute inset-y-[3px] start-0 ms-4 w-px bg-zinc-200 dark:bg-white/30"></div>
{{ $slot }}
</div>
</ui-disclosure>
<?php elseif ($heading): ?>
<div {{ $attributes->class('block space-y-[2px]') }}>
<div class="px-1 py-2">
<div class="text-xs leading-none text-zinc-400">{{ $heading }}</div>
</div>
<div>
{{ $slot }}
</div>
</div>
<?php else: ?>
<div {{ $attributes->class('block space-y-[2px]') }}>
{{ $slot }}
</div>
<?php endif; ?>

View File

@@ -0,0 +1,3 @@
<div>
{{-- The best athlete wants his opponent at his best. --}}
</div>

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth')] class extends Component {
public string $password = '';
/**
* Confirm the current user's password.
*/
public function confirmPassword(): void
{
$this->validate([
'password' => ['required', 'string'],
]);
if (! Auth::guard('web')->validate([
'email' => Auth::user()->email,
'password' => $this->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
session(['auth.password_confirmed_at' => time()]);
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
}
}; ?>
<div class="flex flex-col gap-6">
<x-auth-header
:title="__('Confirm password')"
:description="__('This is a secure area of the application. Please confirm your password before continuing.')"
/>
<!-- Session Status -->
<x-auth-session-status class="text-center" :status="session('status')" />
<form wire:submit="confirmPassword" class="flex flex-col gap-6">
<!-- Password -->
<flux:input
wire:model="password"
:label="__('Password')"
type="password"
required
autocomplete="new-password"
:placeholder="__('Password')"
/>
<flux:button variant="primary" type="submit" class="w-full">{{ __('Confirm') }}</flux:button>
</form>
</div>

View File

@@ -0,0 +1,49 @@
<?php
use Illuminate\Support\Facades\Password;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth')] class extends Component {
public string $email = '';
/**
* Send a password reset link to the provided email address.
*/
public function sendPasswordResetLink(): void
{
$this->validate([
'email' => ['required', 'string', 'email'],
]);
Password::sendResetLink($this->only('email'));
session()->flash('status', __('A reset link will be sent if the account exists.'));
}
}; ?>
<div class="flex flex-col gap-6">
<x-auth-header :title="__('Forgot password')" :description="__('Enter your email to receive a password reset link')" />
<!-- Session Status -->
<x-auth-session-status class="text-center" :status="session('status')" />
<form wire:submit="sendPasswordResetLink" class="flex flex-col gap-6">
<!-- Email Address -->
<flux:input
wire:model="email"
:label="__('Email Address')"
type="email"
required
autofocus
placeholder="email@example.com"
/>
<flux:button variant="primary" type="submit" class="w-full">{{ __('Email password reset link') }}</flux:button>
</form>
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-400">
{{ __('Or, return to') }}
<flux:link :href="route('login')" wire:navigate>{{ __('log in') }}</flux:link>
</div>
</div>

View File

@@ -0,0 +1,126 @@
<?php
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth')] class extends Component {
#[Validate('required|string|email')]
public string $email = '';
#[Validate('required|string')]
public string $password = '';
public bool $remember = false;
/**
* Handle an incoming authentication request.
*/
public function login(): void
{
$this->validate();
$this->ensureIsNotRateLimited();
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
Session::regenerate();
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
}
/**
* Ensure the authentication request is not rate limited.
*/
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the authentication rate limiting throttle key.
*/
protected function throttleKey(): string
{
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
}
}; ?>
<div class="flex flex-col gap-6">
<x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />
<!-- Session Status -->
<x-auth-session-status class="text-center" :status="session('status')" />
<form wire:submit="login" class="flex flex-col gap-6">
<!-- Email Address -->
<flux:input
wire:model="email"
:label="__('Email address')"
type="email"
required
autofocus
autocomplete="email"
placeholder="email@example.com"
/>
<!-- Password -->
<div class="relative">
<flux:input
wire:model="password"
:label="__('Password')"
type="password"
required
autocomplete="current-password"
:placeholder="__('Password')"
/>
@if (Route::has('password.request'))
<flux:link class="absolute end-0 top-0 text-sm" :href="route('password.request')" wire:navigate>
{{ __('Forgot your password?') }}
</flux:link>
@endif
</div>
<!-- Remember Me -->
<flux:checkbox wire:model="remember" :label="__('Remember me')" />
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Log in') }}</flux:button>
</div>
</form>
@if (Route::has('register'))
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-600 dark:text-zinc-400">
{{ __('Don\'t have an account?') }}
<flux:link :href="route('register')" wire:navigate>{{ __('Sign up') }}</flux:link>
</div>
@endif
</div>

View File

@@ -0,0 +1,97 @@
<?php
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth')] class extends Component {
public string $name = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
/**
* Handle an incoming registration request.
*/
public function register(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
]);
$validated['password'] = Hash::make($validated['password']);
event(new Registered(($user = User::create($validated))));
Auth::login($user);
$this->redirectIntended(route('dashboard', absolute: false), navigate: true);
}
}; ?>
<div class="flex flex-col gap-6">
<x-auth-header :title="__('Create an account')" :description="__('Enter your details below to create your account')" />
<!-- Session Status -->
<x-auth-session-status class="text-center" :status="session('status')" />
<form wire:submit="register" class="flex flex-col gap-6">
<!-- Name -->
<flux:input
wire:model="name"
:label="__('Name')"
type="text"
required
autofocus
autocomplete="name"
:placeholder="__('Full name')"
/>
<!-- Email Address -->
<flux:input
wire:model="email"
:label="__('Email address')"
type="email"
required
autocomplete="email"
placeholder="email@example.com"
/>
<!-- Password -->
<flux:input
wire:model="password"
:label="__('Password')"
type="password"
required
autocomplete="new-password"
:placeholder="__('Password')"
/>
<!-- Confirm Password -->
<flux:input
wire:model="password_confirmation"
:label="__('Confirm password')"
type="password"
required
autocomplete="new-password"
:placeholder="__('Confirm password')"
/>
<div class="flex items-center justify-end">
<flux:button type="submit" variant="primary" class="w-full">
{{ __('Create account') }}
</flux:button>
</div>
</form>
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-600 dark:text-zinc-400">
{{ __('Already have an account?') }}
<flux:link :href="route('login')" wire:navigate>{{ __('Log in') }}</flux:link>
</div>
</div>

View File

@@ -0,0 +1,113 @@
<?php
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth')] class extends Component {
#[Locked]
public string $token = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
/**
* Mount the component.
*/
public function mount(string $token): void
{
$this->token = $token;
$this->email = request()->string('email');
}
/**
* Reset the password for the given user.
*/
public function resetPassword(): void
{
$this->validate([
'token' => ['required'],
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$this->only('email', 'password', 'password_confirmation', 'token'),
function ($user) {
$user->forceFill([
'password' => Hash::make($this->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status != Password::PasswordReset) {
$this->addError('email', __($status));
return;
}
Session::flash('status', __($status));
$this->redirectRoute('login', navigate: true);
}
}; ?>
<div class="flex flex-col gap-6">
<x-auth-header :title="__('Reset password')" :description="__('Please enter your new password below')" />
<!-- Session Status -->
<x-auth-session-status class="text-center" :status="session('status')" />
<form wire:submit="resetPassword" class="flex flex-col gap-6">
<!-- Email Address -->
<flux:input
wire:model="email"
:label="__('Email')"
type="email"
required
autocomplete="email"
/>
<!-- Password -->
<flux:input
wire:model="password"
:label="__('Password')"
type="password"
required
autocomplete="new-password"
:placeholder="__('Password')"
/>
<!-- Confirm Password -->
<flux:input
wire:model="password_confirmation"
:label="__('Confirm password')"
type="password"
required
autocomplete="new-password"
:placeholder="__('Confirm password')"
/>
<div class="flex items-center justify-end">
<flux:button type="submit" variant="primary" class="w-full">
{{ __('Reset password') }}
</flux:button>
</div>
</form>
</div>

View File

@@ -0,0 +1,57 @@
<?php
use App\Livewire\Actions\Logout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth')] class extends Component {
/**
* Send an email verification notification to the user.
*/
public function sendVerification(): void
{
if (Auth::user()->hasVerifiedEmail()) {
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
return;
}
Auth::user()->sendEmailVerificationNotification();
Session::flash('status', 'verification-link-sent');
}
/**
* Log the current user out of the application.
*/
public function logout(Logout $logout): void
{
$logout();
$this->redirect('/', navigate: true);
}
}; ?>
<div class="mt-4 flex flex-col gap-6">
<flux:text class="text-center">
{{ __('Please verify your email address by clicking on the link we just emailed to you.') }}
</flux:text>
@if (session('status') == 'verification-link-sent')
<flux:text class="text-center font-medium !dark:text-green-400 !text-green-600">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</flux:text>
@endif
<div class="flex flex-col items-center justify-between space-y-3">
<flux:button wire:click="sendVerification" variant="primary" class="w-full">
{{ __('Resend verification email') }}
</flux:button>
<flux:link class="text-sm cursor-pointer" wire:click="logout">
{{ __('Log out') }}
</flux:link>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div>
{{-- Success is as dangerous as failure. --}}
</div>

View File

@@ -0,0 +1,3 @@
<div>
{{-- Success is as dangerous as failure. --}}
</div>

View File

@@ -0,0 +1,26 @@
<!-- resources/views/livewire/folder/create-modal.blade.php -->
<div x-show="$wire.showModal"
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div class="w-full max-w-md p-6 bg-white rounded-lg">
<h3 class="mb-4 text-lg font-semibold">
{{ $parentFolder ? 'Nueva Subcarpeta en '.$parentFolder->name : 'Nueva Carpeta' }}
</h3>
<input type="text"
wire:model="folderName"
placeholder="Nombre de la carpeta"
class="w-full p-2 border rounded-lg"
@keyup.enter="createFolder">
<div class="flex justify-end mt-4 space-x-2">
<button @click="$wire.showModal = false"
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
Cancelar
</button>
<button wire:click="createFolder"
class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
Crear
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,84 @@
<div>
<!-- Header del Proyecto -->
<div class="flex items-center justify-between pb-6 border-b">
<div>
<h1 class="text-2xl font-bold">{{ $project->name }}</h1>
<div class="flex items-center mt-2 space-x-4">
<span class="px-2 py-1 text-sm rounded-full bg-blue-100 text-blue-800">
{{ ucfirst($project->status) }}
</span>
<span class="text-sm text-gray-500">
{{ $project->created_at->format('d/m/Y') }}
</span>
</div>
</div>
<a href="{{ route('projects.edit', $project) }}"
class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
Editar Proyecto
</a>
</div>
<!-- Layout Principal -->
<div class="grid grid-cols-1 gap-6 mt-6 lg:grid-cols-4">
<!-- Treeview de Carpetas -->
<div class="col-span-1 bg-white rounded-lg shadow">
<div class="p-4 border-b">
<h3 class="font-medium">Estructura de Carpetas</h3>
</div>
<div class="p-4">
<ul class="space-y-2">
@foreach($rootFolders as $folder)
<x-folder-item
:folder="$folder"
:selectedFolderId="$selectedFolderId"
:expandedFolders="$expandedFolders"
wire:key="folder-{{ $folder->id }}"
/>
@endforeach
</ul>
</div>
</div>
<!-- Listado de Documentos -->
<div class="col-span-3">
<div class="bg-white rounded-lg shadow">
<div class="p-4 border-b">
<h3 class="font-medium">Documentos</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Nombre</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Versiones</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Tamaño</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Estado</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($this->documents as $document)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<x-icons icon="document" class="w-5 h-5 mr-2 text-gray-400" />
{{ $document->name }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ $document->versions->count() }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ $document->human_readable_size }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<x-status-badge :status="$document->status" />
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
//
}; ?>
<section class="w-full">
@include('partials.settings-heading')
<x-settings.layout :heading="__('Appearance')" :subheading=" __('Update the appearance settings for your account')">
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance">
<flux:radio value="light" icon="sun">{{ __('Light') }}</flux:radio>
<flux:radio value="dark" icon="moon">{{ __('Dark') }}</flux:radio>
<flux:radio value="system" icon="computer-desktop">{{ __('System') }}</flux:radio>
</flux:radio.group>
</x-settings.layout>
</section>

View File

@@ -0,0 +1,58 @@
<?php
use App\Livewire\Actions\Logout;
use Illuminate\Support\Facades\Auth;
use Livewire\Volt\Component;
new class extends Component {
public string $password = '';
/**
* Delete the currently authenticated user.
*/
public function deleteUser(Logout $logout): void
{
$this->validate([
'password' => ['required', 'string', 'current_password'],
]);
tap(Auth::user(), $logout(...))->delete();
$this->redirect('/', navigate: true);
}
}; ?>
<section class="mt-10 space-y-6">
<div class="relative mb-5">
<flux:heading>{{ __('Delete account') }}</flux:heading>
<flux:subheading>{{ __('Delete your account and all of its resources') }}</flux:subheading>
</div>
<flux:modal.trigger name="confirm-user-deletion">
<flux:button variant="danger" x-data="" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">
{{ __('Delete account') }}
</flux:button>
</flux:modal.trigger>
<flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
<form wire:submit="deleteUser" class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Are you sure you want to delete your account?') }}</flux:heading>
<flux:subheading>
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</flux:subheading>
</div>
<flux:input wire:model="password" :label="__('Password')" type="password" />
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Cancel') }}</flux:button>
</flux:modal.close>
<flux:button variant="danger" type="submit">{{ __('Delete account') }}</flux:button>
</div>
</form>
</flux:modal>
</section>

View File

@@ -0,0 +1,78 @@
<?php
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Livewire\Volt\Component;
new class extends Component {
public string $current_password = '';
public string $password = '';
public string $password_confirmation = '';
/**
* Update the password for the currently authenticated user.
*/
public function updatePassword(): void
{
try {
$validated = $this->validate([
'current_password' => ['required', 'string', 'current_password'],
'password' => ['required', 'string', Password::defaults(), 'confirmed'],
]);
} catch (ValidationException $e) {
$this->reset('current_password', 'password', 'password_confirmation');
throw $e;
}
Auth::user()->update([
'password' => Hash::make($validated['password']),
]);
$this->reset('current_password', 'password', 'password_confirmation');
$this->dispatch('password-updated');
}
}; ?>
<section class="w-full">
@include('partials.settings-heading')
<x-settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
<form wire:submit="updatePassword" class="mt-6 space-y-6">
<flux:input
wire:model="current_password"
:label="__('Current password')"
type="password"
required
autocomplete="current-password"
/>
<flux:input
wire:model="password"
:label="__('New password')"
type="password"
required
autocomplete="new-password"
/>
<flux:input
wire:model="password_confirmation"
:label="__('Confirm Password')"
type="password"
required
autocomplete="new-password"
/>
<div class="flex items-center gap-4">
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
</div>
<x-action-message class="me-3" on="password-updated">
{{ __('Saved.') }}
</x-action-message>
</div>
</form>
</x-settings.layout>
</section>

View File

@@ -0,0 +1,114 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\Rule;
use Livewire\Volt\Component;
new class extends Component {
public string $name = '';
public string $email = '';
/**
* Mount the component.
*/
public function mount(): void
{
$this->name = Auth::user()->name;
$this->email = Auth::user()->email;
}
/**
* Update the profile information for the currently authenticated user.
*/
public function updateProfileInformation(): void
{
$user = Auth::user();
$validated = $this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($user->id)
],
]);
$user->fill($validated);
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
$user->save();
$this->dispatch('profile-updated', name: $user->name);
}
/**
* Send an email verification notification to the current user.
*/
public function resendVerificationNotification(): void
{
$user = Auth::user();
if ($user->hasVerifiedEmail()) {
$this->redirectIntended(default: route('dashboard', absolute: false));
return;
}
$user->sendEmailVerificationNotification();
Session::flash('status', 'verification-link-sent');
}
}; ?>
<section class="w-full">
@include('partials.settings-heading')
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
<div>
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail())
<div>
<flux:text class="mt-4">
{{ __('Your email address is unverified.') }}
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
{{ __('Click here to re-send the verification email.') }}
</flux:link>
</flux:text>
@if (session('status') === 'verification-link-sent')
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
{{ __('A new verification link has been sent to your email address.') }}
</flux:text>
@endif
</div>
@endif
</div>
<div class="flex items-center gap-4">
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
</div>
<x-action-message class="me-3" on="profile-updated">
{{ __('Saved.') }}
</x-action-message>
</div>
</form>
<livewire:settings.delete-user-form />
</x-settings.layout>
</section>

View File

@@ -0,0 +1,10 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ $title ?? config('app.name') }}</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
@fluxAppearance

View File

@@ -0,0 +1,5 @@
<div class="relative mb-6 w-full">
<flux:heading size="xl" level="1">{{ __('Settings') }}</flux:heading>
<flux:subheading size="lg" class="mb-6">{{ __('Manage your profile and account settings') }}</flux:subheading>
<flux:separator variant="subtle" />
</div>

View File

@@ -0,0 +1,157 @@
<!-- resources/views/permissions/assign.blade.php -->
x-layouts.app :title="__('Permissions Management')">
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Gestión de Permisos y Roles') }}
</h2>
</x-slot>
<div class="py-6">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<!-- Buscador -->
<div class="mb-6">
<input type="text" id="search" placeholder="Buscar usuario..."
class="w-full px-4 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Lista de Usuarios -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Usuario</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Roles</th>
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Permisos Directos</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200" id="users-table">
@foreach($users as $user)
<tr class="user-row">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ $user->name }}</div>
<div class="text-sm text-gray-500">{{ $user->email }}</div>
</div>
</div>
</td>
<!-- Asignación de Roles -->
<td class="px-6 py-4 whitespace-nowrap">
<form class="update-roles" data-user-id="{{ $user->id }}">
@csrf
<select name="roles[]" multiple
class="w-48 px-3 py-2 border rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500"
x-data="{}" x-init="tomSelect($el, {
plugins: ['remove_button'],
maxItems: 3
})">
@foreach($roles as $role)
<option value="{{ $role->name }}" {{ $user->hasRole($role) ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
</form>
</td>
<!-- Asignación de Permisos -->
<td class="px-6 py-4">
<form class="update-permissions" data-user-id="{{ $user->id }}">
@csrf
<div class="grid grid-cols-2 gap-4 md:grid-cols-3">
@foreach($permissions->groupBy('category') as $category => $perms)
<div class="p-4 border rounded-lg">
<h3 class="mb-2 text-sm font-semibold">{{ $category }}</h3>
@foreach($perms as $permission)
<label class="flex items-center space-x-2">
<input type="checkbox"
name="permissions[]"
value="{{ $permission->name }}"
{{ $user->hasDirectPermission($permission) ? 'checked' : '' }}
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500">
<span class="text-sm">{{ $permission->name }}</span>
</label>
@endforeach
</div>
@endforeach
</div>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.0.0/dist/css/tom-select.css" rel="stylesheet">
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.0.0/dist/js/tom-select.complete.min.js"></script>
<script>
// Búsqueda en tiempo real
document.getElementById('search').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
document.querySelectorAll('.user-row').forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Actualización automática de roles
document.querySelectorAll('select[name="roles[]"]').forEach(select => {
new TomSelect(select, {
plugins: ['remove_button'],
onChange: function(value) {
const form = select.closest('form');
submitForm(form, 'roles');
}
});
});
// Actualización automática de permisos
document.querySelectorAll('input[name="permissions[]"]').forEach(checkbox => {
checkbox.addEventListener('change', function() {
const form = this.closest('form');
submitForm(form, 'permissions');
});
});
// Función para enviar el formulario
function submitForm(form, type) {
const userId = form.dataset.userId;
const formData = new FormData(form);
fetch(`/admin/users/${userId}/${type}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: formData
})
.then(response => response.json())
.then(data => {
if(data.success) {
showNotification('Cambios guardados exitosamente', 'success');
} else {
showNotification('Error al guardar los cambios', 'error');
}
});
}
function showNotification(message, type) {
// Implementar notificación (ej: Toast)
console.log(`${type}: ${message}`);
}
</script>
@endpush
</x-layouts.app>

View File

@@ -0,0 +1,89 @@
<x-app-layout>
<div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-6">Editar Perfil</h2>
@if (session('status'))
<div class="mb-4 text-green-600">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('profile.update') }}">
@csrf
@method('patch')
<!-- Nombre -->
<div class="mb-4">
<x-input-label for="name" value="Nombre" />
<x-text-input
id="name"
name="name"
type="text"
class="mt-1 block w-full"
:value="old('name', $user->name)"
required
autofocus
/>
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email -->
<div class="mb-4">
<x-input-label for="email" value="Email" />
<x-text-input
id="email"
name="email"
type="email"
class="mt-1 block w-full"
:value="old('email', $user->email)"
required
/>
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Contraseña Actual -->
<div class="mb-4">
<x-input-label for="current_password" value="Contraseña Actual" />
<x-text-input
id="current_password"
name="current_password"
type="password"
class="mt-1 block w-full"
/>
<x-input-error :messages="$errors->get('current_password')" class="mt-2" />
</div>
<!-- Nueva Contraseña -->
<div class="mb-4">
<x-input-label for="password" value="Nueva Contraseña" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-full"
/>
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirmar Contraseña -->
<div class="mb-6">
<x-input-label for="password_confirmation" value="Confirmar Contraseña" />
<x-text-input
id="password_confirmation"
name="password_confirmation"
type="password"
class="mt-1 block w-full"
/>
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex justify-end">
<x-primary-button>
Guardar Cambios
</x-primary-button>
</div>
</form>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,386 @@
<!-- resources/views/projects/create.blade.php -->
<x-layouts.app :title="__('Create Project')">
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
Nuevo Proyecto
</h2>
</x-slot>
<div class="py-6">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ route('projects.store') }}" enctype="multipart/form-data">
@csrf
<!-- Sección de Información Básica -->
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Columna Izquierda -->
<div>
<!-- Nombre -->
<div class="mb-6">
<x-label for="name" value="__('Nombre del Proyecto')" />
<x-input id="name" class="block w-full mt-1"
type="text" name="name" value="old('name')" required />
@error('name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- NombDescripción -->
<div class="mb-6">
<x-label for="description" value="__('Descripción')" />
<x-textarea id="description" class="block w-full mt-1"
name="description" rows="4" required>
{{ old('description') }}
</x-textarea>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Editor Enriquecido -->
<div class="mb-6">
<x-label for="description" value="__('Descripción Detallada')" />
<div id="editor-container">
<!-- Barra de herramientas -->
<div id="toolbar">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
<button class="ql-blockquote"></button>
<button class="ql-code-block"></button>
<button class="ql-link"></button>
<button class="ql-image"></button>
</div>
<!-- Editor -->
<div id="rich-editor">{!! old('description') !!}</div>
</div>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Estado -->
<div class="mb-6">
<x-label for="status" value="__('Estado del Proyecto')" />
<x-select id="status" name="status" class="block w-full mt-1">
<option value="active" {{ old('status') == 'active' ? 'selected' : '' }}>Activo</option>
<option value="inactive" {{ old('status') == 'inactive' ? 'selected' : '' }}>Inactivo</option>
</x-select>
@error('status')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Imagen de Referencia -->
<div class="mb-6">
<x-label for="project_image" value="__('Imagen de Referencia')" />
<div class="relative mt-1">
<input type="file" id="project_image" name="project_image"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100"
accept="image/*"
onchange="previewImage(event)">
<div class="mt-2" id="image-preview-container" style="display:none;">
<img id="image-preview" class="object-cover h-48 w-full rounded-lg">
</div>
</div>
@error('project_image')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
<!-- Columna Derecha -->
<div>
<!-- Dirección -->
<div class="mb-6">
<x-label value="__('Ubicación')" />
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<x-input id="address" class="block w-full mt-1"
type="text" name="address"
value="old('address')"
placeholder="Dirección" />
@error('address')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<x-input id="postal_code" class="block w-full mt-1"
type="text" name="postal_code"
value="old('postal_code')"
placeholder="Código Postal" />
@error('postal_code')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<x-input id="province" class="block w-full mt-1"
type="text" name="province"
value="old('province')"
placeholder="Provincia" />
@error('province')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<x-select id="country" name="country" class="block w-full mt-1">
<option value="">Seleccione País</option>
@foreach(config('countries') as $code => $name)
<option value="{{ $code }}" {{ old('country') == $code ? 'selected' : '' }}>
{{ $name }}
</option>
@endforeach
</x-select>
@error('country')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<!-- Mapa para Coordenadas -->
<div class="mb-6">
<x-label value="__('Seleccione Ubicación en el Mapa')" />
<div id="map" class="h-64 rounded-lg border-2 border-gray-200"></div>
<div class="grid grid-cols-2 gap-4 mt-2">
<div>
<x-label for="latitude" value="__('Latitud')" />
<x-input id="latitude" name="latitude"
type="number" step="any"
value="old('latitude')" required />
</div>
<div>
<x-label for="longitude" value="__('Longitud')" />
<x-input id="longitude" name="longitude"
type="number" step="any"
value="old('longitude')" required />
</div>
</div>
@error('latitude')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@error('longitude')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<!-- Sección Principal en 2 Columnas -->
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Columna Izquierda -->
<div>
<!-- Icono y Categorías -->
<div class="mb-6">
<x-label value="__('Identificación Visual')" />
<div class="grid grid-cols-2 gap-4">
<div>
<x-label for="icon" value="__('Icono del Proyecto')" />
<div class="relative mt-1">
<select id="icon" name="icon"
class="w-full rounded-md shadow-sm border-gray-300 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
<option value="">Seleccionar Icono</option>
@foreach(config('project.icons') as $icon)
<option value="{{ $icon }}"
{{ old('icon') == $icon ? 'selected' : '' }}>
<i class="fas fa-{{ $icon }} mr-2"></i>
{{ Str::title($icon) }}
</option>
@endforeach
</select>
</div>
</div>
<div>
<x-label for="categories" value="__('Categorías')" />
<x-multiselect
name="categories[]"
:options="$categories"
:selected="old('categories', [])"
placeholder="Seleccione categorías"
/>
</div>
</div>
</div>
<!-- Fechas Importantes -->
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<x-label for="start_date" value="__('Fecha de Inicio')" />
<x-input id="start_date" type="date"
name="start_date" value="old('start_date')" />
</div>
<div>
<x-label for="deadline" value="__('Fecha Límite')" />
<x-input id="deadline" type="date"
name="deadline" value="old('deadline')"
min="{{ now()->format('Y-m-d') }}" />
</div>
</div>
</div>
<!-- Columna Derecha -->
<div>
<!-- Archivos Adjuntos -->
<div class="mb-6">
<x-label value="__('Documentos Iniciales')" />
<div x-data="{ files: [] }" class="mt-1">
<div class="flex items-center justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="text-center">
<input type="file"
name="documents[]"
multiple
class="absolute opacity-0"
x-ref="fileInput"
@change="files = Array.from($event.target.files)">
<template x-if="files.length === 0">
<div>
<x-icons.upload class="mx-auto h-12 w-12 text-gray-400" />
<p class="mt-1 text-sm text-gray-600">
Arrastra archivos o haz clic para subir
</p>
</div>
</template>
<template x-if="files.length > 0">
<div class="space-y-2">
<template x-for="(file, index) in files" :key="index">
<div class="flex items-center text-sm text-gray-600">
<x-icons icon="document" class="w-5 h-5 mr-2" />
<span x-text="file.name"></span>
</div>
</template>
</div>
</template>
</div>
</div>
</div>
</div>
<!-- Mapa y Ubicación (Implementación previa) -->
<!-- ... (Mantener sección de mapa y ubicación anterior) ... -->
</div>
</div>
<!-- Resto del formulario -->
<!-- ... (Mantener secciones anteriores de equipo, estado, etc) ... -->
<!-- Miembros del Equipo -->
<div class="mb-6">
Miembros del Equipo
<x-label value="__('Miembros del Equipo')" />
<div class="grid grid-cols-1 mt-2 gap-y-2 gap-x-4 sm:grid-cols-2">
@foreach($users as $user)
<label class="flex items-center space-x-2">
<input type="checkbox"
name="team[]"
value="{{ $user->id }}"
{{ in_array($user->id, old('team', [])) ? 'checked' : '' }}
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ $user->name }}</span>
</label>
@endforeach
</div>
@error('team')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Botones de Acción -->
<div class="flex justify-end mt-8 space-x-4">
<a href="{{ route('projects.index') }}"
class="px-4 py-2 text-gray-600 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50">
{{ __('Cancelar') }}
</a>
<x-button type="submit" class="bg-blue-600 hover:bg-blue-700">
<x-icons icon="save" class="w-5 h-5 mr-2" />
{{ __('Crear Proyecto') }}
</x-button>
</div>
</form>
</div>
</div>
</div>
</div>
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet" />
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify"></script>
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<script>
// Editor Quill
onst toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
['link', 'image', 'video', 'formula'],
[{ 'header': 1 }, { 'header': 2 }], // custom button values
[{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
[{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
[{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
[{ 'direction': 'rtl' }], // text direction
[{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
[{ 'font': [] }],
[{ 'align': [] }],
['clean'] // remove formatting button
];
const quill = new Quill('#rich-editor', {
theme: 'snow',
modules: {
toolbar: toolbarOptions,
placeholder: 'Description...',
}
});
// Tagify para categorías
new Tagify(document.querySelector('[name="categories[]"]'), {
enforceWhitelist: true,
whitelist: @json($categories->pluck('name')),
dropdown: {
enabled: 1,
maxItems: 5
}
});
</script>
<script>
function previewImage(event) {
const reader = new FileReader();
const preview = document.getElementById('image-preview');
reader.onload = function() {
preview.innerHTML = `
<img src="${reader.result}"
class="object-cover w-full h-48 rounded-lg shadow-sm"
alt="Vista previa de la imagen">
`;
}
if(event.target.files[0]) {
reader.readAsDataURL(event.target.files[0]);
}
}
</script>
@endpush
</x-layouts.app>

View File

@@ -0,0 +1,119 @@
<!-- resources/views/projects/index.blade.php -->
<x-layouts.app :title="__('Proyectos')">
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Mis Proyectos') }}
</h2>
</x-slot>
<div class="py-6">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<!-- Barra de búsqueda y creación -->
<div class="flex flex-col justify-between mb-6 space-y-4 md:flex-row md:space-y-0">
<div class="flex-1 max-w-md">
<form action="{{ route('projects.index') }}" method="GET">
<div class="relative">
<input type="search" name="search" value="{{ request('search') }}"
class="w-full pl-10 pr-4 py-2 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Buscar proyectos...">
<x-icons icon="search" class="absolute left-3 top-2.5 w-5 h-5 text-gray-400" />
</div>
</form>
</div>
@can('create', App\Models\Project::class)
<a href="{{ route('projects.create') }}"
class="flex items-center px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
<x-icons icon="plus" class="w-5 h-5 mr-2" />
{{ __('Nuevo Proyecto') }}
</a>
@endcan
</div>
<!-- Listado de proyectos -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
@forelse($projects as $project)
<div class="overflow-hidden bg-white rounded-lg shadow">
<div class="p-6">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900">
<a href="{{ route('projects.show', $project) }}"
class="hover:text-blue-600 hover:underline">
{{ $project->name }}
</a>
</h3>
<p class="mt-2 text-sm text-gray-500">
{{ Str::limit($project->description, 100) }}
</p>
</div>
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="p-1 text-gray-400 hover:text-gray-600">
<x-icons icon="dots-vertical class="w-5 h-5" />
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link href="{{ route('projects.show', $project) }}">
<x-icons icon="eye" class="w-5 h-5 mr-2" /> Ver Detalles
</x-dropdown-link>
@can('update', $project)
<x-dropdown-link href="{{ route('projects.edit', $project) }}">
<x-icons icon="pencil" class="w-5 h-5 mr-2" /> Editar
</x-dropdown-link>
@endcan
@can('delete', $project)
<form method="POST" action="{{ route('projects.destroy', $project) }}">
@csrf
@method('DELETE')
<x-dropdown-link href="#"
onclick="event.preventDefault(); this.closest('form').submit();"
class="text-red-600 hover:bg-red-50">
<x-icons icon="trash" class="w-5 h-5 mr-2" /> Eliminar
</x-dropdown-link>
</form>
@endcan
</x-slot>
</x-dropdown>
</div>
<!-- Metadatos del proyecto -->
<div class="mt-4">
<div class="flex items-center justify-between mt-3 text-sm">
<span class="flex items-center text-gray-500">
<x-icons icon="document" class="w-4 h-4 mr-1" />
{{ $project->documents_count }} documentos
</span>
<span class="px-2 py-1 text-sm rounded-full
{{ $project->status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
{{ __(Str::ucfirst($project->status)) }}
</span>
</div>
<div class="mt-2 text-sm text-gray-500">
<x-icons icon="calendar" class="w-4 h-4 mr-1 inline" />
Creado {{ $project->created_at->diffForHumans() }}
</div>
</div>
</div>
</div>
@empty
<div class="p-6 text-center text-gray-500 col-span-full">
<x-icons icon="folder-remove" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
{{ __('No tienes proyectos asignados.') }}
</div>
@endforelse
</div>
<!-- Paginación -->
@if($projects->hasPages())
<div class="mt-6">
{{ $projects->withQueryString()->links() }}
</div>
@endif
</div>
</div>
</x-layouts.app>

View File

@@ -0,0 +1,48 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
<!-- Barra de herramientas con breadcrumbs -->
<div class="mb-6">
<nav class="flex" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2">
<li>
<div class="flex items-center">
<a href="{{ route('projects.index') }}" class="text-gray-400 hover:text-gray-500">
<x-icons.home class="h-5 w-5" />
</a>
</div>
</li>
@foreach($breadcrumbs as $crumb)
<li>
<div class="flex items-center">
<x-icons.chevron-right class="h-5 w-5 text-gray-400" />
@if(!$loop->last)
<a href="{{ $crumb['url'] }}" class="ml-2 text-sm font-medium text-gray-500 hover:text-gray-700">
{{ $crumb['name'] }}
</a>
@else
<span class="ml-2 text-sm font-medium text-gray-700">{{ $crumb['name'] }}</span>
@endif
</div>
</li>
@endforeach
</ol>
</nav>
</div>
<!-- Barra de herramientas principal -->
<x-toolbar :project="$project" :current-folder="$currentFolder" />
<!-- Componente principal Livewire -->
<livewire:project-show
:project="$project"
:current-folder="$currentFolder ?? null"
wire:key="project-show-{{ $project->id }}-{{ $currentFolder->id ?? 'root' }}"
/>
<!-- Modales -->
@livewire('folder.create-modal', ['project' => $project, 'parentFolder' => $currentFolder ?? null])
@livewire('document.upload-modal', ['project' => $project, 'currentFolder' => $currentFolder ?? null])
</div>
@endsection

File diff suppressed because one or more lines are too long