This commit is contained in:
Rutra
2026-05-04 14:16:59 +02:00
commit 03a14da804
60 changed files with 10549 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title ?? 'Imago Vault' }}</title>
<script src="https://cdn.tailwindcss.com"></script>
@if($slots.head)
{{{ await $slots.head() }}}
@end
</head>
<body class="bg-gray-950 text-gray-100 min-h-screen">
{{-- Flash: success --}}
@if(flashMessages.has('success'))
<div id="flash-success"
class="fixed top-4 right-4 z-50 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg text-sm font-medium pointer-events-none">
{{ flashMessages.get('success') }}
</div>
@end
{{-- Flash: error --}}
@if(flashMessages.has('error'))
<div id="flash-error"
class="fixed top-4 right-4 z-50 bg-red-600 text-white px-6 py-3 rounded-lg shadow-lg text-sm font-medium pointer-events-none">
{{ flashMessages.get('error') }}
</div>
@end
{{{ await $slots.main() }}}
<script>
// Auto-dismiss flash messages after 4 s
setTimeout(() => {
document.querySelectorAll('[id^="flash-"]').forEach(el => {
el.style.transition = 'opacity 0.5s'
el.style.opacity = '0'
setTimeout(() => el.remove(), 500)
})
}, 4000)
</script>
</body>
</html>

View File

@@ -0,0 +1,340 @@
@layouts.main({ title: 'Dashboard — Imago Vault' })
@slot('main')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{{-- ── Header ──────────────────────────────────────────────────────── --}}
<header class="flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold text-white">Media Library</h1>
<p class="text-gray-400 text-sm mt-1">
{{ files.length }} file{{ files.length !== 1 ? 's' : '' }} stored
</p>
</div>
<a href="/logout"
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm rounded-lg
transition border border-gray-700">
Logout
</a>
</header>
{{-- ── Upload zone ─────────────────────────────────────────────────── --}}
<section class="mb-10">
<div id="drop-zone"
class="relative border-2 border-dashed border-gray-700 rounded-2xl p-10
text-center cursor-pointer transition-all duration-200
hover:border-indigo-500 hover:bg-indigo-950/20">
{{-- Default hint (hidden once a file is selected) --}}
<div id="drop-hint" class="flex flex-col items-center gap-3 pointer-events-none">
<svg class="w-12 h-12 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
<p class="text-gray-400 text-sm">
Drag &amp; drop a file here, or
<label for="file-input"
class="text-indigo-400 hover:text-indigo-300 cursor-pointer underline underline-offset-2">
click to browse
</label>
</p>
<p class="text-gray-600 text-xs">
Images (JPG, PNG, GIF, WebP) · Videos (MP4, MOV, AVI, WebM) · Max 100 MB
</p>
</div>
{{-- Preview area (shown after file selection) --}}
<div id="preview-container" class="hidden mt-2">
<img id="image-preview"
class="hidden max-h-64 mx-auto rounded-lg object-contain"
alt="Preview" />
<video id="video-preview"
class="hidden max-h-64 mx-auto rounded-lg"
controls muted playsinline></video>
<p id="preview-filename" class="text-gray-300 text-sm mt-3 font-medium"></p>
<p id="preview-size" class="text-gray-500 text-xs mt-1"></p>
</div>
{{-- Invisible file input covers the whole zone --}}
<input type="file"
id="file-input"
accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/quicktime,video/x-msvideo,video/webm"
class="absolute inset-0 opacity-0 cursor-pointer w-full h-full" />
</div>
{{-- Progress bar (shown during upload) --}}
<div id="progress-wrapper" class="hidden mt-4">
<div class="flex items-center justify-between text-sm mb-2">
<span id="progress-label" class="text-gray-400">Uploading…</span>
<span id="progress-percent" class="text-indigo-400 font-mono font-medium">0%</span>
</div>
<div class="w-full bg-gray-800 rounded-full h-2 overflow-hidden">
<div id="progress-bar"
class="h-full bg-indigo-500 rounded-full transition-all duration-100"
style="width: 0%"></div>
</div>
</div>
{{-- Action buttons (shown after file selection) --}}
<div id="upload-actions" class="hidden flex gap-3 mt-4">
<button id="upload-btn"
class="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold
rounded-lg text-sm transition focus:outline-none focus:ring-2 focus:ring-indigo-400">
Upload File
</button>
<button id="cancel-btn"
class="px-6 py-2.5 bg-gray-800 hover:bg-gray-700 text-gray-300
rounded-lg text-sm transition border border-gray-700">
Cancel
</button>
</div>
{{-- Inline result message --}}
<div id="upload-result" class="hidden mt-4 px-4 py-3 rounded-lg text-sm font-medium"></div>
</section>
{{-- ── Gallery ──────────────────────────────────────────────────────── --}}
<section>
<h2 class="text-lg font-semibold text-gray-200 mb-4">Uploaded Files</h2>
@if(files.length === 0)
<div class="text-center py-20 text-gray-600">
<svg class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14
m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<p>No files uploaded yet. Drop one above to get started.</p>
</div>
@else
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
@each(file in files)
<div class="bg-gray-900 rounded-xl overflow-hidden border border-gray-800
hover:border-gray-700 transition group">
{{-- Media preview --}}
@if(file.type === 'image')
<div class="aspect-video bg-gray-950 overflow-hidden">
<img src="{{ file.url }}"
alt="{{ file.filename }}"
loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition duration-300" />
</div>
@elseif(file.type === 'video')
<div class="aspect-video bg-black">
<video src="{{ file.url }}"
class="w-full h-full object-contain"
controls preload="metadata" muted playsinline></video>
</div>
@else
<div class="aspect-video bg-gray-950 flex items-center justify-center">
<svg class="w-12 h-12 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
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>
</div>
@end
{{-- Info row --}}
<div class="p-3">
<p class="text-gray-300 text-xs font-medium truncate mb-2"
title="{{ file.filename }}">
{{ file.filename }}
</p>
<div class="flex items-center gap-2">
<button onclick="copyUrl('{{ file.url }}')"
class="flex-1 px-3 py-1.5 bg-gray-800 hover:bg-gray-700 text-gray-300
text-xs rounded-lg transition flex items-center gap-1.5 justify-center">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2
m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
Copy URL
</button>
<a href="/api/media/{{ file.filename }}" target="_blank" rel="noopener"
class="px-3 py-1.5 bg-indigo-900/40 hover:bg-indigo-900/60 text-indigo-400
text-xs rounded-lg transition">
Open
</a>
</div>
</div>
</div>
@end
</div>
@end
</section>
</div>
{{-- ── JavaScript ───────────────────────────────────────────────────── --}}
<script>
// CSRF token injected server-side — sent as a header on the XHR upload request
const CSRF_TOKEN = '{{ csrfToken }}'
const dropZone = document.getElementById('drop-zone')
const fileInput = document.getElementById('file-input')
const dropHint = document.getElementById('drop-hint')
const previewCont = document.getElementById('preview-container')
const imagePreview = document.getElementById('image-preview')
const videoPreview = document.getElementById('video-preview')
const previewName = document.getElementById('preview-filename')
const previewSize = document.getElementById('preview-size')
const uploadActions = document.getElementById('upload-actions')
const uploadBtn = document.getElementById('upload-btn')
const cancelBtn = document.getElementById('cancel-btn')
const progressWrap = document.getElementById('progress-wrapper')
const progressBar = document.getElementById('progress-bar')
const progressPct = document.getElementById('progress-percent')
const uploadResult = document.getElementById('upload-result')
let selectedFile = null
// ── Drag & drop ─────────────────────────────────────────────────────
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
dropZone.classList.add('border-indigo-500', 'bg-indigo-950/20')
})
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500', 'bg-indigo-950/20')
})
dropZone.addEventListener('drop', (e) => {
e.preventDefault()
dropZone.classList.remove('border-indigo-500', 'bg-indigo-950/20')
const file = e.dataTransfer.files[0]
if (file) handleFileSelected(file)
})
fileInput.addEventListener('change', () => {
if (fileInput.files[0]) handleFileSelected(fileInput.files[0])
})
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / 1048576).toFixed(1) + ' MB'
}
function handleFileSelected(file) {
selectedFile = file
imagePreview.classList.add('hidden')
videoPreview.classList.add('hidden')
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => {
imagePreview.src = e.target.result
imagePreview.classList.remove('hidden')
}
reader.readAsDataURL(file)
} else if (file.type.startsWith('video/')) {
videoPreview.src = URL.createObjectURL(file)
videoPreview.classList.remove('hidden')
}
previewName.textContent = file.name
previewSize.textContent = formatBytes(file.size)
dropHint.classList.add('hidden')
previewCont.classList.remove('hidden')
uploadActions.classList.remove('hidden')
uploadResult.classList.add('hidden')
resetProgress()
}
function resetProgress() {
progressWrap.classList.add('hidden')
progressBar.style.width = '0%'
progressPct.textContent = '0%'
}
cancelBtn.addEventListener('click', () => {
selectedFile = null
fileInput.value = ''
dropHint.classList.remove('hidden')
previewCont.classList.add('hidden')
uploadActions.classList.add('hidden')
uploadResult.classList.add('hidden')
resetProgress()
if (videoPreview.src) {
URL.revokeObjectURL(videoPreview.src)
videoPreview.src = ''
}
})
uploadBtn.addEventListener('click', () => {
if (!selectedFile) return
doUpload(selectedFile)
})
function doUpload(file) {
const formData = new FormData()
formData.append('file', file)
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100)
progressBar.style.width = pct + '%'
progressPct.textContent = pct + '%'
}
})
xhr.addEventListener('load', () => {
let data = {}
try { data = JSON.parse(xhr.responseText) } catch {}
progressWrap.classList.add('hidden')
uploadActions.classList.add('hidden')
uploadResult.classList.remove('hidden')
if (xhr.status === 200 && data.success) {
uploadResult.className =
'mt-4 px-4 py-3 rounded-lg text-sm font-medium bg-green-900/40 border border-green-700 text-green-300'
uploadResult.textContent = 'Uploaded: ' + data.filename
// Reload the gallery so the new file appears
setTimeout(() => window.location.reload(), 1200)
} else {
uploadResult.className =
'mt-4 px-4 py-3 rounded-lg text-sm font-medium bg-red-900/40 border border-red-700 text-red-300'
uploadResult.textContent = data.message || 'Upload failed. Please try again.'
uploadBtn.disabled = false
}
})
xhr.addEventListener('error', () => {
progressWrap.classList.add('hidden')
uploadResult.classList.remove('hidden')
uploadResult.className =
'mt-4 px-4 py-3 rounded-lg text-sm font-medium bg-red-900/40 border border-red-700 text-red-300'
uploadResult.textContent = 'Network error. Please check your connection and try again.'
uploadBtn.disabled = false
})
xhr.open('POST', '/upload')
xhr.setRequestHeader('X-CSRF-TOKEN', CSRF_TOKEN)
uploadBtn.disabled = true
previewCont.classList.add('hidden')
progressWrap.classList.remove('hidden')
resetProgress()
xhr.send(formData)
}
// ── Copy URL to clipboard ─────────────────────────────────────────
function copyUrl(url) {
const fullUrl = window.location.origin + url
navigator.clipboard.writeText(fullUrl).then(() => {
const btn = event.currentTarget
const orig = btn.innerHTML
btn.textContent = 'Copied!'
btn.classList.add('text-green-400')
setTimeout(() => { btn.innerHTML = orig; btn.classList.remove('text-green-400') }, 1500)
}).catch(() => {
prompt('Copy this URL:', window.location.origin + url)
})
}
</script>
@endslot
@end

View File

@@ -0,0 +1,77 @@
@layouts.main({ title: 'Sign In — Imago Vault' })
@slot('main')
<div class="min-h-screen flex items-center justify-center px-4">
<div class="w-full max-w-md">
{{-- Logo / heading --}}
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-indigo-600 rounded-2xl mb-4 shadow-lg">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
</div>
<h1 class="text-2xl font-bold text-white">Imago Vault</h1>
<p class="text-gray-400 text-sm mt-1">Sign in to manage your media files</p>
</div>
{{-- Credential error message --}}
@if(flashMessages.has('errors'))
<div class="bg-red-900/40 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-6 text-sm">
{{ flashMessages.get('errors').form ?? 'An error occurred.' }}
</div>
@end
{{-- Login form --}}
<form method="POST" action="/login"
class="bg-gray-900 rounded-2xl p-8 shadow-2xl border border-gray-800">
{{ csrfField() }}
<div class="mb-5">
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">
Username
</label>
<input
type="text"
id="username"
name="username"
value="{{ flashMessages.get('username') ?? '' }}"
required
autocomplete="username"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white
placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500
focus:border-transparent transition"
placeholder="admin"
/>
</div>
<div class="mb-6">
<label for="password" class="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<input
type="password"
id="password"
name="password"
required
autocomplete="current-password"
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white
placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500
focus:border-transparent transition"
placeholder="••••••••"
/>
</div>
<button
type="submit"
class="w-full py-3 px-6 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold
rounded-lg transition duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-400"
>
Sign in
</button>
</form>
</div>
</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>