update: tester submit struk
parent
b701c86656
commit
a4b5c45312
|
@ -4,26 +4,26 @@ using eSPJ.Models;
|
|||
|
||||
namespace eSPJ.Controllers.SpjDriverController;
|
||||
|
||||
[Route("")]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
|
||||
private readonly ILogger<HomeController> _logger;
|
||||
|
||||
public HomeController(ILogger<HomeController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Home/Index.cshtml");
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Home/Index.cshtml");
|
||||
}
|
||||
|
||||
public IActionResult Privacy()
|
||||
[HttpGet("kosong")]
|
||||
public IActionResult Kosong()
|
||||
{
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Home/Privacy.cshtml");
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Home/Kosong.cshtml");
|
||||
}
|
||||
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace eSPJ.Controllers.SpjDriverController
|
|||
{
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Scan/Detail.cshtml");
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("create")]
|
||||
public IActionResult Create()
|
||||
{
|
||||
|
@ -29,30 +29,19 @@ namespace eSPJ.Controllers.SpjDriverController
|
|||
{
|
||||
try
|
||||
{
|
||||
// Validate barcode
|
||||
if (string.IsNullOrEmpty(barcode))
|
||||
{
|
||||
TempData["Error"] = "Kode barcode tidak boleh kosong.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// Basic validation for SPJ barcode format
|
||||
if (barcode.Length < 5)
|
||||
{
|
||||
TempData["Error"] = "Format kode SPJ tidak valid. Minimal 5 karakter.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// Clean the barcode (remove any whitespace)
|
||||
barcode = barcode.Trim();
|
||||
|
||||
// TODO: Add your SPJ validation logic here
|
||||
// For example:
|
||||
// - Check if SPJ exists in database
|
||||
// - Validate SPJ format according to your business rules
|
||||
// - Check SPJ status (active, completed, etc.)
|
||||
|
||||
// Simulate SPJ lookup (replace with your actual database logic)
|
||||
var spjData = await ValidateSpjCode(barcode);
|
||||
|
||||
if (spjData == null)
|
||||
|
@ -61,20 +50,13 @@ namespace eSPJ.Controllers.SpjDriverController
|
|||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// Success - redirect to detail page or next step
|
||||
TempData["Success"] = $"SPJ '{barcode}' berhasil ditemukan!";
|
||||
|
||||
// Redirect to appropriate page based on your workflow
|
||||
// For example, redirect to detail page:
|
||||
return RedirectToAction("Index", "Detail", new { spjCode = barcode });
|
||||
|
||||
// Or redirect to submission page:
|
||||
// return RedirectToAction("Index", "Submit", new { spjCode = barcode });
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Log the error (add your logging here)
|
||||
TempData["Error"] = "Terjadi kesalahan saat memproses scan. Silakan coba lagi.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
@ -82,20 +64,10 @@ namespace eSPJ.Controllers.SpjDriverController
|
|||
|
||||
private async Task<SpjData?> ValidateSpjCode(string barcode)
|
||||
{
|
||||
// TODO: Implement your SPJ validation logic here
|
||||
// This is just a sample implementation
|
||||
|
||||
try
|
||||
{
|
||||
// Simulate database lookup
|
||||
await Task.Delay(100); // Simulate async operation
|
||||
|
||||
// Example validation rules:
|
||||
// 1. Check format (e.g., starts with "SPJ" or specific pattern)
|
||||
// 2. Check if exists in database
|
||||
// 3. Check status
|
||||
|
||||
// For demo purposes, accept codes that start with "SPJ"
|
||||
await Task.Delay(100);
|
||||
if (barcode.ToUpper().StartsWith("SPJ"))
|
||||
{
|
||||
return new SpjData
|
||||
|
@ -107,7 +79,6 @@ namespace eSPJ.Controllers.SpjDriverController
|
|||
};
|
||||
}
|
||||
|
||||
// Return null if not found or invalid
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
|
@ -116,7 +87,6 @@ namespace eSPJ.Controllers.SpjDriverController
|
|||
}
|
||||
}
|
||||
|
||||
// Sample model for SPJ data (replace with your actual model)
|
||||
public class SpjData
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
|
|
@ -18,5 +18,43 @@ namespace eSPJ.Controllers.SpjDriverController
|
|||
{
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Submit/Struk.cshtml");
|
||||
}
|
||||
|
||||
[HttpPost("struk")]
|
||||
public IActionResult ProcessStruk(string NomorStruk, string BeratMuatan)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate inputs
|
||||
if (string.IsNullOrEmpty(NomorStruk) || string.IsNullOrEmpty(BeratMuatan))
|
||||
{
|
||||
TempData["Error"] = "Nomor struk dan berat muatan harus diisi.";
|
||||
return RedirectToAction("Struk");
|
||||
}
|
||||
|
||||
if (NomorStruk.Length < 6)
|
||||
{
|
||||
TempData["Error"] = "Nomor struk minimal 6 digit.";
|
||||
return RedirectToAction("Struk");
|
||||
}
|
||||
|
||||
if (!decimal.TryParse(BeratMuatan, out decimal berat) || berat <= 0)
|
||||
{
|
||||
TempData["Error"] = "Berat muatan harus berupa angka yang valid.";
|
||||
return RedirectToAction("Struk");
|
||||
}
|
||||
|
||||
// Here you would normally save to database
|
||||
// For now, just simulate success
|
||||
|
||||
TempData["Success"] = $"Struk berhasil disubmit! No: {NomorStruk}, Berat: {BeratMuatan} kg";
|
||||
return RedirectToAction("Index", "Home");
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
TempData["Error"] = "Terjadi kesalahan saat memproses struk. Silakan coba lagi.";
|
||||
return RedirectToAction("Struk");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
@{
|
||||
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
|
||||
ViewData["Title"] = "History - DLH";
|
||||
}
|
||||
|
||||
<div class="max-w-sm mx-auto bg-gray-50 min-h-screen">
|
||||
<div class="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-4 py-4 sticky top-0 z-10 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="@Url.Action("Index", "Home")" class="p-2 hover:bg-white/10 rounded-full transition-all duration-200">
|
||||
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
||||
</a>
|
||||
<h1 class="text-lg font-bold">Riwayat Perjalanan</h1>
|
||||
<div class="w-9"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var spjList = new[]
|
||||
{
|
||||
new {
|
||||
Id = 1,
|
||||
NoSpj = "SPJ/07-2025/PKM/000478",
|
||||
Plat = "B 5678 ABC",
|
||||
Kode = "JRC 007",
|
||||
Tujuan = "Bantar Gebang",
|
||||
Status = "In Progress",
|
||||
Tanggal = "28 Jul 2025",
|
||||
Waktu = "16:45"
|
||||
},
|
||||
new {
|
||||
Id = 2,
|
||||
NoSpj = "SPJ/07-2025/PKM/000476",
|
||||
Plat = "B 9632 TOR",
|
||||
Kode = "JRC 005",
|
||||
Tujuan = "RDF Rorotan",
|
||||
Status = "Completed",
|
||||
Tanggal = "27 Jul 2025",
|
||||
Waktu = "14:30"
|
||||
},
|
||||
new {
|
||||
Id = 3,
|
||||
NoSpj = "SPJ/07-2025/PKM/000477",
|
||||
Plat = "B 1234 XYZ",
|
||||
Kode = "JRC 006",
|
||||
Tujuan = "RDF Pesanggarahan",
|
||||
Status = "Completed",
|
||||
Tanggal = "26 Jul 2025",
|
||||
Waktu = "09:15"
|
||||
},
|
||||
new {
|
||||
Id = 4,
|
||||
NoSpj = "SPJ/07-2025/PKM/000479",
|
||||
Plat = "B 9876 DEF",
|
||||
Kode = "JRC 008",
|
||||
Tujuan = "RDF Sunter",
|
||||
Status = "Completed",
|
||||
Tanggal = "25 Jul 2025",
|
||||
Waktu = "11:20"
|
||||
},
|
||||
new {
|
||||
Id = 5,
|
||||
NoSpj = "SPJ/07-2025/PKM/000480",
|
||||
Plat = "B 4321 GHI",
|
||||
Kode = "JRC 009",
|
||||
Tujuan = "Bantar Gebang",
|
||||
Status = "Completed",
|
||||
Tanggal = "24 Jul 2025",
|
||||
Waktu = "08:45"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
<div class="px-4 py-4 space-y-3">
|
||||
@foreach (var spj in spjList)
|
||||
{
|
||||
<a href="@Url.Action("Details", "History", new { id = spj.Id })" class="block">
|
||||
<div class="bg-white rounded-2xl p-4 shadow-sm border border-gray-100 hover:shadow-lg hover:border-orange-200 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-orange-400 to-orange-500"></div>
|
||||
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="w-2 h-2 bg-orange-400 rounded-full"></div>
|
||||
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">No. SPJ</span>
|
||||
</div>
|
||||
<div class="font-bold text-gray-900 text-sm">@spj.NoSpj</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
@if (spj.Status == "Completed")
|
||||
{
|
||||
<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs font-semibold flex items-center gap-1">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
Selesai
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs font-semibold flex items-center gap-1">
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
Berlangsung
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-xl p-3 mb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
|
||||
<i class="w-5 h-5 text-orange-600" data-lucide="car"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-gray-900 text-sm">@spj.Plat</div>
|
||||
<div class="text-xs text-gray-500">@spj.Kode</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-gray-500">@spj.Tanggal</div>
|
||||
<div class="text-xs font-medium text-gray-700">@spj.Waktu</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2 border-t border-gray-100">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<i class="w-4 h-4 text-green-600" data-lucide="map-pin"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-xs text-gray-500 mb-1">Tujuan</div>
|
||||
<div class="font-semibold text-gray-900 text-sm">@spj.Tujuan</div>
|
||||
</div>
|
||||
<div class="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
||||
<i class="w-4 h-4 text-gray-400" data-lucide="chevron-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Bottom Navigation -->
|
||||
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_Navigation.cshtml" />
|
||||
|
||||
<!-- Kalau butuh tampilan kosong (jika tidak ada data) -->
|
||||
|
||||
@* <div class="flex flex-col items-center justify-center py-16 px-4">
|
||||
<div class="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="w-12 h-12 text-gray-400" data-lucide="clock"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Belum Ada Riwayat</h3>
|
||||
<p class="text-gray-500 text-center text-sm">Riwayat perjalanan Anda akan muncul di sini setelah melakukan perjalanan pertama.</p>
|
||||
</div> *@
|
||||
|
||||
</div>
|
|
@ -0,0 +1,221 @@
|
|||
@{
|
||||
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
|
||||
ViewData["Title"] = "Home Page";
|
||||
}
|
||||
|
||||
<div class="container max-w-sm mx-auto bg-white min-h-screen">
|
||||
|
||||
<div class="absolute top-0 max-w-sm container mx-auto bg-orange-500 text-white rounded-br-[125px] h-[250px] flex flex-row justify-between items-start px-6 py-6 shadow-lg z-20">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-md font-bold leading-tight text-white">Bonny Agung Putra</h1>
|
||||
<p class="text-xs opacity-90 font-medium text-orange-100">Driver UPST</span></p>
|
||||
<div class="mt-5 flex items-center gap-2">
|
||||
<i class="w-4 h-4 text-white" data-lucide="map-pin"></i>
|
||||
<span class="text-sm opacity-90">Lokasi Anda:</span>
|
||||
</div>
|
||||
<p id="userLocation" class="font-semibold text-xs tracking-wide cursor-pointer underline text-white hover:text-orange-200 transition">
|
||||
Mendeteksi lokasi...
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative flex flex-col items-center justify-center">
|
||||
<div class="w-12 h-12 rounded-full border-3 border-white overflow-hidden shadow-md flex items-center justify-center cursor-pointer group" id="profileMenuButton">
|
||||
<i class="w-8 h-8 text-white" data-lucide="user"></i>
|
||||
</div>
|
||||
<div id="profileMenuDropdown" class="absolute top-12 right-0 mt-2 w-32 bg-white rounded shadow-lg py-2 z-50 hidden">
|
||||
<form method="post" asp-controller="Auth" asp-action="Logout">
|
||||
<button type="submit" class="hover:cursor-pointer flex items-center gap-2 w-full text-left px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 transition rounded-md">
|
||||
<i class="w-4 h-4" data-lucide="log-out"></i>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 mx-4 px-6 py-12 mt-40 rounded-3xl relative overflow-hidden shadow-2xl z-21 border border-slate-200">
|
||||
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
|
||||
<div class="absolute top-4 right-8 w-6 h-6 bg-orange-300 rounded-full opacity-60 animate-bounce" style="animation-delay: 0.5s;"></div>
|
||||
<div class="absolute top-12 left-12 w-4 h-4 bg-blue-400 rounded-full opacity-40 animate-pulse" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute bottom-8 left-8 w-5 h-5 bg-indigo-300 rounded-full opacity-50 animate-bounce" style="animation-delay: 1.5s;"></div>
|
||||
<div class="absolute bottom-16 right-16 w-3 h-3 bg-slate-400 rounded-full opacity-30 animate-pulse" style="animation-delay: 2s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 text-center">
|
||||
<div class="w-24 h-24 mx-auto mb-6 relative">
|
||||
<div class="w-full h-full bg-gradient-to-br from-orange-400 to-orange-600 rounded-full flex items-center justify-center shadow-xl animate-pulse">
|
||||
<div class="w-20 h-20 bg-white rounded-full flex items-center justify-center">
|
||||
<i class="w-10 h-10 text-orange-500" data-lucide="clipboard-list"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 border-4 border-orange-300 rounded-full animate-ping opacity-30"></div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold bg-gradient-to-r from-slate-700 to-slate-900 bg-clip-text text-transparent">
|
||||
Belum Ada SPJ
|
||||
</h2>
|
||||
<p class="text-sm text-slate-600 leading-relaxed px-4 mb-2">
|
||||
Anda belum memiliki <span class="font-semibold text-orange-600">Surat Perintah Jalan</span> yang aktif saat ini.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/70 backdrop-blur-sm border border-white/30 rounded-2xl p-5 mb-6 text-left">
|
||||
<div class="space-y-2 text-xs text-slate-600">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-1 h-1 bg-orange-400 rounded-full mt-1.5 flex-shrink-0"></div>
|
||||
<p>SPJ akan diterbitkan oleh admin sesuai jadwal kerja</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-1 h-1 bg-orange-400 rounded-full mt-1.5 flex-shrink-0"></div>
|
||||
<p>Periksa koneksi internet dan aktifkan lokasi GPS</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="refreshButton" class="bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white px-6 py-3 rounded-2xl text-sm font-bold shadow-lg hover:shadow-xl transition-all duration-300 flex items-center gap-2 mx-auto transform hover:scale-105">
|
||||
<i class="w-4 h-4" data-lucide="refresh-cw" id="refreshIcon"></i>
|
||||
<span id="refreshText">Refresh Halaman</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_NavigationAdmin.cshtml" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsHomeKosong">
|
||||
<style>
|
||||
@@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@@keyframes shimmer {
|
||||
0% { background-position: -200px 0; }
|
||||
100% { background-position: calc(200px + 100%) 0; }
|
||||
}
|
||||
|
||||
.float-animation {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
||||
background-size: 200px 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.refresh-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const userLocationEl = document.getElementById("userLocation");
|
||||
|
||||
function reverseGeocode(lat, lng) {
|
||||
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const address = data.display_name || `${lat}, ${lng}`;
|
||||
userLocationEl.textContent = address;
|
||||
localStorage.setItem("user_latitude", lat);
|
||||
localStorage.setItem("user_longitude", lng);
|
||||
localStorage.setItem("user_address", address);
|
||||
})
|
||||
.catch(() => {
|
||||
userLocationEl.textContent = `${lat}, ${lng}`;
|
||||
});
|
||||
}
|
||||
|
||||
function getLocationUpdate() {
|
||||
if ("geolocation" in navigator) {
|
||||
userLocationEl.textContent = "Mendeteksi lokasi baru...";
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function (position) {
|
||||
const lat = position.coords.latitude.toFixed(6);
|
||||
const lng = position.coords.longitude.toFixed(6);
|
||||
reverseGeocode(lat, lng);
|
||||
},
|
||||
function () {
|
||||
userLocationEl.textContent = "Lokasi tidak diizinkan";
|
||||
}
|
||||
);
|
||||
} else {
|
||||
userLocationEl.textContent = "Browser tidak mendukung lokasi";
|
||||
}
|
||||
}
|
||||
|
||||
const savedAddress = localStorage.getItem("user_address");
|
||||
if (savedAddress) {
|
||||
userLocationEl.textContent = savedAddress;
|
||||
} else {
|
||||
getLocationUpdate();
|
||||
}
|
||||
|
||||
userLocationEl.addEventListener("click", function () {
|
||||
getLocationUpdate();
|
||||
});
|
||||
|
||||
const refreshBtn = document.getElementById('refreshButton');
|
||||
const refreshIcon = document.getElementById('refreshIcon');
|
||||
const refreshText = document.getElementById('refreshText');
|
||||
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener("click", function() {
|
||||
refreshIcon.style.animation = 'spin 1s linear infinite';
|
||||
refreshText.textContent = 'Memuat...';
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.style.opacity = '0.8';
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const mainIcon = document.querySelector('[data-lucide="clipboard-list"]');
|
||||
if (mainIcon) {
|
||||
mainIcon.closest('.w-24').classList.add('float-animation');
|
||||
}
|
||||
|
||||
const mainCard = document.querySelector('.bg-gradient-to-br');
|
||||
if (mainCard) {
|
||||
mainCard.style.opacity = '0';
|
||||
mainCard.style.transform = 'translateY(20px)';
|
||||
mainCard.style.transition = 'all 0.6s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
mainCard.style.opacity = '1';
|
||||
mainCard.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const btn = document.getElementById("profileMenuButton");
|
||||
const dropdown = document.getElementById("profileMenuDropdown");
|
||||
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
dropdown.classList.toggle("hidden");
|
||||
});
|
||||
|
||||
document.addEventListener("click", function () {
|
||||
dropdown.classList.add("hidden");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</register-block>
|
|
@ -5,30 +5,6 @@
|
|||
|
||||
@section Styles {
|
||||
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
|
||||
<style>
|
||||
/* Html5-QRCode specific styles */
|
||||
#scanner-container video {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#scanner-container canvas {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Hide Html5-QRCode default UI elements */
|
||||
#scanner-container select,
|
||||
#scanner-container button {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
|
||||
|
@ -43,9 +19,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanner Section -->
|
||||
<div class="p-4">
|
||||
<!-- Alert Messages -->
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
|
@ -66,12 +40,9 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
<!-- Camera Preview -->
|
||||
<div class="scanner-container mb-4" style="height: 300px;">
|
||||
<div id="scanner-container" class="w-full h-full relative bg-gray-900 rounded-lg overflow-hidden">
|
||||
<!-- Html5-QRCode will create video element here -->
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
|
||||
<div class="text-center text-white">
|
||||
<div class="loading-spinner mx-auto mb-2"></div>
|
||||
|
@ -81,7 +52,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanner Controls -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<button id="start-scanner" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner">
|
||||
<i class="w-5 h-5 inline mr-2" data-lucide="camera"></i>
|
||||
|
@ -93,7 +63,6 @@
|
|||
Hentikan Scan
|
||||
</button>
|
||||
|
||||
<!-- Camera Permission Info -->
|
||||
<div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-start">
|
||||
<i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
|
||||
|
@ -109,7 +78,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permission Denied Info -->
|
||||
<div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div class="flex items-start">
|
||||
<i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i>
|
||||
|
@ -125,7 +93,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanning Tips -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
|
||||
<div class="flex items-start">
|
||||
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
|
||||
|
@ -143,7 +110,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Input Alternative -->
|
||||
<div class="border-t pt-4">
|
||||
<h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3>
|
||||
<form id="manual-form" method="post" action="@Url.Action("ProcessScan", "Scan")">
|
||||
|
@ -160,7 +126,6 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Scan Result -->
|
||||
<div id="scan-result" class="hidden mt-4 p-4 bg-green-50 border border-green-200 rounded-lg scan-result-card">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
|
||||
|
@ -177,7 +142,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div id="error-message" class="hidden mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
|
||||
|
@ -185,16 +149,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_NavigationAdmin.cshtml" />
|
||||
</div>
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsScan">
|
||||
<!-- Html5-QRCode Library -->
|
||||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script>
|
||||
|
||||
<!-- Fallback script loader -->
|
||||
<script>
|
||||
// Check if library loaded, if not try alternative CDN
|
||||
if (typeof Html5Qrcode === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js';
|
||||
|
@ -238,7 +199,6 @@
|
|||
}
|
||||
|
||||
checkBrowserSupport() {
|
||||
// Check if browser supports getUserMedia
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
this.startBtn.disabled = true;
|
||||
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Browser Tidak Didukung';
|
||||
|
@ -248,7 +208,6 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if Html5Qrcode is loaded
|
||||
if (typeof Html5Qrcode === 'undefined') {
|
||||
this.startBtn.disabled = true;
|
||||
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Library Tidak Dimuat';
|
||||
|
@ -258,7 +217,6 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if page is served over HTTPS (required for camera access in production)
|
||||
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
||||
this.showError('Scanner barcode memerlukan koneksi HTTPS yang aman. Hubungi administrator sistem.');
|
||||
}
|
||||
|
@ -271,7 +229,6 @@
|
|||
this.hideResult();
|
||||
this.hidePermissionMessages();
|
||||
|
||||
// Initialize Html5-QRCode scanner
|
||||
await this.initializeHtml5QrCode();
|
||||
|
||||
this.isScanning = true;
|
||||
|
@ -287,23 +244,17 @@
|
|||
|
||||
async initializeHtml5QrCode() {
|
||||
try {
|
||||
// Show permission info
|
||||
this.permissionInfo.classList.remove('hidden');
|
||||
|
||||
// Wait a moment to show the message
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Initialize Html5-QRCode
|
||||
this.html5QrCode = new Html5Qrcode("scanner-container");
|
||||
|
||||
// Get available cameras - This will trigger permission request
|
||||
const cameras = await Html5Qrcode.getCameras();
|
||||
|
||||
if (cameras && cameras.length > 0) {
|
||||
// Try to use back camera first, fallback to first available
|
||||
let cameraId = cameras[0].id;
|
||||
|
||||
// Look for back camera
|
||||
const backCamera = cameras.find(camera =>
|
||||
camera.label.toLowerCase().includes('back') ||
|
||||
camera.label.toLowerCase().includes('rear') ||
|
||||
|
@ -314,14 +265,12 @@
|
|||
cameraId = backCamera.id;
|
||||
}
|
||||
|
||||
// Start scanning with square QR code focus
|
||||
await this.html5QrCode.start(
|
||||
cameraId,
|
||||
{
|
||||
fps: 10, // Frame per second
|
||||
fps: 10,
|
||||
qrbox: function(viewfinderWidth, viewfinderHeight) {
|
||||
// Make it square - use the smaller dimension
|
||||
let minEdgePercentage = 0.7; // 70% of the smaller edge
|
||||
let minEdgePercentage = 0.7;
|
||||
let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight);
|
||||
let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage);
|
||||
return {
|
||||
|
@ -329,15 +278,13 @@
|
|||
height: qrboxSize
|
||||
};
|
||||
},
|
||||
aspectRatio: 1.0, // Square ratio for QR codes
|
||||
aspectRatio: 1.0,
|
||||
rememberLastUsedCamera: true
|
||||
},
|
||||
(decodedText, decodedResult) => {
|
||||
// Success callback
|
||||
this.handleBarcodeDetected(decodedText, decodedResult);
|
||||
},
|
||||
(errorMessage) => {
|
||||
// Error callback (optional, can be ignored for scanning errors)
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -377,19 +324,14 @@
|
|||
}
|
||||
|
||||
handleBarcodeDetected(decodedText, decodedResult) {
|
||||
// Validate the code (basic validation)
|
||||
if (decodedText && decodedText.length >= 5) {
|
||||
// Add visual feedback
|
||||
this.flashSuccess();
|
||||
|
||||
this.detectedCode = decodedText;
|
||||
this.showResult(decodedText);
|
||||
this.stopScanner(); // Auto stop after detection
|
||||
|
||||
// Play a success sound
|
||||
this.stopScanner();
|
||||
this.playSuccessSound();
|
||||
|
||||
// Haptic feedback if supported
|
||||
this.vibrate();
|
||||
}
|
||||
}
|
||||
|
@ -399,7 +341,6 @@
|
|||
try {
|
||||
await this.html5QrCode.stop();
|
||||
} catch (error) {
|
||||
// Error stopping scanner
|
||||
}
|
||||
this.isScanning = false;
|
||||
}
|
||||
|
@ -409,7 +350,6 @@
|
|||
}
|
||||
|
||||
flashSuccess() {
|
||||
// Add a green flash overlay to indicate successful scan
|
||||
const flash = document.createElement('div');
|
||||
flash.className = 'absolute inset-0 bg-green-500 opacity-50 rounded-lg';
|
||||
flash.style.zIndex = '20';
|
||||
|
@ -421,15 +361,13 @@
|
|||
}
|
||||
|
||||
vibrate() {
|
||||
// Provide haptic feedback on mobile devices
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate([200]); // Vibrate for 200ms
|
||||
navigator.vibrate([200]);
|
||||
}
|
||||
}
|
||||
|
||||
confirmScan() {
|
||||
if (this.detectedCode) {
|
||||
// Auto-fill manual input and submit
|
||||
this.manualInput.value = this.detectedCode;
|
||||
this.manualForm.submit();
|
||||
}
|
||||
|
@ -441,12 +379,10 @@
|
|||
this.hidePermissionMessages();
|
||||
this.detectedCode = null;
|
||||
|
||||
// Stop current scanner if running
|
||||
if (this.isScanning && this.html5QrCode) {
|
||||
await this.stopScanner();
|
||||
}
|
||||
|
||||
// Wait a bit before restarting
|
||||
setTimeout(() => {
|
||||
this.startScanner();
|
||||
}, 500);
|
||||
|
@ -499,7 +435,6 @@
|
|||
}
|
||||
|
||||
playSuccessSound() {
|
||||
// Create a simple beep sound
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
|
@ -517,14 +452,11 @@
|
|||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.2);
|
||||
} catch (e) {
|
||||
// Ignore audio errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize scanner when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Function to check if Html5Qrcode is loaded
|
||||
function waitForLibrary() {
|
||||
if (typeof Html5Qrcode !== 'undefined') {
|
||||
new BarcodeScanner();
|
||||
|
@ -533,7 +465,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Start checking
|
||||
waitForLibrary();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<div class="fixed bottom-0 left-1/2 transform -translate-x-1/2 w-full max-w-sm z-99">
|
||||
<div class="relative backdrop-blur-lg border border-gray-200/50 rounded-t-3xl shadow-xl overflow-hidden">
|
||||
<div class="absolute -top-0 left-1/2 transform -translate-x-1/2 w-20 h-10">
|
||||
<div class="w-full h-full bg-transparent relative">
|
||||
<div class="absolute left-0 top-0 w-10 h-10 backdrop-blur-lg rounded-br-full"></div>
|
||||
<div class="absolute right-0 top-0 w-10 h-10 backdrop-blur-lg rounded-bl-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Content -->
|
||||
<div class="flex justify-between items-center px-8 relative pt-6">
|
||||
<!-- Home Button -->
|
||||
<a href="@Url.Action("Kosong", "Home")" class="flex flex-col items-center gap-1 px-4 py-2 transition-all duration-300 hover:scale-105 group">
|
||||
<div class="relative">
|
||||
<i class="w-6 h-6 text-gray-400 group-hover:text-orange-500 transition-colors duration-300" data-lucide="home"></i>
|
||||
<div class="absolute -bottom-0.5 left-1/2 transform -translate-x-1/2 w-0 h-0.5 bg-orange-500 group-hover:w-full transition-all duration-300"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 group-hover:text-orange-500 font-medium transition-colors duration-300">Home</span>
|
||||
</a>
|
||||
|
||||
<div class="w-12"></div>
|
||||
|
||||
<!-- Profile Button -->
|
||||
|
||||
<a href="@Url.Action("Index", "History")" class="flex flex-col items-center gap-1 px-4 py-2 transition-all duration-300 hover:scale-105 group">
|
||||
<div class="relative">
|
||||
<i class="w-6 h-6 text-gray-400 group-hover:text-orange-500 transition-colors duration-300" data-lucide="clipboard-check"></i>
|
||||
<div class="absolute -bottom-0.5 left-1/2 transform -translate-x-1/2 w-0 h-0.5 bg-orange-500 group-hover:w-full transition-all duration-300"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 group-hover:text-orange-500 font-medium transition-colors duration-300">History</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Submit -->
|
||||
<div class="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<a href="@Url.Action("Index", "Scan")" id="odoBtn" class="hover:cursor-pointer w-14 h-14 bg-gradient-to-br from-orange-500 via-orange-400 to-orange-600 rounded-full shadow-xl flex items-center justify-center transition-all duration-300 hover:scale-110 hover:rotate-6 border-4 border-white ring-2 ring-orange-200">
|
||||
<i class="w-6 h-6 text-white" data-lucide="camera"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-30"></div>
|
||||
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsNav">
|
||||
|
||||
</register-block>
|
|
@ -3,6 +3,9 @@
|
|||
ViewData["Title"] = "Submit Struk";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
|
||||
}
|
||||
|
||||
<div class="max-w-sm mx-auto bg-white min-h-screen">
|
||||
<!-- Header with Orange Background -->
|
||||
|
@ -16,13 +19,134 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/submit-struk" method="post" class="px-8 py-8 space-y-6 bg-white rounded-xl mt-2">
|
||||
<div class="px-8 py-4">
|
||||
<!-- Camera Scanner Section -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col items-center space-y-2 mb-4">
|
||||
<div class="bg-orange-100 rounded-full p-3">
|
||||
<i data-lucide="camera" class="w-7 h-7 text-orange-500"></i>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-orange-500">Scan Struk Otomatis</h2>
|
||||
<p class="text-sm text-gray-500 text-center">Arahkan kamera ke struk untuk membaca data secara otomatis.</p>
|
||||
</div>
|
||||
|
||||
<!-- Scanner Container -->
|
||||
<div class="scanner-container mb-4" style="height: 300px;">
|
||||
<div id="scanner-container" class="w-full h-full relative bg-gray-900 rounded-lg overflow-hidden">
|
||||
<div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
|
||||
<div class="text-center text-white">
|
||||
<div class="loading-spinner mx-auto mb-2"></div>
|
||||
<p class="text-sm">Memuat scanner...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanner Controls -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<button id="start-scanner" type="button" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner">
|
||||
<i class="w-5 h-5 inline mr-2" data-lucide="camera"></i>
|
||||
Mulai Scan Struk
|
||||
</button>
|
||||
|
||||
<button id="stop-scanner" type="button" class="w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner hidden">
|
||||
<i class="w-5 h-5 inline mr-2" data-lucide="camera-off"></i>
|
||||
Hentikan Scan
|
||||
</button>
|
||||
|
||||
<!-- Permission Messages -->
|
||||
<div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-start">
|
||||
<i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
|
||||
<div class="text-blue-800 text-sm">
|
||||
<p class="font-medium mb-1">📸 Meminta Akses Kamera...</p>
|
||||
<p class="mb-2">Browser akan meminta izin akses kamera. Pastikan untuk:</p>
|
||||
<ul class="text-xs space-y-1 list-disc list-inside">
|
||||
<li>Klik tombol <strong>"Allow"</strong> atau <strong>"Izinkan"</strong></li>
|
||||
<li>Jika popup tidak muncul, cek address bar browser</li>
|
||||
<li>Pastikan kamera tidak sedang digunakan aplikasi lain</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div class="flex items-start">
|
||||
<i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i>
|
||||
<div class="text-red-800 text-sm">
|
||||
<p class="font-medium mb-1">Akses Kamera Ditolak</p>
|
||||
<p class="mb-2">Untuk menggunakan scanner, aktifkan akses kamera:</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>Klik ikon kunci/kamera di address bar browser</li>
|
||||
<li>Pilih "Allow" atau "Izinkan" untuk kamera</li>
|
||||
<li>Refresh halaman dan coba lagi</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OCR Processing Message -->
|
||||
<div id="ocr-processing" class="hidden bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<div class="flex items-center">
|
||||
<div class="loading-spinner-small mr-2"></div>
|
||||
<span class="text-yellow-800 text-sm">Memproses teks dari struk...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OCR Result -->
|
||||
<div id="ocr-result" class="hidden bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
|
||||
<span class="text-green-800 font-medium">Data struk berhasil dibaca!</span>
|
||||
</div>
|
||||
<div class="text-green-700 text-sm space-y-1">
|
||||
<p>Nomor Struk: <span id="detected-receipt-number" class="font-mono font-bold">-</span></p>
|
||||
<p>Berat Muatan: <span id="detected-weight" class="font-mono font-bold">-</span> kg</p>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-3">
|
||||
<button id="apply-ocr-data" type="button" class="bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded-lg transition-colors text-sm">
|
||||
Gunakan Data Ini
|
||||
</button>
|
||||
<button id="retry-ocr" type="button" class="bg-gray-500 hover:bg-gray-600 text-white px-3 py-2 rounded-lg transition-colors text-sm">
|
||||
Scan Ulang
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tips for scanning -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
|
||||
<div class="flex items-start">
|
||||
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
|
||||
<div class="text-gray-700">
|
||||
<p class="font-medium mb-1">Tips Scan Struk:</p>
|
||||
<ul class="text-xs space-y-1">
|
||||
<li>• Pastikan struk dalam pencahayaan yang cukup</li>
|
||||
<li>• Letakkan struk rata tanpa lipatan</li>
|
||||
<li>• Jaga jarak 15-30cm dari kamera</li>
|
||||
<li>• Tunggu beberapa detik untuk proses OCR</li>
|
||||
<li>• Periksa data yang terdeteksi sebelum submit</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="flex items-center my-6">
|
||||
<div class="flex-1 border-t border-gray-200"></div>
|
||||
<span class="px-3 text-sm text-gray-500 bg-white">atau input manual</span>
|
||||
<div class="flex-1 border-t border-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="@Url.Action("ProcessStruk", "Submit")" method="post" class="px-8 py-4 space-y-6 bg-white">
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<div class="bg-orange-100 rounded-full p-3">
|
||||
<i data-lucide="file-text" class="w-7 h-7 text-orange-500"></i>
|
||||
<i data-lucide="edit-3" class="w-7 h-7 text-orange-500"></i>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-orange-500">Isi Data Struk</h2>
|
||||
<p class="text-sm text-gray-500 text-center">Masukkan nomor struk dan berat muatan dengan benar.</p>
|
||||
<h2 class="text-xl font-bold text-orange-500">Input Manual</h2>
|
||||
<p class="text-sm text-gray-500 text-center">Masukkan nomor struk dan berat muatan secara manual.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -65,11 +189,13 @@
|
|||
</div>
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsSubmitStruk">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@4.1.1/dist/tesseract.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const nomorStrukInput = document.getElementById('NomorStruk');
|
||||
const beratMuatanInput = document.getElementById('BeratMuatan');
|
||||
|
||||
// Input validation for manual entry
|
||||
nomorStrukInput.addEventListener('input', function() {
|
||||
this.value = this.value.replace(/[^0-9]/g, '');
|
||||
});
|
||||
|
@ -77,6 +203,396 @@
|
|||
beratMuatanInput.addEventListener('input', function() {
|
||||
this.value = this.value.replace(/[^0-9.]/g, '');
|
||||
});
|
||||
|
||||
// Initialize Receipt Scanner with OCR
|
||||
class ReceiptScanner {
|
||||
constructor() {
|
||||
this.isScanning = false;
|
||||
this.stream = null;
|
||||
this.video = null;
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.detectedData = {
|
||||
receiptNumber: '',
|
||||
weight: ''
|
||||
};
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
this.checkBrowserSupport();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.startBtn = document.getElementById('start-scanner');
|
||||
this.stopBtn = document.getElementById('stop-scanner');
|
||||
this.loadingDiv = document.getElementById('loading-scanner');
|
||||
this.ocrProcessing = document.getElementById('ocr-processing');
|
||||
this.ocrResult = document.getElementById('ocr-result');
|
||||
this.permissionInfo = document.getElementById('permission-info');
|
||||
this.permissionDenied = document.getElementById('permission-denied');
|
||||
this.applyDataBtn = document.getElementById('apply-ocr-data');
|
||||
this.retryOcrBtn = document.getElementById('retry-ocr');
|
||||
this.detectedReceiptSpan = document.getElementById('detected-receipt-number');
|
||||
this.detectedWeightSpan = document.getElementById('detected-weight');
|
||||
this.scannerContainer = document.getElementById('scanner-container');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.startBtn.addEventListener('click', () => this.startScanner());
|
||||
this.stopBtn.addEventListener('click', () => this.stopScanner());
|
||||
this.applyDataBtn.addEventListener('click', () => this.applyDetectedData());
|
||||
this.retryOcrBtn.addEventListener('click', () => this.retryOcr());
|
||||
}
|
||||
|
||||
checkBrowserSupport() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
this.disableScanner('Browser Tidak Didukung');
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
||||
this.showWarning('Scanner berfungsi optimal dengan koneksi HTTPS yang aman.');
|
||||
}
|
||||
}
|
||||
|
||||
disableScanner(message) {
|
||||
this.startBtn.disabled = true;
|
||||
this.startBtn.innerHTML = `<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>${message}`;
|
||||
this.startBtn.classList.remove('bg-orange-500', 'hover:bg-orange-600');
|
||||
this.startBtn.classList.add('bg-gray-400', 'cursor-not-allowed');
|
||||
}
|
||||
|
||||
async startScanner() {
|
||||
try {
|
||||
this.showLoading();
|
||||
this.hideMessages();
|
||||
this.permissionInfo.classList.remove('hidden');
|
||||
|
||||
// Get camera access
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
});
|
||||
|
||||
this.setupVideoElement();
|
||||
this.isScanning = true;
|
||||
this.startBtn.classList.add('hidden');
|
||||
this.stopBtn.classList.remove('hidden');
|
||||
this.hideLoading();
|
||||
this.hideMessages();
|
||||
|
||||
// Start continuous capture for OCR
|
||||
this.startContinuousCapture();
|
||||
|
||||
} catch (error) {
|
||||
this.handleCameraError(error);
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
setupVideoElement() {
|
||||
// Create video element
|
||||
this.video = document.createElement('video');
|
||||
this.video.autoplay = true;
|
||||
this.video.playsInline = true;
|
||||
this.video.muted = true;
|
||||
this.video.srcObject = this.stream;
|
||||
|
||||
// Style video element
|
||||
this.video.className = 'w-full h-full object-cover rounded-lg';
|
||||
|
||||
// Clear container and add video
|
||||
this.scannerContainer.innerHTML = '';
|
||||
this.scannerContainer.appendChild(this.video);
|
||||
|
||||
// Create canvas for capture
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
}
|
||||
|
||||
startContinuousCapture() {
|
||||
// Capture frame every 3 seconds for OCR processing
|
||||
const captureInterval = setInterval(() => {
|
||||
if (!this.isScanning) {
|
||||
clearInterval(captureInterval);
|
||||
return;
|
||||
}
|
||||
this.captureAndProcessFrame();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async captureAndProcessFrame() {
|
||||
if (!this.video || !this.canvas || !this.isScanning) return;
|
||||
|
||||
try {
|
||||
// Set canvas size to video size
|
||||
this.canvas.width = this.video.videoWidth;
|
||||
this.canvas.height = this.video.videoHeight;
|
||||
|
||||
// Draw video frame to canvas
|
||||
this.ctx.drawImage(this.video, 0, 0);
|
||||
|
||||
// Convert to blob for OCR
|
||||
this.canvas.toBlob(async (blob) => {
|
||||
await this.processImageWithOCR(blob);
|
||||
}, 'image/jpeg', 0.8);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error capturing frame:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async processImageWithOCR(imageBlob) {
|
||||
try {
|
||||
this.showOcrProcessing();
|
||||
|
||||
const { data: { text } } = await Tesseract.recognize(
|
||||
imageBlob,
|
||||
'ind+eng',
|
||||
{
|
||||
logger: m => {
|
||||
// Optional: show OCR progress
|
||||
if (m.status === 'recognizing text') {
|
||||
console.log(`OCR Progress: ${Math.round(m.progress * 100)}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await this.extractDataFromText(text);
|
||||
this.hideOcrProcessing();
|
||||
|
||||
} catch (error) {
|
||||
console.error('OCR Error:', error);
|
||||
this.hideOcrProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
async extractDataFromText(text) {
|
||||
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
||||
|
||||
let receiptNumber = '';
|
||||
let weight = '';
|
||||
|
||||
// Patterns for receipt number detection
|
||||
const receiptPatterns = [
|
||||
/(?:no|nomor|receipt|struk|bon)[\s.:]*([0-9]{6,})/i,
|
||||
/([0-9]{8,})/g, // Long number sequences
|
||||
/(?:ticket|tiket)[\s.:]*([0-9]+)/i
|
||||
];
|
||||
|
||||
// Patterns for weight detection
|
||||
const weightPatterns = [
|
||||
/(?:berat|weight|kg|kilogram)[\s.:]*([0-9]+(?:\.[0-9]+)?)/i,
|
||||
/([0-9]+(?:\.[0-9]+)?)[\s]*(?:kg|kilogram)/i,
|
||||
/(?:muatan|load)[\s.:]*([0-9]+(?:\.[0-9]+)?)/i
|
||||
];
|
||||
|
||||
// Search for receipt number
|
||||
for (const line of lines) {
|
||||
for (const pattern of receiptPatterns) {
|
||||
const match = line.match(pattern);
|
||||
if (match && match[1] && match[1].length >= 6) {
|
||||
receiptNumber = match[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (receiptNumber) break;
|
||||
}
|
||||
|
||||
// Search for weight
|
||||
for (const line of lines) {
|
||||
for (const pattern of weightPatterns) {
|
||||
const match = line.match(pattern);
|
||||
if (match && match[1]) {
|
||||
const weightValue = parseFloat(match[1]);
|
||||
if (weightValue > 0 && weightValue < 100000) { // Reasonable weight range
|
||||
weight = match[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (weight) break;
|
||||
}
|
||||
|
||||
// If we found either piece of data, show results
|
||||
if (receiptNumber || weight) {
|
||||
this.detectedData.receiptNumber = receiptNumber;
|
||||
this.detectedData.weight = weight;
|
||||
this.showOcrResult();
|
||||
this.playSuccessSound();
|
||||
this.vibrate();
|
||||
|
||||
// Auto-stop scanning after successful detection
|
||||
setTimeout(() => {
|
||||
this.stopScanner();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
showOcrResult() {
|
||||
this.detectedReceiptSpan.textContent = this.detectedData.receiptNumber || 'Tidak terdeteksi';
|
||||
this.detectedWeightSpan.textContent = this.detectedData.weight || 'Tidak terdeteksi';
|
||||
this.ocrResult.classList.remove('hidden');
|
||||
}
|
||||
|
||||
applyDetectedData() {
|
||||
if (this.detectedData.receiptNumber) {
|
||||
nomorStrukInput.value = this.detectedData.receiptNumber;
|
||||
nomorStrukInput.classList.add('auto-filled');
|
||||
setTimeout(() => nomorStrukInput.classList.remove('auto-filled'), 2000);
|
||||
}
|
||||
if (this.detectedData.weight) {
|
||||
beratMuatanInput.value = this.detectedData.weight;
|
||||
beratMuatanInput.classList.add('auto-filled');
|
||||
setTimeout(() => beratMuatanInput.classList.remove('auto-filled'), 2000);
|
||||
}
|
||||
this.hideMessages();
|
||||
|
||||
// Show success message
|
||||
this.showSuccess('Data dari scan telah diisi ke form!');
|
||||
}
|
||||
|
||||
async retryOcr() {
|
||||
this.hideMessages();
|
||||
this.detectedData = { receiptNumber: '', weight: '' };
|
||||
|
||||
if (this.isScanning) {
|
||||
await this.stopScanner();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.startScanner();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async stopScanner() {
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
}
|
||||
|
||||
this.isScanning = false;
|
||||
this.startBtn.classList.remove('hidden');
|
||||
this.stopBtn.classList.add('hidden');
|
||||
this.hideLoading();
|
||||
this.hideMessages();
|
||||
|
||||
// Reset scanner container
|
||||
this.scannerContainer.innerHTML = `
|
||||
<div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
|
||||
<div class="text-center text-white">
|
||||
<div class="loading-spinner mx-auto mb-2"></div>
|
||||
<p class="text-sm">Memuat scanner...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.loadingDiv = document.getElementById('loading-scanner');
|
||||
}
|
||||
|
||||
handleCameraError(error) {
|
||||
if (error.name === 'NotAllowedError') {
|
||||
this.permissionDenied.classList.remove('hidden');
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
this.showError('Kamera tidak ditemukan pada perangkat ini.');
|
||||
} else if (error.name === 'NotReadableError') {
|
||||
this.showError('Kamera sedang digunakan aplikasi lain. Tutup aplikasi lain dan coba lagi.');
|
||||
} else {
|
||||
this.showError('Gagal mengakses kamera: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
showLoading() {
|
||||
this.loadingDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
this.loadingDiv.classList.add('hidden');
|
||||
}
|
||||
|
||||
showOcrProcessing() {
|
||||
this.ocrProcessing.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideOcrProcessing() {
|
||||
this.ocrProcessing.classList.add('hidden');
|
||||
}
|
||||
|
||||
hideMessages() {
|
||||
this.permissionInfo.classList.add('hidden');
|
||||
this.permissionDenied.classList.add('hidden');
|
||||
this.ocrProcessing.classList.add('hidden');
|
||||
this.ocrResult.classList.add('hidden');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
// Create temporary error message
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'bg-red-50 border border-red-200 rounded-lg p-3 mt-2';
|
||||
errorDiv.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
|
||||
<span class="text-red-800 text-sm">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
this.scannerContainer.parentNode.insertBefore(errorDiv, this.scannerContainer.nextSibling);
|
||||
|
||||
setTimeout(() => {
|
||||
errorDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
// Create temporary success message
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'bg-green-50 border border-green-200 rounded-lg p-3 mt-2';
|
||||
successDiv.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
|
||||
<span class="text-green-800 text-sm">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
this.scannerContainer.parentNode.insertBefore(successDiv, this.scannerContainer.nextSibling);
|
||||
|
||||
setTimeout(() => {
|
||||
successDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
playSuccessSound() {
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'square';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.2);
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
|
||||
vibrate() {
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate([200]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the receipt scanner
|
||||
new ReceiptScanner();
|
||||
});
|
||||
</script>
|
||||
</register-block>
|
|
@ -1,4 +1,3 @@
|
|||
/* Scanner specific styles */
|
||||
.scanner-container {
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
|
@ -47,15 +46,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Video element styling */
|
||||
#video-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scaleX(-1); /* Mirror effect for better UX */
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Canvas overlay for QuaggaJS */
|
||||
.drawingBuffer {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
|
@ -63,7 +60,6 @@
|
|||
z-index: 5 !important;
|
||||
}
|
||||
|
||||
/* Button states */
|
||||
.btn-scanner {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
@ -72,7 +68,6 @@
|
|||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Loading animation improvements */
|
||||
.loading-spinner {
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid white;
|
||||
|
@ -82,6 +77,15 @@
|
|||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-spinner-small {
|
||||
border: 2px solid rgba(245, 158, 11, 0.3);
|
||||
border-top: 2px solid #f59e0b;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
@ -91,7 +95,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Result card styling */
|
||||
.scan-result-card {
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
@ -107,7 +110,58 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
/* OCR Processing Animation */
|
||||
.ocr-processing {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* Receipt capture overlay */
|
||||
.receipt-overlay {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
bottom: 10%;
|
||||
border: 2px dashed rgba(245, 158, 11, 0.8);
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.receipt-overlay::before {
|
||||
content: "Arahkan ke teks struk";
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(245, 158, 11, 0.9);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Form transition effects */
|
||||
.form-section {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-section.disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.scanner-overlay {
|
||||
width: 200px;
|
||||
|
@ -117,11 +171,74 @@
|
|||
.scanner-container {
|
||||
height: 250px !important;
|
||||
}
|
||||
|
||||
.receipt-overlay::before {
|
||||
font-size: 11px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.scanner-container {
|
||||
background: #0a0a0a;
|
||||
}
|
||||
}
|
||||
|
||||
#scanner-container video {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#scanner-container canvas {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#scanner-container select,
|
||||
#scanner-container button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Success feedback */
|
||||
.success-flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(34, 197, 94, 0.3);
|
||||
border-radius: 8px;
|
||||
z-index: 15;
|
||||
animation: flash 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Input highlight when auto-filled */
|
||||
.auto-filled {
|
||||
background-color: rgba(34, 197, 94, 0.1) !important;
|
||||
border-color: rgb(34, 197, 94) !important;
|
||||
animation: highlight 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,11 +54,15 @@
|
|||
--color-blue-50: oklch(97% 0.014 254.604);
|
||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||
--color-blue-400: oklch(70.7% 0.165 254.624);
|
||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||
--color-blue-800: oklch(42.4% 0.199 265.638);
|
||||
--color-indigo-50: oklch(96.2% 0.018 272.314);
|
||||
--color-indigo-100: oklch(93% 0.034 272.788);
|
||||
--color-indigo-200: oklch(87% 0.065 274.039);
|
||||
--color-indigo-300: oklch(78.5% 0.115 274.713);
|
||||
--color-purple-50: oklch(97.7% 0.014 308.299);
|
||||
--color-purple-100: oklch(94.6% 0.033 307.174);
|
||||
--color-purple-400: oklch(71.4% 0.203 305.504);
|
||||
|
@ -69,8 +73,10 @@
|
|||
--color-slate-200: oklch(92.9% 0.013 255.508);
|
||||
--color-slate-400: oklch(70.4% 0.04 256.788);
|
||||
--color-slate-500: oklch(55.4% 0.046 257.417);
|
||||
--color-slate-600: oklch(44.6% 0.043 257.281);
|
||||
--color-slate-700: oklch(37.2% 0.044 257.287);
|
||||
--color-slate-800: oklch(27.9% 0.041 260.031);
|
||||
--color-slate-900: oklch(20.8% 0.042 265.755);
|
||||
--color-gray-50: oklch(98.5% 0.002 247.839);
|
||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||
|
@ -84,6 +90,7 @@
|
|||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
--container-xs: 20rem;
|
||||
--container-sm: 24rem;
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
|
@ -95,6 +102,8 @@
|
|||
--text-lg--line-height: calc(1.75 / 1.125);
|
||||
--text-xl: 1.25rem;
|
||||
--text-xl--line-height: calc(1.75 / 1.25);
|
||||
--text-2xl: 1.5rem;
|
||||
--text-2xl--line-height: calc(2 / 1.5);
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
@ -113,7 +122,9 @@
|
|||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--animate-spin: spin 1s linear infinite;
|
||||
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
--animate-bounce: bounce 1s infinite;
|
||||
--blur-sm: 8px;
|
||||
--blur-lg: 16px;
|
||||
--default-transition-duration: 150ms;
|
||||
|
@ -352,6 +363,12 @@
|
|||
.right-4 {
|
||||
right: calc(var(--spacing) * 4);
|
||||
}
|
||||
.right-8 {
|
||||
right: calc(var(--spacing) * 8);
|
||||
}
|
||||
.right-16 {
|
||||
right: calc(var(--spacing) * 16);
|
||||
}
|
||||
.right-full {
|
||||
right: 100%;
|
||||
}
|
||||
|
@ -373,6 +390,9 @@
|
|||
.bottom-8 {
|
||||
bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.bottom-16 {
|
||||
bottom: calc(var(--spacing) * 16);
|
||||
}
|
||||
.bottom-50 {
|
||||
bottom: calc(var(--spacing) * 50);
|
||||
}
|
||||
|
@ -388,6 +408,12 @@
|
|||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
.left-8 {
|
||||
left: calc(var(--spacing) * 8);
|
||||
}
|
||||
.left-12 {
|
||||
left: calc(var(--spacing) * 12);
|
||||
}
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
|
@ -568,6 +594,9 @@
|
|||
.my-5 {
|
||||
margin-block: calc(var(--spacing) * 5);
|
||||
}
|
||||
.my-6 {
|
||||
margin-block: calc(var(--spacing) * 6);
|
||||
}
|
||||
.my-auto {
|
||||
margin-block: auto;
|
||||
}
|
||||
|
@ -637,6 +666,9 @@
|
|||
.mt-1 {
|
||||
margin-top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.mt-1\.5 {
|
||||
margin-top: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
@ -655,6 +687,9 @@
|
|||
.mt-8 {
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: calc(var(--spacing) * 10);
|
||||
}
|
||||
.mt-40 {
|
||||
margin-top: calc(var(--spacing) * 40);
|
||||
}
|
||||
|
@ -688,6 +723,9 @@
|
|||
.mb-6 {
|
||||
margin-bottom: calc(var(--spacing) * 6);
|
||||
}
|
||||
.mb-8 {
|
||||
margin-bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mb-auto {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
@ -748,6 +786,9 @@
|
|||
.h-1 {
|
||||
height: calc(var(--spacing) * 1);
|
||||
}
|
||||
.h-1\.5 {
|
||||
height: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.h-2 {
|
||||
height: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
@ -790,6 +831,9 @@
|
|||
.h-25 {
|
||||
height: calc(var(--spacing) * 25);
|
||||
}
|
||||
.h-28 {
|
||||
height: calc(var(--spacing) * 28);
|
||||
}
|
||||
.h-30 {
|
||||
height: calc(var(--spacing) * 30);
|
||||
}
|
||||
|
@ -820,6 +864,12 @@
|
|||
.w-0 {
|
||||
width: calc(var(--spacing) * 0);
|
||||
}
|
||||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\.5 {
|
||||
width: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.w-2 {
|
||||
width: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
@ -865,6 +915,9 @@
|
|||
.w-25 {
|
||||
width: calc(var(--spacing) * 25);
|
||||
}
|
||||
.w-28 {
|
||||
width: calc(var(--spacing) * 28);
|
||||
}
|
||||
.w-32 {
|
||||
width: calc(var(--spacing) * 32);
|
||||
}
|
||||
|
@ -892,6 +945,9 @@
|
|||
.max-w-sm {
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
.max-w-xs {
|
||||
max-width: var(--container-xs);
|
||||
}
|
||||
.min-w-0 {
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
@ -940,6 +996,10 @@
|
|||
--tw-translate-x: calc(var(--spacing) * -12);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.translate-x-10 {
|
||||
--tw-translate-x: calc(var(--spacing) * 10);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.translate-x-16 {
|
||||
--tw-translate-x: calc(var(--spacing) * 16);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
|
@ -952,6 +1012,10 @@
|
|||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-10 {
|
||||
--tw-translate-y: calc(var(--spacing) * -10);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-16 {
|
||||
--tw-translate-y: calc(var(--spacing) * -16);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
|
@ -969,6 +1033,12 @@
|
|||
.transform {
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.animate-bounce {
|
||||
animation: var(--animate-bounce);
|
||||
}
|
||||
.animate-ping {
|
||||
animation: var(--animate-ping);
|
||||
}
|
||||
.animate-pulse {
|
||||
animation: var(--animate-pulse);
|
||||
}
|
||||
|
@ -1145,6 +1215,9 @@
|
|||
.rounded-2xl {
|
||||
border-radius: var(--radius-2xl);
|
||||
}
|
||||
.rounded-3xl {
|
||||
border-radius: var(--radius-3xl);
|
||||
}
|
||||
.rounded-full {
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
|
@ -1302,6 +1375,12 @@
|
|||
.border-white {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
.border-white\/30 {
|
||||
border-color: color-mix(in srgb, #fff 30%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-color: color-mix(in oklab, var(--color-white) 30%, transparent);
|
||||
}
|
||||
}
|
||||
.border-yellow-200 {
|
||||
border-color: var(--color-yellow-200);
|
||||
}
|
||||
|
@ -1323,6 +1402,9 @@
|
|||
.bg-blue-100 {
|
||||
background-color: var(--color-blue-100);
|
||||
}
|
||||
.bg-blue-400 {
|
||||
background-color: var(--color-blue-400);
|
||||
}
|
||||
.bg-blue-500 {
|
||||
background-color: var(--color-blue-500);
|
||||
}
|
||||
|
@ -1362,12 +1444,18 @@
|
|||
.bg-green-600 {
|
||||
background-color: var(--color-green-600);
|
||||
}
|
||||
.bg-indigo-300 {
|
||||
background-color: var(--color-indigo-300);
|
||||
}
|
||||
.bg-orange-50 {
|
||||
background-color: var(--color-orange-50);
|
||||
}
|
||||
.bg-orange-100 {
|
||||
background-color: var(--color-orange-100);
|
||||
}
|
||||
.bg-orange-300 {
|
||||
background-color: var(--color-orange-300);
|
||||
}
|
||||
.bg-orange-400 {
|
||||
background-color: var(--color-orange-400);
|
||||
}
|
||||
|
@ -1389,6 +1477,9 @@
|
|||
.bg-slate-100 {
|
||||
background-color: var(--color-slate-100);
|
||||
}
|
||||
.bg-slate-400 {
|
||||
background-color: var(--color-slate-400);
|
||||
}
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
@ -1407,6 +1498,12 @@
|
|||
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/70 {
|
||||
background-color: color-mix(in srgb, #fff 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-yellow-50 {
|
||||
background-color: var(--color-yellow-50);
|
||||
}
|
||||
|
@ -1439,6 +1536,10 @@
|
|||
--tw-gradient-from: var(--color-blue-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.from-blue-500 {
|
||||
--tw-gradient-from: var(--color-blue-500);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.from-blue-600 {
|
||||
--tw-gradient-from: var(--color-blue-600);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
|
@ -1491,6 +1592,15 @@
|
|||
--tw-gradient-from: var(--color-slate-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.from-slate-700 {
|
||||
--tw-gradient-from: var(--color-slate-700);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.via-blue-50 {
|
||||
--tw-gradient-via: var(--color-blue-50);
|
||||
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops);
|
||||
}
|
||||
.via-orange-400 {
|
||||
--tw-gradient-via: var(--color-orange-400);
|
||||
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
|
||||
|
@ -1505,6 +1615,10 @@
|
|||
--tw-gradient-to: var(--color-blue-200);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-blue-600 {
|
||||
--tw-gradient-to: var(--color-blue-600);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-emerald-600 {
|
||||
--tw-gradient-to: var(--color-emerald-600);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
|
@ -1521,6 +1635,10 @@
|
|||
--tw-gradient-to: var(--color-indigo-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-indigo-100 {
|
||||
--tw-gradient-to: var(--color-indigo-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-orange-50 {
|
||||
--tw-gradient-to: var(--color-orange-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
|
@ -1577,6 +1695,14 @@
|
|||
--tw-gradient-to: var(--color-slate-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-slate-200 {
|
||||
--tw-gradient-to: var(--color-slate-200);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-slate-900 {
|
||||
--tw-gradient-to: var(--color-slate-900);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-teal-50 {
|
||||
--tw-gradient-to: var(--color-teal-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
|
@ -1585,6 +1711,9 @@
|
|||
--tw-gradient-to: var(--color-yellow-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.bg-clip-text {
|
||||
background-clip: text;
|
||||
}
|
||||
.object-contain {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
@ -1660,6 +1789,9 @@
|
|||
.py-8 {
|
||||
padding-block: calc(var(--spacing) * 8);
|
||||
}
|
||||
.py-12 {
|
||||
padding-block: calc(var(--spacing) * 12);
|
||||
}
|
||||
.py-16 {
|
||||
padding-block: calc(var(--spacing) * 16);
|
||||
}
|
||||
|
@ -1783,6 +1915,10 @@
|
|||
.font-mono {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.text-2xl {
|
||||
font-size: var(--text-2xl);
|
||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||
}
|
||||
.text-base {
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--tw-leading, var(--text-base--line-height));
|
||||
|
@ -1935,12 +2071,18 @@
|
|||
.text-slate-500 {
|
||||
color: var(--color-slate-500);
|
||||
}
|
||||
.text-slate-600 {
|
||||
color: var(--color-slate-600);
|
||||
}
|
||||
.text-slate-700 {
|
||||
color: var(--color-slate-700);
|
||||
}
|
||||
.text-slate-800 {
|
||||
color: var(--color-slate-800);
|
||||
}
|
||||
.text-transparent {
|
||||
color: transparent;
|
||||
}
|
||||
.text-white {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
@ -1977,9 +2119,15 @@
|
|||
.opacity-30 {
|
||||
opacity: 30%;
|
||||
}
|
||||
.opacity-40 {
|
||||
opacity: 40%;
|
||||
}
|
||||
.opacity-50 {
|
||||
opacity: 50%;
|
||||
}
|
||||
.opacity-60 {
|
||||
opacity: 60%;
|
||||
}
|
||||
.opacity-75 {
|
||||
opacity: 75%;
|
||||
}
|
||||
|
@ -1996,6 +2144,10 @@
|
|||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-2xl {
|
||||
--tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
|
@ -2016,6 +2168,10 @@
|
|||
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.ring-2 {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
|
@ -2092,6 +2248,16 @@
|
|||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.transition-shadow {
|
||||
transition-property: box-shadow;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.transition-transform {
|
||||
transition-property: transform, translate, scale, rotate;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.duration-150 {
|
||||
--tw-duration: 150ms;
|
||||
transition-duration: 150ms;
|
||||
|
@ -2104,6 +2270,10 @@
|
|||
--tw-duration: 300ms;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
.duration-500 {
|
||||
--tw-duration: 500ms;
|
||||
transition-duration: 500ms;
|
||||
}
|
||||
.ease-in-out {
|
||||
--tw-ease: var(--ease-in-out);
|
||||
transition-timing-function: var(--ease-in-out);
|
||||
|
@ -2127,6 +2297,59 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:rotate-180 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:from-blue-100 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-from: var(--color-blue-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:from-orange-100 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-from: var(--color-orange-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:to-indigo-200 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-to: var(--color-indigo-200);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:to-orange-200 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-to: var(--color-orange-200);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:text-blue-600 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-blue-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:text-blue-700 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:text-orange-500 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
|
@ -2134,6 +2357,20 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:text-orange-600 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-orange-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:text-orange-700 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-orange-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.group-hover\:opacity-100 {
|
||||
&:is(:where(.group):hover *) {
|
||||
@media (hover: hover) {
|
||||
|
@ -2175,6 +2412,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-blue-200 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
border-color: var(--color-blue-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-orange-200 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -2259,6 +2503,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-slate-50 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-slate-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-white\/10 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -2279,6 +2530,30 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-gradient-to-br {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-position: to bottom right in oklab;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:from-blue-50 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-from: var(--color-blue-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:from-orange-50 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-from: var(--color-orange-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:from-orange-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -2287,6 +2562,22 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:to-indigo-100 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-to: var(--color-indigo-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:to-orange-100 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-gradient-to: var(--color-orange-100);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:to-orange-500 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -2346,6 +2637,22 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:shadow-md {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:shadow-xl {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
.focus\:border-orange-500 {
|
||||
&:focus {
|
||||
border-color: var(--color-orange-500);
|
||||
|
@ -2680,11 +2987,27 @@
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(-25%);
|
||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||
}
|
||||
50% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
|
|
Loading…
Reference in New Issue