Initial commit

This commit is contained in:
Rutra
2026-02-25 00:34:39 +01:00
commit 54b0fc3485
178 changed files with 12761 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">Sauvegarde et Restauration</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Export Section -->
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Exporter les données</h2>
<p class="text-gray-600 mb-4">
Téléchargez une sauvegarde complète de la base de données au format JSON.
<br>
<span class="text-sm text-yellow-600">Note : Les fichiers médias (images, musiques) ne sont pas inclus dans ce fichier mais leurs références le sont.</span>
</p>
<a href="/admin/backup/export" class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 inline-flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>
Télécharger la sauvegarde
</a>
</div>
<!-- Import Section -->
<div class="bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Importer une sauvegarde</h2>
<p class="text-gray-600 mb-4">
Restaurez la base de données à partir d'un fichier JSON.
<br>
<span class="text-sm text-red-600 font-bold">ATTENTION : Cette action supprimera toutes les données actuelles !</span>
</p>
<form action="/admin/backup/import" method="POST" enctype="multipart/form-data" onsubmit="return confirm('Êtes-vous sûr de vouloir écraser toutes les données actuelles ? Cette action est irréversible.');">
{{{ csrfField() }}}
<div class="mb-4">
<input type="file" name="backup_file" accept=".json" required class="block w-full text-sm text-gray-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100
"/>
</div>
<button type="submit" class="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 inline-flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>
Importer et Restaurer
</button>
</form>
</div>
</div>
@endslot
@end

View File

@@ -0,0 +1,18 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">{{ category ? 'Modifier la catégorie' : 'Nouvelle catégorie' }}</h1>
<form action="{{ category ? `/admin/categories/${category.id}` : '/admin/categories' }}" method="post" class="bg-white p-6 rounded shadow max-w-xl">
{{{ csrfField() }}}
<div class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Nom</label>
<input type="text" name="name" id="name" value="{{ category?.name || '' }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Enregistrer
</button>
</div>
</form>
@endslot
@end

View File

@@ -0,0 +1,40 @@
@component('admin/layouts/main')
@slot('main')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Catégories</h1>
<a href="/admin/categories/create" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Nouvelle catégorie
</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Nom</th>
<th class="px-4 py-2 text-right text-sm font-medium text-gray-700">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@each(c in categories)
<tr>
<td class="px-4 py-2">{{ c.name }}</td>
<td class="px-4 py-2 text-right">
<a href="/admin/categories/{{ c.id }}/edit" class="text-blue-600 hover:underline mr-2">Modifier</a>
<form action="/admin/categories/{{ c.id }}/delete" method="post" class="inline">
{{{ csrfField() }}}
<button type="submit" class="text-red-600 hover:underline" onclick="return confirm('Supprimer ?')">
Supprimer
</button>
</form>
</td>
</tr>
@else
<tr>
<td colspan="2" class="px-4 py-6 text-center text-gray-500">Aucune catégorie</td>
</tr>
@endeach
</tbody>
</table>
</div>
@endslot
@end

View File

@@ -0,0 +1,35 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">Dashboard</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<a href="/admin/information/edit" class="bg-white p-6 rounded shadow hover:shadow-lg">
<h2 class="font-semibold text-lg">Informations</h2>
<p class="text-gray-600">{{ hasInformation ? 'Configuré' : 'Non configuré' }}</p>
</a>
<a href="/admin/projects" class="bg-white p-6 rounded shadow hover:shadow-lg">
<h2 class="font-semibold text-lg">Projets</h2>
<p class="text-gray-600">{{ projectsCount }} projet(s)</p>
</a>
<a href="/admin/music" class="bg-white p-6 rounded shadow hover:shadow-lg">
<h2 class="font-semibold text-lg">Musiques</h2>
<p class="text-gray-600">{{ musicsCount }} piste(s)</p>
</a>
<a href="/admin/categories" class="bg-white p-6 rounded shadow hover:shadow-lg">
<h2 class="font-semibold text-lg">Catégories</h2>
<p class="text-gray-600">{{ categoriesCount }} catégorie(s)</p>
</a>
<a href="/admin/tags" class="bg-white p-6 rounded shadow hover:shadow-lg">
<h2 class="font-semibold text-lg">Tags</h2>
<p class="text-gray-600">{{ tagsCount }} tag(s)</p>
</a>
<a href="/admin/experiences" class="bg-white p-6 rounded shadow hover:shadow-lg">
<h2 class="font-semibold text-lg">Expériences</h2>
<p class="text-gray-600">{{ experiencesCount }} expérience(s)</p>
</a>
<a href="/admin/trainings" class="bg-white p-6 rounded shadow hover:shadow-lg">
<h2 class="font-semibold text-lg">Formations</h2>
<p class="text-gray-600">{{ trainingsCount }} formation(s)</p>
</a>
</div>
@endslot
@end

View File

@@ -0,0 +1,40 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">{{ experience ? 'Modifier l\'expérience' : 'Nouvelle expérience' }}</h1>
<form action="{{ experience ? `/admin/experiences/${experience.id}` : '/admin/experiences' }}" method="post" class="bg-white p-6 rounded shadow max-w-2xl">
{{{ csrfField() }}}
<div class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Nom / Titre</label>
<input type="text" name="name" id="name" value="{{ experience?.name || '' }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="place" class="block text-sm font-medium text-gray-700 mb-1">Lieu / Entreprise</label>
<input type="text" name="place" id="place" value="{{ experience?.place || '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="start" class="block text-sm font-medium text-gray-700 mb-1">Date de début</label>
<input type="date" name="start" id="start" value="{{ experience?.start ? experience.start.toISODate() : '' }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="end" class="block text-sm font-medium text-gray-700 mb-1">Date de fin</label>
<input type="date" name="end" id="end" value="{{ experience?.end ? experience.end.toISODate() : '' }}"
class="w-full px-3 py-2 border rounded" />
<p class="text-xs text-gray-500 mt-1">Laisser vide pour "En cours"</p>
</div>
</div>
<div>
<label for="missions" class="block text-sm font-medium text-gray-700 mb-1">Missions / Description</label>
<textarea name="missions" id="missions" rows="5" class="w-full px-3 py-2 border rounded">{{ experience?.missions || '' }}</textarea>
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Enregistrer
</button>
</div>
</form>
@endslot
@end

View File

@@ -0,0 +1,47 @@
@component('admin/layouts/main')
@slot('main')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Expériences Professionnelles</h1>
<a href="/admin/experiences/create" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Nouvelle expérience
</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<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 tracking-wider">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lieu</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Période</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@each(experience in experiences)
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ experience.name }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">{{ experience.place || '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">
{{ experience.start.toFormat('MM/yyyy') }} - {{ experience.end ? experience.end.toFormat('MM/yyyy') : 'En cours' }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="/admin/experiences/{{ experience.id }}/edit" class="text-indigo-600 hover:text-indigo-900 mr-4">Modifier</a>
<form action="/admin/experiences/{{ experience.id }}/delete" method="POST" class="inline" onsubmit="return confirm('Êtes-vous sûr ?')">
{{{ csrfField() }}}
<button type="submit" class="text-red-600 hover:text-red-900">Supprimer</button>
</form>
</td>
</tr>
@endeach
</tbody>
</table>
</div>
@endslot
@end

View File

@@ -0,0 +1,76 @@
@component('admin/layouts/main')
@slot('main')
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">Gestionnaire d'Images</h1>
<!-- Upload Form -->
<div class="bg-white p-6 rounded-lg shadow-md mb-8">
<h2 class="text-lg font-semibold mb-4">Téléverser une nouvelle image</h2>
<form action="/admin/images" method="POST" enctype="multipart/form-data" class="flex items-end gap-4">
{{ csrfField() }}
<div class="flex-1">
<label for="image" class="block text-sm font-medium text-gray-700 mb-1">Fichier Image</label>
<input
type="file"
name="image"
id="image"
accept="image/*"
required
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
>
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Téléverser
</button>
</form>
@if(flashMessages.has('error'))
<div class="mt-4 p-3 bg-red-100 text-red-700 rounded">
{{ flashMessages.get('error') }}
</div>
@endif
@if(flashMessages.has('success'))
<div class="mt-4 p-3 bg-green-100 text-green-700 rounded">
{{ flashMessages.get('success') }}
</div>
@endif
</div>
<!-- Image Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6">
@each(image in images)
<div class="bg-white rounded-lg shadow-sm overflow-hidden border border-gray-200 group relative">
<div class="aspect-square bg-gray-100 relative">
<img
src="{{ image.filePath }}"
alt="{{ image.originalName }}"
class="w-full h-full object-cover"
>
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<button
onclick="navigator.clipboard.writeText('{{ image.filePath }}')"
class="bg-white text-gray-800 text-xs px-2 py-1 rounded hover:bg-gray-100"
title="Copier le lien"
>
Copier Lien
</button>
<form action="/admin/images/{{ image.id }}?_method=DELETE" method="POST" onsubmit="return confirm('Supprimer cette image ?')">
{{ csrfField() }}
<button type="submit" class="bg-red-500 text-white text-xs px-2 py-1 rounded hover:bg-red-600">
Supprimer
</button>
</form>
</div>
</div>
<div class="p-3 text-xs text-gray-500 truncate" title="{{ image.originalName }}">
{{ image.originalName }}
</div>
</div>
@else
<div class="col-span-full text-center text-gray-500 py-12">
Aucune image trouvée.
</div>
@endeach
</div>
</div>
@endslot
@end

View File

@@ -0,0 +1,44 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">Modifier les informations</h1>
<form action="/admin/information" method="post" class="bg-white p-6 rounded shadow max-w-xl">
{{{ csrfField() }}}
<div class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Nom</label>
<input type="text" name="name" id="name" value="{{ information.name }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="headline" class="block text-sm font-medium text-gray-700 mb-1">Titre</label>
<input type="text" name="headline" id="headline" value="{{ information.headline }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="contact" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input type="email" name="contact" id="contact" value="{{ information.contact }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="linkedin" class="block text-sm font-medium text-gray-700 mb-1">LinkedIn</label>
<input type="url" name="linkedin" id="linkedin" value="{{ information.linkedin || '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="github" class="block text-sm font-medium text-gray-700 mb-1">GitHub</label>
<input type="url" name="github" id="github" value="{{ information.github || '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="birthday" class="block text-sm font-medium text-gray-700 mb-1">Date de naissance</label>
<input type="date" name="birthday" id="birthday"
value="{{ information.birthday ? information.birthday.toISODate() : '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Enregistrer
</button>
</div>
</form>
@endslot
@end

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Admin - Portfolio</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"></script>
</head>
<body class="min-h-screen bg-gray-100">
<nav class="bg-gray-800 text-white px-4 py-2">
<div class="container mx-auto flex items-center justify-between">
<a href="/admin" class="font-bold">Admin Portfolio</a>
<div class="flex gap-4 flex-wrap">
<a href="/admin" class="hover:underline">Dashboard</a>
<a href="/admin/information/edit" class="hover:underline">Informations</a>
<a href="/admin/projects" class="hover:underline">Projets</a>
<a href="/admin/images" class="hover:underline">Images</a>
<a href="/admin/music" class="hover:underline">Musiques</a>
<a href="/admin/categories" class="hover:underline">Catégories</a>
<a href="/admin/tags" class="hover:underline">Tags</a>
<a href="/admin/experiences" class="hover:underline">Expériences</a>
<a href="/admin/trainings" class="hover:underline">Formations</a>
<a href="/admin/backup" class="hover:underline">Sauvegarde</a>
<form action="/admin/logout" method="post" class="inline">
{{{ csrfField() }}}
<button type="submit" class="hover:underline">Déconnexion</button>
</form>
</div>
</div>
</nav>
<main class="container mx-auto px-4 py-6">
{{{ await $slots.main() }}}
</main>
</body>
</html>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Connexion Admin - Portfolio</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gray-100 flex items-center justify-center">
<div class="bg-white p-8 rounded shadow-md w-96">
<h1 class="text-xl font-bold mb-6">Connexion Admin</h1>
@error('error')
<div class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
{{ $message }}
</div>
@enderror
<form action="/admin/login" method="post">
{{{ csrfField() }}}
<div class="mb-4">
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Identifiant</label>
<input
type="text"
name="email"
id="email"
value="{{ old('input.email') || '' }}"
required
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
/>
</div>
<div class="mb-6">
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Mot de passe</label>
<input
type="password"
name="password"
id="password"
required
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"
>
Se connecter
</button>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,29 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">{{ music ? 'Modifier la musique' : 'Nouvelle musique' }}</h1>
<form action="{{ music ? `/admin/music/${music.id}` : '/admin/music' }}?_method={{ music ? 'PUT' : 'POST' }}" method="post" enctype="multipart/form-data" class="bg-white p-6 rounded shadow max-w-xl">
{{{ csrfField() }}}
<div class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Nom</label>
<input type="text" name="name" id="name" value="{{ music?.name || '' }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="file" class="block text-sm font-medium text-gray-700 mb-1">Fichier Audio (MP3)</label>
@if(music && music.url)
<div class="mb-2 text-sm text-gray-600">
Fichier actuel: <a href="{{ music.url }}" target="_blank" class="text-blue-600 hover:underline">{{ music.url }}</a>
<audio controls src="{{ music.url }}" class="mt-2 w-full"></audio>
</div>
@endif
<input type="file" name="file" id="file" accept=".mp3,.wav,.ogg" {{ music ? '' : 'required' }}
class="w-full px-3 py-2 border rounded" />
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Enregistrer
</button>
</div>
</form>
@endslot
@end

View File

@@ -0,0 +1,42 @@
@component('admin/layouts/main')
@slot('main')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Musiques</h1>
<a href="/admin/music/create" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Nouvelle piste
</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Nom</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">URL</th>
<th class="px-4 py-2 text-right text-sm font-medium text-gray-700">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@each(m in musics)
<tr>
<td class="px-4 py-2">{{ m.name }}</td>
<td class="px-4 py-2">{{ m.url }}</td>
<td class="px-4 py-2 text-right">
<a href="/admin/music/{{ m.id }}/edit" class="text-blue-600 hover:underline mr-2">Modifier</a>
<form action="/admin/music/{{ m.id }}/delete" method="post" class="inline">
{{{ csrfField() }}}
<button type="submit" class="text-red-600 hover:underline" onclick="return confirm('Supprimer ?')">
Supprimer
</button>
</form>
</td>
</tr>
@else
<tr>
<td colspan="3" class="px-4 py-6 text-center text-gray-500">Aucune musique</td>
</tr>
@endeach
</tbody>
</table>
</div>
@endslot
@end

View File

@@ -0,0 +1,120 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">{{ project ? 'Modifier le projet' : 'Nouveau projet' }}</h1>
<form action="{{ project ? `/admin/projects/${project.id}` : '/admin/projects' }}" method="post" class="bg-white p-6 rounded shadow max-w-2xl">
{{{ csrfField() }}}
<div class="space-y-4">
<div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Titre</label>
<input type="text" name="title" id="title" value="{{ project?.title || '' }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-700 mb-1">Contenu (Markdown)</label>
<textarea name="content" id="content" rows="8" class="w-full px-3 py-2 border rounded">{{ project?.content || '' }}</textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="start" class="block text-sm font-medium text-gray-700 mb-1">Début</label>
<input type="date" name="start" id="start" value="{{ project?.start ? project.start.toISODate() : '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="end" class="block text-sm font-medium text-gray-700 mb-1">Fin</label>
<input type="date" name="end" id="end" value="{{ project?.end ? project.end.toISODate() : '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
</div>
<div>
<label for="thumbnailUrl" class="block text-sm font-medium text-gray-700 mb-1">Image miniature</label>
<div x-data="{ open: false, selected: '{{ project?.thumbnailUrl || '' }}' }" class="relative">
<input type="hidden" name="thumbnailUrl" x-model="selected">
<button type="button" @click="open = !open" class="w-full px-3 py-2 border rounded text-left flex items-center justify-between bg-white">
<span x-text="selected ? selected.split('/').pop() : 'Sélectionner une image'"></span>
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</button>
<div x-show="open" @click.away="open = false" class="absolute z-10 w-full mt-1 bg-white border rounded shadow-lg max-h-60 overflow-y-auto grid grid-cols-3 gap-2 p-2">
<div @click="selected = ''; open = false" class="col-span-3 p-2 hover:bg-gray-100 cursor-pointer text-sm text-gray-500 text-center border-b mb-2">
Aucune image
</div>
@each(image in images)
<div @click="selected = '{{ image.filePath }}'; open = false" class="cursor-pointer group relative aspect-square border rounded overflow-hidden hover:border-blue-500" :class="{'ring-2 ring-blue-500': selected === '{{ image.filePath }}'}">
<img src="{{ image.filePath }}" alt="{{ image.originalName }}" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center text-white text-xs p-1 text-center transition-opacity">
{{ image.originalName }}
</div>
</div>
@endeach
</div>
<!-- Preview of selected image -->
<template x-if="selected">
<div class="mt-2">
<p class="text-xs text-gray-500 mb-1">Aperçu:</p>
<img :src="selected" class="h-24 w-auto object-contain border rounded bg-gray-50">
</div>
</template>
</div>
</div>
<div>
<label for="categoryId" class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
<select name="categoryId" id="categoryId" class="w-full px-3 py-2 border rounded">
<option value="">—</option>
@each(cat in categories)
<option value="{{ cat.id }}" {{ project?.categoryId === cat.id ? 'selected' : '' }}>{{ cat.name }}</option>
@endeach
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Tags Selection -->
<div class="bg-gray-50 p-4 rounded border" x-data="{ search: '' }">
<label class="block text-sm font-bold text-gray-700 mb-2 border-b pb-1">Tags</label>
<input type="text" x-model="search" placeholder="Rechercher..." class="w-full px-2 py-1 mb-2 border rounded text-sm mb-2">
<div class="max-h-60 overflow-y-auto space-y-2 pr-2">
@each(tag in tags)
<label class="flex items-center hover:bg-gray-100 p-1 rounded cursor-pointer transition-colors" x-show="'{{ tag.name.toLowerCase() }}'.includes(search.toLowerCase())">
<input type="checkbox" name="tagIds" value="{{ tag.id }}" class="mr-2 h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
{{ project?.tags?.find(t => t.id === tag.id) ? 'checked' : '' }} />
<span class="text-sm text-gray-700" style="color: {{ tag.color }}">{{ tag.name }}</span>
</label>
@endeach
</div>
</div>
<!-- Trainings Selection -->
<div class="bg-gray-50 p-4 rounded border">
<label class="block text-sm font-bold text-gray-700 mb-2 border-b pb-1">Formations</label>
<div class="max-h-60 overflow-y-auto space-y-2 pr-2">
@each(t in trainings)
<label class="flex items-center hover:bg-gray-100 p-1 rounded cursor-pointer transition-colors">
<input type="checkbox" name="trainingIds" value="{{ t.id }}" class="mr-2 h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
{{ project?.trainings?.find(tr => tr.id === t.id) ? 'checked' : '' }} />
<span class="text-sm text-gray-700">{{ t.name }}</span>
</label>
@endeach
</div>
</div>
<!-- Experiences Selection -->
<div class="bg-gray-50 p-4 rounded border">
<label class="block text-sm font-bold text-gray-700 mb-2 border-b pb-1">Expériences</label>
<div class="max-h-60 overflow-y-auto space-y-2 pr-2">
@each(e in experiences)
<label class="flex items-center hover:bg-gray-100 p-1 rounded cursor-pointer transition-colors">
<input type="checkbox" name="experienceIds" value="{{ e.id }}" class="mr-2 h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
{{ project?.experiences?.find(exp => exp.id === e.id) ? 'checked' : '' }} />
<span class="text-sm text-gray-700">{{ e.name }} - {{e.place }}</span>
</label>
@endeach
</div>
</div>
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Enregistrer
</button>
</div>
</form>
@endslot
@end

View File

@@ -0,0 +1,44 @@
@component('admin/layouts/main')
@slot('main')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Projets</h1>
<a href="/admin/projects/create" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Nouveau projet
</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Titre</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Catégorie</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Début</th>
<th class="px-4 py-2 text-right text-sm font-medium text-gray-700">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@each(p in projects)
<tr>
<td class="px-4 py-2">{{ p.title }}</td>
<td class="px-4 py-2">{{ p.category?.name || '-' }}</td>
<td class="px-4 py-2">{{ p.start ? p.start.toISODate() : '-' }}</td>
<td class="px-4 py-2 text-right">
<a href="/admin/projects/{{ p.id }}/edit" class="text-blue-600 hover:underline mr-2">Modifier</a>
<form action="/admin/projects/{{ p.id }}/delete" method="post" class="inline">
{{{ csrfField() }}}
<button type="submit" class="text-red-600 hover:underline" onclick="return confirm('Supprimer ?')">
Supprimer
</button>
</form>
</td>
</tr>
@else
<tr>
<td colspan="4" class="px-4 py-6 text-center text-gray-500">Aucun projet</td>
</tr>
@endeach
</tbody>
</table>
</div>
@endslot
@end

View File

@@ -0,0 +1,34 @@
@component('admin/layouts/main')
@slot('main')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{{ tag ? 'Modifier le tag' : 'Nouveau tag' }}</h1>
</div>
<form action="{{ tag ? `/admin/tags/${tag.id}` : '/admin/tags' }}" method="POST" class="bg-white rounded shadow p-6 max-w-lg">
{{{ csrfField() }}}
<div class="mb-4">
<label for="name" class="block text-gray-700 text-sm font-bold mb-2">Nom</label>
<input type="text" name="name" id="name" value="{{ tag?.name || '' }}" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required>
</div>
<div class="mb-4">
<label for="color" class="block text-gray-700 text-sm font-bold mb-2">Couleur (Hex/Nom)</label>
<div class="flex gap-2">
<input type="text" name="color" id="color" value="{{ tag?.color || defaultColor || '#000000' }}" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="#000000">
<input type="color" onchange="document.getElementById('color').value = this.value" value="{{ tag?.color || defaultColor || '#000000' }}" class="h-10 w-10 border rounded cursor-pointer">
</div>
<p class="text-xs text-gray-500 mt-1">Optionnel. Utilisé pour l'affichage.</p>
</div>
<div class="flex items-center justify-between">
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
Enregistrer
</button>
<a href="/admin/tags" class="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800">
Annuler
</a>
</div>
</form>
@endslot
@end

View File

@@ -0,0 +1,42 @@
@component('admin/layouts/main')
@slot('main')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Tags</h1>
<a href="/admin/tags/create" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Nouveau tag
</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Nom</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Couleur</th>
<th class="px-4 py-2 text-right text-sm font-medium text-gray-700">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@each(t in tags)
<tr>
<td class="px-4 py-2">{{ t.name }}</td>
<td class="px-4 py-2">{{ t.color || '-' }}</td>
<td class="px-4 py-2 text-right">
<a href="/admin/tags/{{ t.id }}/edit" class="text-blue-600 hover:underline mr-2">Modifier</a>
<form action="/admin/tags/{{ t.id }}/delete" method="post" class="inline">
{{{ csrfField() }}}
<button type="submit" class="text-red-600 hover:underline" onclick="return confirm('Supprimer ?')">
Supprimer
</button>
</form>
</td>
</tr>
@else
<tr>
<td colspan="3" class="px-4 py-6 text-center text-gray-500">Aucun tag</td>
</tr>
@endeach
</tbody>
</table>
</div>
@endslot
@end

View File

@@ -0,0 +1,39 @@
@component('admin/layouts/main')
@slot('main')
<h1 class="text-2xl font-bold mb-6">{{ training ? 'Modifier la formation' : 'Nouvelle formation' }}</h1>
<form action="{{ training ? `/admin/trainings/${training.id}` : '/admin/trainings' }}" method="post" class="bg-white p-6 rounded shadow max-w-xl">
{{{ csrfField() }}}
<div class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Nom</label>
<input type="text" name="name" id="name" value="{{ training?.name || '' }}" required
class="w-full px-3 py-2 border rounded" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="start" class="block text-sm font-medium text-gray-700 mb-1">Début</label>
<input type="date" name="start" id="start" value="{{ training?.start ? training.start.toISODate() : '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
<div>
<label for="end" class="block text-sm font-medium text-gray-700 mb-1">Fin</label>
<input type="date" name="end" id="end" value="{{ training?.end ? training.end.toISODate() : '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
</div>
<div>
<label for="skill" class="block text-sm font-medium text-gray-700 mb-1">Compétence</label>
<textarea name="skill" id="skill" class="w-full px-3 py-2 border rounded">{{ training?.skill || '' }}</textarea>
</div>
<div>
<label for="place" class="block text-sm font-medium text-gray-700 mb-1">Lieu</label>
<input type="text" name="place" id="place" value="{{ training?.place || '' }}"
class="w-full px-3 py-2 border rounded" />
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Enregistrer
</button>
</div>
</form>
@endslot
@end

View File

@@ -0,0 +1,46 @@
@component('admin/layouts/main')
@slot('main')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Formations</h1>
<a href="/admin/trainings/create" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Nouvelle formation
</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Nom</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Début</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Fin</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-700">Compétence</th>
<th class="px-4 py-2 text-right text-sm font-medium text-gray-700">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@each(t in trainings)
<tr>
<td class="px-4 py-2">{{ t.name }}</td>
<td class="px-4 py-2">{{ t.start ? t.start.toISODate() : '-' }}</td>
<td class="px-4 py-2">{{ t.end ? t.end.toISODate() : '-' }}</td>
<td class="px-4 py-2">{{ t.skill || '-' }}</td>
<td class="px-4 py-2 text-right">
<a href="/admin/trainings/{{ t.id }}/edit" class="text-blue-600 hover:underline mr-2">Modifier</a>
<form action="/admin/trainings/{{ t.id }}/delete" method="post" class="inline">
{{{ csrfField() }}}
<button type="submit" class="text-red-600 hover:underline" onclick="return confirm('Supprimer ?')">
Supprimer
</button>
</form>
</td>
</tr>
@else
<tr>
<td colspan="5" class="px-4 py-6 text-center text-gray-500">Aucune formation</td>
</tr>
@endeach
</tbody>
</table>
</div>
@endslot
@end

View File

@@ -0,0 +1,8 @@
<h1>
404 - Page not found
</h1>
<p>
This template is rendered by the
<a href="http://docs.adonisjs.com/guides/exception-handling#status-pages">status pages feature</a>
of the global exception handler.
</p>

View File

@@ -0,0 +1,8 @@
<h1>
{{ error.code }} - Server error
</h1>
<p>
This template is rendered by the
<a href="http://docs.adonisjs.com/guides/exception-handling#status-pages">status pages feature</a>
of the global exception handler.
</p>

View File

@@ -0,0 +1,408 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
AdonisJS - A fully featured web framework for Node.js
</title>
<link rel="preconnect" href="https://fonts.bunny.net" />
<link
href="https://fonts.bunny.net/css?family=instrument-sans:400,400i,500,500i,600,600i,700,700i"
rel="stylesheet"
/>
<style>
:root {
--sand-1: #fdfdfc;
--sand-2: #f9f9f8;
--sand-3: #f1f0ef;
--sand-4: #e9e8e6;
--sand-5: #e2e1de;
--sand-6: #dad9d6;
--sand-7: #cfceca;
--sand-8: #bcbbb5;
--sand-9: #8d8d86;
--sand-10: #82827c;
--sand-11: #63635e;
--sand-12: #21201c;
}
</style>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: [ "Instrument Sans", "sans-serif" ]
},
colors: {
primary: {
DEFAULT: "#5A45FF",
lighter: "#a599ff"
},
sand: {
1: "var(--sand-1)",
2: "var(--sand-2)",
3: "var(--sand-3)",
4: "var(--sand-4)",
5: "var(--sand-5)",
6: "var(--sand-6)",
7: "var(--sand-7)",
8: "var(--sand-8)",
9: "var(--sand-9)",
10: "var(--sand-10)",
11: "var(--sand-11)",
12: "var(--sand-12)"
}
}
}
}
};
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('dumper')
</head>
<body class="min-h-screen w-screen font-sans">
<div
class="fixed xl:absolute left-8 right-8 top-0 bottom-0 xl:inset-0 max-w-screen-xl mx-auto before:content-[''] before:[background:repeating-linear-gradient(0deg,var(--sand-5)_0_4px,transparent_0_8px)] before:absolute before:top-0 before:left-0 before:h-full before:w-px after:content-[''] after:[background:repeating-linear-gradient(0deg,var(--sand-5)_0_4px,transparent_0_8px)] after:absolute after:top-0 after:right-0 after:h-full after:w-px"
>
</div>
<div class="pt-4 h-full flex flex-col">
{{-- Header --}}
<div class="grow pb-4 bg-gradient-to-b from-sand-1 to-sand-2 flex justify-center items-center">
<a href="https://adonisjs.com" target="_blank" class="isolate">
<svg class="w-16 h-16 fill-primary" viewBox="0 0 33 33">
<path
fill-rule="evenodd"
d="M0 16.333c0 13.173 3.16 16.333 16.333 16.333 13.173 0 16.333-3.16 16.333-16.333C32.666 3.16 29.506 0 16.333 0 3.16 0 0 3.16 0 16.333Zm6.586 3.393L11.71 8.083c.865-1.962 2.528-3.027 4.624-3.027 2.096 0 3.759 1.065 4.624 3.027l5.123 11.643c.233.566.432 1.297.432 1.93 0 2.893-2.029 4.923-4.923 4.923-.986 0-1.769-.252-2.561-.506-.812-.261-1.634-.526-2.695-.526-1.048 0-1.89.267-2.718.529-.801.253-1.59.503-2.538.503-2.894 0-4.923-2.03-4.923-4.924 0-.632.2-1.363.432-1.929Zm9.747-9.613-5.056 11.443c1.497-.699 3.227-1.032 5.056-1.032 1.763 0 3.56.333 4.99 1.032l-4.99-11.444Z"
clip-rule="evenodd"
/>
</svg>
</a>
</div>
{{-- Bento with documentation, Adocasts, packages and Discord --}}
<div
class="isolate mt-10 max-w-screen-xl mx-auto px-16 xl:px-8 grid grid-cols-1 xl:grid-cols-2 xl:grid-rows-3 gap-8"
>
<article
class="row-span-3 relative p-6 shadow-sm hover:shadow border border-sand-7 hover:border-sand-8 rounded-2xl transition ease-in-out duration-700 group flex flex-col gap-8"
>
<div class="relative opacity-80">
<svg fill="none" viewBox="0 0 240 105">
<path fill="#F9F9F8" d="M0 4a4 4 0 0 1 4-4h232a4 4 0 0 1 4 4v101H0V4Z" />
<g fill="#000" fill-rule="evenodd" clip-path="url(#a)" clip-rule="evenodd">
<path
d="M24 11.444c0 4.391 1.053 5.445 5.444 5.445s5.445-1.054 5.445-5.445c0-4.39-1.054-5.444-5.445-5.444C25.054 6 24 7.053 24 11.444Zm2.195 1.131 1.708-3.88c.288-.655.843-1.01 1.541-1.01.699 0 1.253.355 1.542 1.01l1.707 3.88c.078.189.144.433.144.644 0 .964-.676 1.64-1.64 1.64-.33 0-.59-.083-.854-.168-.271-.087-.545-.175-.899-.175-.35 0-.63.089-.906.176-.267.085-.53.168-.846.168-.964 0-1.64-.677-1.64-1.641 0-.211.066-.455.143-.644Zm3.25-3.204-1.686 3.814c.499-.233 1.075-.344 1.685-.344.588 0 1.187.111 1.664.344l-1.664-3.814Zm26.473-.678c-.378 0-.65.268-.65.64 0 .374.272.641.65.641s.651-.267.651-.64-.273-.64-.65-.64Zm-11.907 5.502c-1.009 0-1.738-.745-1.738-1.91 0-1.187.73-1.933 1.737-1.933.468 0 .814.158 1.019.468V8.86h1.05v5.25h-1.05v-.372c-.2.304-.546.456-1.019.456Zm-.667-1.91c0-.652.352-1.077.887-1.077.54 0 .887.42.887 1.071 0 .64-.346 1.056-.887 1.056-.535 0-.887-.415-.887-1.05Zm4.384-.011c0-.646.351-1.06.877-1.06.53 0 .882.414.882 1.06 0 .646-.352 1.06-.883 1.06-.525 0-.876-.414-.876-1.06Zm11.571.835c0 .194-.147.31-.52.31-.42 0-.682-.221-.682-.489h-1.05c.026.725.714 1.265 1.711 1.265.946 0 1.55-.42 1.55-1.165 0-.557-.358-.945-1.066-1.087l-.762-.152c-.23-.047-.367-.163-.367-.315 0-.226.23-.347.525-.347.42 0 .583.195.583.426h.997c-.026-.683-.562-1.203-1.56-1.203-.929 0-1.559.468-1.559 1.176 0 .64.415.93 1.035 1.06l.756.164c.247.052.41.157.41.357Zm-2.85 1.002h-1.05v-3.675h1.05v3.675Zm-4.264-3.675v.384c.268-.31.625-.468 1.066-.468.824 0 1.36.536 1.36 1.365v2.394h-1.05v-2.173c0-.446-.252-.714-.688-.714-.436 0-.688.268-.688.714v2.173h-1.05v-3.675h1.05Zm-3.58-.084c-1.119 0-1.948.809-1.948 1.922s.83 1.921 1.948 1.921c1.123 0 1.953-.808 1.953-1.921s-.83-1.922-1.953-1.922Zm-8.758.856c-.535 0-.887.425-.887 1.076 0 .636.352 1.05.887 1.05.54 0 .887-.414.887-1.055 0-.65-.346-1.07-.887-1.07Zm-1.958 1.076c0 1.166.73 1.911 1.732 1.911.478 0 .82-.152 1.024-.456v.372h1.05v-3.675h-1.05v.384c-.21-.31-.556-.468-1.024-.468-1.003 0-1.732.746-1.732 1.932Z"
/>
</g>
<rect width="8" height="3" x="162" y="9.944" fill="#DAD9D6" rx="1" />
<rect width="14" height="3" x="174" y="9.944" fill="#DAD9D6" rx="1" />
<rect width="10" height="3" x="192" y="9.944" fill="#DAD9D6" rx="1" />
<rect width="10" height="3" x="206" y="9.944" fill="#DAD9D6" rx="1" />
<rect width="81" height="6" x="24" y="32" fill="#DAD9D6" rx="2" />
<rect width="95" height="6" x="24" y="44" fill="#DAD9D6" rx="2" />
<rect width="16" height="5" x="24" y="60" fill="#21201C" rx="1" />
<path fill="#DAD9D6" d="M24 85a4 4 0 0 1 4-4h184a4 4 0 0 1 4 4v20H24V85Z" />
<path fill="url(#b)" fill-opacity=".2" d="M24 85a4 4 0 0 1 4-4h184a4 4 0 0 1 4 4v20H24V85Z" />
<defs>
<linearGradient id="b" x1="120" x2="120" y1="81" y2="105" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0" />
<stop offset="1" stop-color="#82827C" />
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M24 6h36.307v10.889H24z" />
</clipPath>
</defs>
</svg>
<div class="absolute left-0 right-0 bottom-0 h-16 bg-gradient-to-b from-white/0 to-white">
</div>
</div>
<div class="flex flex-row gap-4">
<div class="shrink-0 w-10 h-10 bg-primary/20 rounded-md flex justify-center items-center">
<svg class="h-6 w-6 fill-primary" viewBox="0 0 256 256">
<path
fill="currentColor"
d="M208 24H72a32 32 0 0 0-32 32v168a8 8 0 0 0 8 8h144a8 8 0 0 0 0-16H56a16 16 0 0 1 16-16h136a8 8 0 0 0 8-8V32a8 8 0 0 0-8-8m-88 16h48v72l-19.21-14.4a8 8 0 0 0-9.6 0L120 112Zm80 144H72a31.8 31.8 0 0 0-16 4.29V56a16 16 0 0 1 16-16h32v88a8 8 0 0 0 12.8 6.4L144 114l27.21 20.4A8 8 0 0 0 176 136a8 8 0 0 0 8-8V40h16Z"
/>
</svg>
</div>
<div class="space-y-1">
<h2 class="text-lg font-semibold">
<a href="https://docs.adonisjs.com" target="_blank">
<span>Documentation</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-700">
Dive into the official documentation to learn AdonisJS. Read carefully to discover an unmatched set of features, best practices and developer experience. Through examples, guides and API references, you'll find everything you need to build your next project. From installation to deployment, we've got you covered.
</p>
</div>
</div>
</article>
<article
class="relative p-6 shadow-sm hover:shadow border border-sand-7 hover:border-sand-8 rounded-2xl transition ease-in-out duration-700 group flex flex-row gap-4"
>
<div class="shrink-0 w-10 h-10 bg-primary/20 rounded-md flex justify-center items-center">
<svg class="h-6 w-6 fill-primary" viewBox="0 0 256 256">
<path
fill="currentColor"
d="m164.44 105.34-48-32A8 8 0 0 0 104 80v64a8 8 0 0 0 12.44 6.66l48-32a8 8 0 0 0 0-13.32M120 129.05V95l25.58 17ZM216 40H40a16 16 0 0 0-16 16v112a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 128H40V56h176zm16 40a8 8 0 0 1-8 8H32a8 8 0 0 1 0-16h192a8 8 0 0 1 8 8"
/>
</svg>
</div>
<div class="space-y-1">
<h2 class="text-lg font-semibold">
<a href="https://adocasts.com" target="_blank">
<span>Adocasts</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-700">
Level up your development and Adonis skills with hours of video content, from beginner to advanced, through databases, testing, and more.
</p>
</div>
</article>
<article
class="relative p-6 shadow-sm hover:shadow border border-sand-7 hover:border-sand-8 rounded-2xl transition ease-in-out duration-700 group flex flex-row gap-4"
>
<div class="shrink-0 w-10 h-10 bg-primary/20 rounded-md flex justify-center items-center">
<svg class="h-6 w-6 fill-primary" viewBox="0 0 256 256">
<path
fill="currentColor"
d="M208 96a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16h-32a16 16 0 0 0-16 16v8H96v-8a16 16 0 0 0-16-16H48a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h8v64h-8a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32a16 16 0 0 0 16-16v-8h64v8a16 16 0 0 0 16 16h32a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-8V96Zm-32-48h32v32h-32ZM48 48h32v15.9a.5.5 0 0 0 0 .2V80H48Zm32 160H48v-32h32v15.9a.5.5 0 0 0 0 .2zm128 0h-32v-32h32Zm-24-48h-8a16 16 0 0 0-16 16v8H96v-8a16 16 0 0 0-16-16h-8V96h8a16 16 0 0 0 16-16v-8h64v8a16 16 0 0 0 16 16h8Z"
/>
</svg>
</div>
<div class="space-y-1">
<h2 class="text-lg font-semibold">
<a href="https://packages.adonisjs.com" target="_blank">
<span>Packages</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-700">
Supercharge your AdonisJS application with packages built and maintained by both the core team and the community.
</p>
</div>
</article>
<article
class="relative p-6 shadow-sm hover:shadow border border-sand-7 hover:border-sand-8 rounded-2xl transition ease-in-out duration-700 group flex flex-row gap-4"
>
<div class="shrink-0 w-10 h-10 bg-primary/20 rounded-md flex justify-center items-center">
<svg class="h-6 w-6 fill-primary" viewBox="0 0 256 256">
<path
fill="currentColor"
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m0 192a88 88 0 1 1 88-88 88.1 88.1 0 0 1-88 88m44.42-143.16-64 32a8.05 8.05 0 0 0-3.58 3.58l-32 64A8 8 0 0 0 80 184a8.1 8.1 0 0 0 3.58-.84l64-32a8.05 8.05 0 0 0 3.58-3.58l32-64a8 8 0 0 0-10.74-10.74M138 138l-40.11 20.11L118 118l40.15-20.07Z"
/>
</svg>
</div>
<div class="space-y-1">
<h2 class="text-lg font-semibold">
<a href="https://discord.gg/vDcEjq6" target="_blank">
<span>Discord</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-700">
Never get lost again, ask questions, and share your knowledge or projects with a growing and supportive community. Join us.
</p>
</div>
</article>
</div>
{{-- Features --}}
<div class="grow mt-10 mb-8 px-16 xl:px-8 max-w-screen-xl mx-auto">
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<article
class="relative py-4 px-5 bg-white border border-transparent rounded-lg hover:border-sand-8 hover:shadow-sm transition duration-100 ease-in-out group"
>
<h2 class="font-semibold text-sand-12">
<a href="https://lucid.adonisjs.com" target="_blank" class="flex flex-row gap-2">
<span class="bg-[#D5EAE7] h-6 w-6 flex justify-center items-center rounded">
<svg class="h-4 w-4 fill-[#0E766E]" viewBox="0 0 24 24">
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</g>
</svg>
</span>
<span>Lucid</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="mt-4 text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-100">
A SQL ORM with a powerful query builder, active record, migrations, and model factories. Everything you need to work with databases.
</p>
<svg
class="absolute top-4 right-5 opacity-0 group-hover:opacity-100 text-sand-9 w-4 h-4 transition ease-in-out duration-100"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6m-7 1 9-9m-5 0h5v5"
/>
</svg>
</article>
<article
class="relative py-4 px-5 bg-white border border-transparent rounded-lg hover:border-sand-8 hover:shadow-sm transition duration-100 ease-in-out group"
>
<h2 class="font-semibold text-sand-12">
<a href="https://vinejs.dev/" target="_blank" class="flex flex-row gap-2">
<span class="bg-[#F3DBFC] h-6 w-6 flex justify-center items-center rounded">
<svg class="h-4 w-4 fill-[#CA5AF2]" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3a12 12 0 0 0 8.5 3A12 12 0 0 1 12 21 12 12 0 0 1 3.5 6 12 12 0 0 0 12 3"
/>
</svg>
</span>
<span>Vine</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="mt-4 text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-100">
A simple yet feature rich and type-safe form data validation. It comes with 50+ built-in rules and an expressive API to define custom rules.
</p>
<svg
class="absolute top-4 right-5 opacity-0 group-hover:opacity-100 text-sand-9 w-4 h-4 transition ease-in-out duration-100"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6m-7 1 9-9m-5 0h5v5"
/>
</svg>
</article>
<article
class="relative py-4 px-5 bg-white border border-transparent rounded-lg hover:border-sand-8 hover:shadow-sm transition duration-100 ease-in-out group"
>
<h2 class="font-semibold text-sand-12">
<a href="https://edgejs.dev/" target="_blank" class="flex flex-row gap-2">
<span class="bg-[#B8EAE0] h-6 w-6 flex justify-center items-center rounded">
<svg class="h-4 w-4 fill-[#4BBBA5]" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 4a2 2 0 0 0-2 2v3a2 3 0 0 1-2 3 2 3 0 0 1 2 3v3a2 2 0 0 0 2 2M17 4a2 2 0 0 1 2 2v3a2 3 0 0 0 2 3 2 3 0 0 0-2 3v3a2 2 0 0 1-2 2"
/>
</svg>
</span>
<span>Edge</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="mt-4 text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-100">
Write your views with ease and enjoy the power of a simple, modern and battteries included template engine. You'll love it.
</p>
<svg
class="absolute top-4 right-5 opacity-0 group-hover:opacity-100 text-sand-9 w-4 h-4 transition ease-in-out duration-100"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6m-7 1 9-9m-5 0h5v5"
/>
</svg>
</article>
<article
class="relative py-4 px-5 bg-white border border-transparent rounded-lg hover:border-sand-8 hover:shadow-sm transition duration-100 ease-in-out group"
>
<h2 class="font-semibold text-sand-12">
<a href="https://japa.dev" target="_blank" class="flex flex-row gap-2">
<span class="bg-[#FACDDC] h-6 w-6 flex justify-center items-center rounded">
<svg class="h-4 w-4 fill-[#DD3074]" viewBox="0 0 256 256">
<path
fill="currentColor"
d="m240.49 83.51-60-60a12 12 0 0 0-17 0L34.28 152.75a48.77 48.77 0 0 0 69 69l111.2-111.26 21.31-7.11a12 12 0 0 0 4.7-19.87M86.28 204.75a24.77 24.77 0 0 1-35-35l28.13-28.13c7.73-2.41 19.58-3 35.06 5a84 84 0 0 0 21.95 8ZM204.2 88.62a12.15 12.15 0 0 0-4.69 2.89l-38.89 38.9c-7.73 2.41-19.58 3-35.06-5a84 84 0 0 0-21.94-8L172 49l37.79 37.79Z"
/>
</svg>
</span>
<span>Japa</span>
<span class="absolute inset-0"></span>
</a>
</h2>
<p class="mt-4 text-sm text-sand-11 group-hover:text-sand-12 transition ease-in-out duration-100">
From JSON API tests using Open API schema to browser tests with Playwright, it comes with everything you need to test your application.
</p>
<svg
class="absolute top-4 right-5 opacity-0 group-hover:opacity-100 text-sand-9 w-4 h-4 transition ease-in-out duration-100"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6m-7 1 9-9m-5 0h5v5"
/>
</svg>
</article>
</div>
</div>
<div
class="text-sm text-center [&>code]:font-medium [&>code]:text-[#a599ff] bg-sand-12 text-sand-1 relative py-2"
>
Route for this page is registered in <code>start/routes.ts</code> file, rendering <code>resources/views/pages/home.edge</code> template
</div>
</div>
</body>
</html>