update: tester submit struk
parent
b701c86656
commit
a4b5c45312
|
@ -4,26 +4,26 @@ using eSPJ.Models;
|
||||||
|
|
||||||
namespace eSPJ.Controllers.SpjDriverController;
|
namespace eSPJ.Controllers.SpjDriverController;
|
||||||
|
|
||||||
|
[Route("")]
|
||||||
public class HomeController : Controller
|
public class HomeController : Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
private readonly ILogger<HomeController> _logger;
|
private readonly ILogger<HomeController> _logger;
|
||||||
|
|
||||||
public HomeController(ILogger<HomeController> logger)
|
public HomeController(ILogger<HomeController> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
[HttpGet("")]
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
return View("~/Views/Admin/Transport/SpjDriver/Home/Index.cshtml");
|
return View("~/Views/Admin/Transport/SpjDriver/Home/Index.cshtml");
|
||||||
}
|
}
|
||||||
|
[HttpGet("kosong")]
|
||||||
public IActionResult Privacy()
|
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)]
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
public IActionResult Error()
|
public IActionResult Error()
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,7 +17,7 @@ namespace eSPJ.Controllers.SpjDriverController
|
||||||
{
|
{
|
||||||
return View("~/Views/Admin/Transport/SpjDriver/Scan/Detail.cshtml");
|
return View("~/Views/Admin/Transport/SpjDriver/Scan/Detail.cshtml");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("create")]
|
[HttpGet("create")]
|
||||||
public IActionResult Create()
|
public IActionResult Create()
|
||||||
{
|
{
|
||||||
|
@ -29,30 +29,19 @@ namespace eSPJ.Controllers.SpjDriverController
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Validate barcode
|
|
||||||
if (string.IsNullOrEmpty(barcode))
|
if (string.IsNullOrEmpty(barcode))
|
||||||
{
|
{
|
||||||
TempData["Error"] = "Kode barcode tidak boleh kosong.";
|
TempData["Error"] = "Kode barcode tidak boleh kosong.";
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic validation for SPJ barcode format
|
|
||||||
if (barcode.Length < 5)
|
if (barcode.Length < 5)
|
||||||
{
|
{
|
||||||
TempData["Error"] = "Format kode SPJ tidak valid. Minimal 5 karakter.";
|
TempData["Error"] = "Format kode SPJ tidak valid. Minimal 5 karakter.";
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean the barcode (remove any whitespace)
|
|
||||||
barcode = barcode.Trim();
|
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);
|
var spjData = await ValidateSpjCode(barcode);
|
||||||
|
|
||||||
if (spjData == null)
|
if (spjData == null)
|
||||||
|
@ -61,20 +50,13 @@ namespace eSPJ.Controllers.SpjDriverController
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success - redirect to detail page or next step
|
|
||||||
TempData["Success"] = $"SPJ '{barcode}' berhasil ditemukan!";
|
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 });
|
return RedirectToAction("Index", "Detail", new { spjCode = barcode });
|
||||||
|
|
||||||
// Or redirect to submission page:
|
|
||||||
// return RedirectToAction("Index", "Submit", new { spjCode = barcode });
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// Log the error (add your logging here)
|
|
||||||
TempData["Error"] = "Terjadi kesalahan saat memproses scan. Silakan coba lagi.";
|
TempData["Error"] = "Terjadi kesalahan saat memproses scan. Silakan coba lagi.";
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
@ -82,20 +64,10 @@ namespace eSPJ.Controllers.SpjDriverController
|
||||||
|
|
||||||
private async Task<SpjData?> ValidateSpjCode(string barcode)
|
private async Task<SpjData?> ValidateSpjCode(string barcode)
|
||||||
{
|
{
|
||||||
// TODO: Implement your SPJ validation logic here
|
|
||||||
// This is just a sample implementation
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Simulate database lookup
|
await Task.Delay(100);
|
||||||
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"
|
|
||||||
if (barcode.ToUpper().StartsWith("SPJ"))
|
if (barcode.ToUpper().StartsWith("SPJ"))
|
||||||
{
|
{
|
||||||
return new SpjData
|
return new SpjData
|
||||||
|
@ -107,7 +79,6 @@ namespace eSPJ.Controllers.SpjDriverController
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return null if not found or invalid
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
@ -116,7 +87,6 @@ namespace eSPJ.Controllers.SpjDriverController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sample model for SPJ data (replace with your actual model)
|
|
||||||
public class SpjData
|
public class SpjData
|
||||||
{
|
{
|
||||||
public string Code { get; set; } = string.Empty;
|
public string Code { get; set; } = string.Empty;
|
||||||
|
|
|
@ -18,5 +18,43 @@ namespace eSPJ.Controllers.SpjDriverController
|
||||||
{
|
{
|
||||||
return View("~/Views/Admin/Transport/SpjDriver/Submit/Struk.cshtml");
|
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 {
|
@section Styles {
|
||||||
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scanner Section -->
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<!-- Alert Messages -->
|
|
||||||
@if (TempData["Success"] != null)
|
@if (TempData["Success"] != null)
|
||||||
{
|
{
|
||||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
@ -66,12 +40,9 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Camera Preview -->
|
|
||||||
<div class="scanner-container mb-4" style="height: 300px;">
|
<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="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 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="text-center text-white">
|
||||||
<div class="loading-spinner mx-auto mb-2"></div>
|
<div class="loading-spinner mx-auto mb-2"></div>
|
||||||
|
@ -81,7 +52,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scanner Controls -->
|
|
||||||
<div class="space-y-3 mb-4">
|
<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">
|
<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>
|
<i class="w-5 h-5 inline mr-2" data-lucide="camera"></i>
|
||||||
|
@ -93,7 +63,6 @@
|
||||||
Hentikan Scan
|
Hentikan Scan
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Camera Permission Info -->
|
|
||||||
<div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
|
<div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
|
<i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
|
||||||
|
@ -109,7 +78,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Permission Denied Info -->
|
|
||||||
<div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
|
<div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scanning Tips -->
|
|
||||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
|
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
|
||||||
|
@ -143,7 +110,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Manual Input Alternative -->
|
|
||||||
<div class="border-t pt-4">
|
<div class="border-t pt-4">
|
||||||
<h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3>
|
<h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3>
|
||||||
<form id="manual-form" method="post" action="@Url.Action("ProcessScan", "Scan")">
|
<form id="manual-form" method="post" action="@Url.Action("ProcessScan", "Scan")">
|
||||||
|
@ -160,7 +126,6 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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 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">
|
<div class="flex items-center mb-2">
|
||||||
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
|
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
|
||||||
|
@ -177,7 +142,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Message -->
|
|
||||||
<div id="error-message" class="hidden mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
<div id="error-message" class="hidden mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
|
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
|
||||||
|
@ -185,16 +149,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_NavigationAdmin.cshtml" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<register-block dynamic-section="scripts" key="jsScan">
|
<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>
|
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script>
|
||||||
|
|
||||||
<!-- Fallback script loader -->
|
|
||||||
<script>
|
<script>
|
||||||
// Check if library loaded, if not try alternative CDN
|
|
||||||
if (typeof Html5Qrcode === 'undefined') {
|
if (typeof Html5Qrcode === 'undefined') {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js';
|
script.src = 'https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js';
|
||||||
|
@ -238,7 +199,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
checkBrowserSupport() {
|
checkBrowserSupport() {
|
||||||
// Check if browser supports getUserMedia
|
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
this.startBtn.disabled = true;
|
this.startBtn.disabled = true;
|
||||||
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Browser Tidak Didukung';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Html5Qrcode is loaded
|
|
||||||
if (typeof Html5Qrcode === 'undefined') {
|
if (typeof Html5Qrcode === 'undefined') {
|
||||||
this.startBtn.disabled = true;
|
this.startBtn.disabled = true;
|
||||||
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Library Tidak Dimuat';
|
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;
|
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') {
|
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.');
|
this.showError('Scanner barcode memerlukan koneksi HTTPS yang aman. Hubungi administrator sistem.');
|
||||||
}
|
}
|
||||||
|
@ -271,7 +229,6 @@
|
||||||
this.hideResult();
|
this.hideResult();
|
||||||
this.hidePermissionMessages();
|
this.hidePermissionMessages();
|
||||||
|
|
||||||
// Initialize Html5-QRCode scanner
|
|
||||||
await this.initializeHtml5QrCode();
|
await this.initializeHtml5QrCode();
|
||||||
|
|
||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
|
@ -287,23 +244,17 @@
|
||||||
|
|
||||||
async initializeHtml5QrCode() {
|
async initializeHtml5QrCode() {
|
||||||
try {
|
try {
|
||||||
// Show permission info
|
|
||||||
this.permissionInfo.classList.remove('hidden');
|
this.permissionInfo.classList.remove('hidden');
|
||||||
|
|
||||||
// Wait a moment to show the message
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
// Initialize Html5-QRCode
|
|
||||||
this.html5QrCode = new Html5Qrcode("scanner-container");
|
this.html5QrCode = new Html5Qrcode("scanner-container");
|
||||||
|
|
||||||
// Get available cameras - This will trigger permission request
|
|
||||||
const cameras = await Html5Qrcode.getCameras();
|
const cameras = await Html5Qrcode.getCameras();
|
||||||
|
|
||||||
if (cameras && cameras.length > 0) {
|
if (cameras && cameras.length > 0) {
|
||||||
// Try to use back camera first, fallback to first available
|
|
||||||
let cameraId = cameras[0].id;
|
let cameraId = cameras[0].id;
|
||||||
|
|
||||||
// Look for back camera
|
|
||||||
const backCamera = cameras.find(camera =>
|
const backCamera = cameras.find(camera =>
|
||||||
camera.label.toLowerCase().includes('back') ||
|
camera.label.toLowerCase().includes('back') ||
|
||||||
camera.label.toLowerCase().includes('rear') ||
|
camera.label.toLowerCase().includes('rear') ||
|
||||||
|
@ -314,14 +265,12 @@
|
||||||
cameraId = backCamera.id;
|
cameraId = backCamera.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start scanning with square QR code focus
|
|
||||||
await this.html5QrCode.start(
|
await this.html5QrCode.start(
|
||||||
cameraId,
|
cameraId,
|
||||||
{
|
{
|
||||||
fps: 10, // Frame per second
|
fps: 10,
|
||||||
qrbox: function(viewfinderWidth, viewfinderHeight) {
|
qrbox: function(viewfinderWidth, viewfinderHeight) {
|
||||||
// Make it square - use the smaller dimension
|
let minEdgePercentage = 0.7;
|
||||||
let minEdgePercentage = 0.7; // 70% of the smaller edge
|
|
||||||
let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight);
|
let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight);
|
||||||
let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage);
|
let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage);
|
||||||
return {
|
return {
|
||||||
|
@ -329,15 +278,13 @@
|
||||||
height: qrboxSize
|
height: qrboxSize
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
aspectRatio: 1.0, // Square ratio for QR codes
|
aspectRatio: 1.0,
|
||||||
rememberLastUsedCamera: true
|
rememberLastUsedCamera: true
|
||||||
},
|
},
|
||||||
(decodedText, decodedResult) => {
|
(decodedText, decodedResult) => {
|
||||||
// Success callback
|
|
||||||
this.handleBarcodeDetected(decodedText, decodedResult);
|
this.handleBarcodeDetected(decodedText, decodedResult);
|
||||||
},
|
},
|
||||||
(errorMessage) => {
|
(errorMessage) => {
|
||||||
// Error callback (optional, can be ignored for scanning errors)
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -377,19 +324,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBarcodeDetected(decodedText, decodedResult) {
|
handleBarcodeDetected(decodedText, decodedResult) {
|
||||||
// Validate the code (basic validation)
|
|
||||||
if (decodedText && decodedText.length >= 5) {
|
if (decodedText && decodedText.length >= 5) {
|
||||||
// Add visual feedback
|
|
||||||
this.flashSuccess();
|
this.flashSuccess();
|
||||||
|
|
||||||
this.detectedCode = decodedText;
|
this.detectedCode = decodedText;
|
||||||
this.showResult(decodedText);
|
this.showResult(decodedText);
|
||||||
this.stopScanner(); // Auto stop after detection
|
this.stopScanner();
|
||||||
|
|
||||||
// Play a success sound
|
|
||||||
this.playSuccessSound();
|
this.playSuccessSound();
|
||||||
|
|
||||||
// Haptic feedback if supported
|
|
||||||
this.vibrate();
|
this.vibrate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -399,7 +341,6 @@
|
||||||
try {
|
try {
|
||||||
await this.html5QrCode.stop();
|
await this.html5QrCode.stop();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error stopping scanner
|
|
||||||
}
|
}
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
}
|
}
|
||||||
|
@ -409,7 +350,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
flashSuccess() {
|
flashSuccess() {
|
||||||
// Add a green flash overlay to indicate successful scan
|
|
||||||
const flash = document.createElement('div');
|
const flash = document.createElement('div');
|
||||||
flash.className = 'absolute inset-0 bg-green-500 opacity-50 rounded-lg';
|
flash.className = 'absolute inset-0 bg-green-500 opacity-50 rounded-lg';
|
||||||
flash.style.zIndex = '20';
|
flash.style.zIndex = '20';
|
||||||
|
@ -421,15 +361,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
vibrate() {
|
vibrate() {
|
||||||
// Provide haptic feedback on mobile devices
|
|
||||||
if ('vibrate' in navigator) {
|
if ('vibrate' in navigator) {
|
||||||
navigator.vibrate([200]); // Vibrate for 200ms
|
navigator.vibrate([200]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmScan() {
|
confirmScan() {
|
||||||
if (this.detectedCode) {
|
if (this.detectedCode) {
|
||||||
// Auto-fill manual input and submit
|
|
||||||
this.manualInput.value = this.detectedCode;
|
this.manualInput.value = this.detectedCode;
|
||||||
this.manualForm.submit();
|
this.manualForm.submit();
|
||||||
}
|
}
|
||||||
|
@ -441,12 +379,10 @@
|
||||||
this.hidePermissionMessages();
|
this.hidePermissionMessages();
|
||||||
this.detectedCode = null;
|
this.detectedCode = null;
|
||||||
|
|
||||||
// Stop current scanner if running
|
|
||||||
if (this.isScanning && this.html5QrCode) {
|
if (this.isScanning && this.html5QrCode) {
|
||||||
await this.stopScanner();
|
await this.stopScanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a bit before restarting
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.startScanner();
|
this.startScanner();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
@ -499,7 +435,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
playSuccessSound() {
|
playSuccessSound() {
|
||||||
// Create a simple beep sound
|
|
||||||
try {
|
try {
|
||||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
const oscillator = audioContext.createOscillator();
|
const oscillator = audioContext.createOscillator();
|
||||||
|
@ -517,14 +452,11 @@
|
||||||
oscillator.start(audioContext.currentTime);
|
oscillator.start(audioContext.currentTime);
|
||||||
oscillator.stop(audioContext.currentTime + 0.2);
|
oscillator.stop(audioContext.currentTime + 0.2);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore audio errors
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize scanner when page loads
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Function to check if Html5Qrcode is loaded
|
|
||||||
function waitForLibrary() {
|
function waitForLibrary() {
|
||||||
if (typeof Html5Qrcode !== 'undefined') {
|
if (typeof Html5Qrcode !== 'undefined') {
|
||||||
new BarcodeScanner();
|
new BarcodeScanner();
|
||||||
|
@ -533,7 +465,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start checking
|
|
||||||
waitForLibrary();
|
waitForLibrary();
|
||||||
});
|
});
|
||||||
</script>
|
</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";
|
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">
|
<div class="max-w-sm mx-auto bg-white min-h-screen">
|
||||||
<!-- Header with Orange Background -->
|
<!-- Header with Orange Background -->
|
||||||
|
@ -16,13 +19,134 @@
|
||||||
</div>
|
</div>
|
||||||
</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="flex flex-col items-center space-y-2">
|
||||||
<div class="bg-orange-100 rounded-full p-3">
|
<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>
|
</div>
|
||||||
<h2 class="text-xl font-bold text-orange-500">Isi Data Struk</h2>
|
<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 dengan benar.</p>
|
<p class="text-sm text-gray-500 text-center">Masukkan nomor struk dan berat muatan secara manual.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -65,11 +189,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<register-block dynamic-section="scripts" key="jsSubmitStruk">
|
<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>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const nomorStrukInput = document.getElementById('NomorStruk');
|
const nomorStrukInput = document.getElementById('NomorStruk');
|
||||||
const beratMuatanInput = document.getElementById('BeratMuatan');
|
const beratMuatanInput = document.getElementById('BeratMuatan');
|
||||||
|
|
||||||
|
// Input validation for manual entry
|
||||||
nomorStrukInput.addEventListener('input', function() {
|
nomorStrukInput.addEventListener('input', function() {
|
||||||
this.value = this.value.replace(/[^0-9]/g, '');
|
this.value = this.value.replace(/[^0-9]/g, '');
|
||||||
});
|
});
|
||||||
|
@ -77,6 +203,396 @@
|
||||||
beratMuatanInput.addEventListener('input', function() {
|
beratMuatanInput.addEventListener('input', function() {
|
||||||
this.value = this.value.replace(/[^0-9.]/g, '');
|
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>
|
</script>
|
||||||
</register-block>
|
</register-block>
|
|
@ -1,4 +1,3 @@
|
||||||
/* Scanner specific styles */
|
|
||||||
.scanner-container {
|
.scanner-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
|
@ -47,15 +46,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Video element styling */
|
|
||||||
#video-preview {
|
#video-preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
transform: scaleX(-1); /* Mirror effect for better UX */
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Canvas overlay for QuaggaJS */
|
|
||||||
.drawingBuffer {
|
.drawingBuffer {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
|
@ -63,7 +60,6 @@
|
||||||
z-index: 5 !important;
|
z-index: 5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button states */
|
|
||||||
.btn-scanner {
|
.btn-scanner {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +68,6 @@
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading animation improvements */
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
border-top: 2px solid white;
|
border-top: 2px solid white;
|
||||||
|
@ -82,6 +77,15 @@
|
||||||
animation: spin 1s linear infinite;
|
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 {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
@ -91,7 +95,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Result card styling */
|
|
||||||
.scan-result-card {
|
.scan-result-card {
|
||||||
animation: slideInUp 0.3s ease-out;
|
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) {
|
@media (max-width: 640px) {
|
||||||
.scanner-overlay {
|
.scanner-overlay {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
@ -117,11 +171,74 @@
|
||||||
.scanner-container {
|
.scanner-container {
|
||||||
height: 250px !important;
|
height: 250px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receipt-overlay::before {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode support */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.scanner-container {
|
.scanner-container {
|
||||||
background: #0a0a0a;
|
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-50: oklch(97% 0.014 254.604);
|
||||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
--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-500: oklch(62.3% 0.214 259.815);
|
||||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||||
--color-blue-800: oklch(42.4% 0.199 265.638);
|
--color-blue-800: oklch(42.4% 0.199 265.638);
|
||||||
--color-indigo-50: oklch(96.2% 0.018 272.314);
|
--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-50: oklch(97.7% 0.014 308.299);
|
||||||
--color-purple-100: oklch(94.6% 0.033 307.174);
|
--color-purple-100: oklch(94.6% 0.033 307.174);
|
||||||
--color-purple-400: oklch(71.4% 0.203 305.504);
|
--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-200: oklch(92.9% 0.013 255.508);
|
||||||
--color-slate-400: oklch(70.4% 0.04 256.788);
|
--color-slate-400: oklch(70.4% 0.04 256.788);
|
||||||
--color-slate-500: oklch(55.4% 0.046 257.417);
|
--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-700: oklch(37.2% 0.044 257.287);
|
||||||
--color-slate-800: oklch(27.9% 0.041 260.031);
|
--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-50: oklch(98.5% 0.002 247.839);
|
||||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||||
|
@ -84,6 +90,7 @@
|
||||||
--color-black: #000;
|
--color-black: #000;
|
||||||
--color-white: #fff;
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
|
--container-xs: 20rem;
|
||||||
--container-sm: 24rem;
|
--container-sm: 24rem;
|
||||||
--text-xs: 0.75rem;
|
--text-xs: 0.75rem;
|
||||||
--text-xs--line-height: calc(1 / 0.75);
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
|
@ -95,6 +102,8 @@
|
||||||
--text-lg--line-height: calc(1.75 / 1.125);
|
--text-lg--line-height: calc(1.75 / 1.125);
|
||||||
--text-xl: 1.25rem;
|
--text-xl: 1.25rem;
|
||||||
--text-xl--line-height: calc(1.75 / 1.25);
|
--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-medium: 500;
|
||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
|
@ -113,7 +122,9 @@
|
||||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--animate-spin: spin 1s linear infinite;
|
--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-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
--animate-bounce: bounce 1s infinite;
|
||||||
--blur-sm: 8px;
|
--blur-sm: 8px;
|
||||||
--blur-lg: 16px;
|
--blur-lg: 16px;
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
|
@ -352,6 +363,12 @@
|
||||||
.right-4 {
|
.right-4 {
|
||||||
right: calc(var(--spacing) * 4);
|
right: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.right-8 {
|
||||||
|
right: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.right-16 {
|
||||||
|
right: calc(var(--spacing) * 16);
|
||||||
|
}
|
||||||
.right-full {
|
.right-full {
|
||||||
right: 100%;
|
right: 100%;
|
||||||
}
|
}
|
||||||
|
@ -373,6 +390,9 @@
|
||||||
.bottom-8 {
|
.bottom-8 {
|
||||||
bottom: calc(var(--spacing) * 8);
|
bottom: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
.bottom-16 {
|
||||||
|
bottom: calc(var(--spacing) * 16);
|
||||||
|
}
|
||||||
.bottom-50 {
|
.bottom-50 {
|
||||||
bottom: calc(var(--spacing) * 50);
|
bottom: calc(var(--spacing) * 50);
|
||||||
}
|
}
|
||||||
|
@ -388,6 +408,12 @@
|
||||||
.left-1\/2 {
|
.left-1\/2 {
|
||||||
left: calc(1/2 * 100%);
|
left: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
|
.left-8 {
|
||||||
|
left: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.left-12 {
|
||||||
|
left: calc(var(--spacing) * 12);
|
||||||
|
}
|
||||||
.z-0 {
|
.z-0 {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
@ -568,6 +594,9 @@
|
||||||
.my-5 {
|
.my-5 {
|
||||||
margin-block: calc(var(--spacing) * 5);
|
margin-block: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.my-6 {
|
||||||
|
margin-block: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.my-auto {
|
.my-auto {
|
||||||
margin-block: auto;
|
margin-block: auto;
|
||||||
}
|
}
|
||||||
|
@ -637,6 +666,9 @@
|
||||||
.mt-1 {
|
.mt-1 {
|
||||||
margin-top: calc(var(--spacing) * 1);
|
margin-top: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.mt-1\.5 {
|
||||||
|
margin-top: calc(var(--spacing) * 1.5);
|
||||||
|
}
|
||||||
.mt-2 {
|
.mt-2 {
|
||||||
margin-top: calc(var(--spacing) * 2);
|
margin-top: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
@ -655,6 +687,9 @@
|
||||||
.mt-8 {
|
.mt-8 {
|
||||||
margin-top: calc(var(--spacing) * 8);
|
margin-top: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
.mt-10 {
|
||||||
|
margin-top: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
.mt-40 {
|
.mt-40 {
|
||||||
margin-top: calc(var(--spacing) * 40);
|
margin-top: calc(var(--spacing) * 40);
|
||||||
}
|
}
|
||||||
|
@ -688,6 +723,9 @@
|
||||||
.mb-6 {
|
.mb-6 {
|
||||||
margin-bottom: calc(var(--spacing) * 6);
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.mb-8 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.mb-auto {
|
.mb-auto {
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
@ -748,6 +786,9 @@
|
||||||
.h-1 {
|
.h-1 {
|
||||||
height: calc(var(--spacing) * 1);
|
height: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.h-1\.5 {
|
||||||
|
height: calc(var(--spacing) * 1.5);
|
||||||
|
}
|
||||||
.h-2 {
|
.h-2 {
|
||||||
height: calc(var(--spacing) * 2);
|
height: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
@ -790,6 +831,9 @@
|
||||||
.h-25 {
|
.h-25 {
|
||||||
height: calc(var(--spacing) * 25);
|
height: calc(var(--spacing) * 25);
|
||||||
}
|
}
|
||||||
|
.h-28 {
|
||||||
|
height: calc(var(--spacing) * 28);
|
||||||
|
}
|
||||||
.h-30 {
|
.h-30 {
|
||||||
height: calc(var(--spacing) * 30);
|
height: calc(var(--spacing) * 30);
|
||||||
}
|
}
|
||||||
|
@ -820,6 +864,12 @@
|
||||||
.w-0 {
|
.w-0 {
|
||||||
width: calc(var(--spacing) * 0);
|
width: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
.w-1 {
|
||||||
|
width: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
|
.w-1\.5 {
|
||||||
|
width: calc(var(--spacing) * 1.5);
|
||||||
|
}
|
||||||
.w-2 {
|
.w-2 {
|
||||||
width: calc(var(--spacing) * 2);
|
width: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
@ -865,6 +915,9 @@
|
||||||
.w-25 {
|
.w-25 {
|
||||||
width: calc(var(--spacing) * 25);
|
width: calc(var(--spacing) * 25);
|
||||||
}
|
}
|
||||||
|
.w-28 {
|
||||||
|
width: calc(var(--spacing) * 28);
|
||||||
|
}
|
||||||
.w-32 {
|
.w-32 {
|
||||||
width: calc(var(--spacing) * 32);
|
width: calc(var(--spacing) * 32);
|
||||||
}
|
}
|
||||||
|
@ -892,6 +945,9 @@
|
||||||
.max-w-sm {
|
.max-w-sm {
|
||||||
max-width: var(--container-sm);
|
max-width: var(--container-sm);
|
||||||
}
|
}
|
||||||
|
.max-w-xs {
|
||||||
|
max-width: var(--container-xs);
|
||||||
|
}
|
||||||
.min-w-0 {
|
.min-w-0 {
|
||||||
min-width: calc(var(--spacing) * 0);
|
min-width: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
@ -940,6 +996,10 @@
|
||||||
--tw-translate-x: calc(var(--spacing) * -12);
|
--tw-translate-x: calc(var(--spacing) * -12);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
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 {
|
.translate-x-16 {
|
||||||
--tw-translate-x: calc(var(--spacing) * 16);
|
--tw-translate-x: calc(var(--spacing) * 16);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
@ -952,6 +1012,10 @@
|
||||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
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 {
|
.-translate-y-16 {
|
||||||
--tw-translate-y: calc(var(--spacing) * -16);
|
--tw-translate-y: calc(var(--spacing) * -16);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
@ -969,6 +1033,12 @@
|
||||||
.transform {
|
.transform {
|
||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
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 {
|
.animate-pulse {
|
||||||
animation: var(--animate-pulse);
|
animation: var(--animate-pulse);
|
||||||
}
|
}
|
||||||
|
@ -1145,6 +1215,9 @@
|
||||||
.rounded-2xl {
|
.rounded-2xl {
|
||||||
border-radius: var(--radius-2xl);
|
border-radius: var(--radius-2xl);
|
||||||
}
|
}
|
||||||
|
.rounded-3xl {
|
||||||
|
border-radius: var(--radius-3xl);
|
||||||
|
}
|
||||||
.rounded-full {
|
.rounded-full {
|
||||||
border-radius: calc(infinity * 1px);
|
border-radius: calc(infinity * 1px);
|
||||||
}
|
}
|
||||||
|
@ -1302,6 +1375,12 @@
|
||||||
.border-white {
|
.border-white {
|
||||||
border-color: var(--color-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-yellow-200 {
|
||||||
border-color: var(--color-yellow-200);
|
border-color: var(--color-yellow-200);
|
||||||
}
|
}
|
||||||
|
@ -1323,6 +1402,9 @@
|
||||||
.bg-blue-100 {
|
.bg-blue-100 {
|
||||||
background-color: var(--color-blue-100);
|
background-color: var(--color-blue-100);
|
||||||
}
|
}
|
||||||
|
.bg-blue-400 {
|
||||||
|
background-color: var(--color-blue-400);
|
||||||
|
}
|
||||||
.bg-blue-500 {
|
.bg-blue-500 {
|
||||||
background-color: var(--color-blue-500);
|
background-color: var(--color-blue-500);
|
||||||
}
|
}
|
||||||
|
@ -1362,12 +1444,18 @@
|
||||||
.bg-green-600 {
|
.bg-green-600 {
|
||||||
background-color: var(--color-green-600);
|
background-color: var(--color-green-600);
|
||||||
}
|
}
|
||||||
|
.bg-indigo-300 {
|
||||||
|
background-color: var(--color-indigo-300);
|
||||||
|
}
|
||||||
.bg-orange-50 {
|
.bg-orange-50 {
|
||||||
background-color: var(--color-orange-50);
|
background-color: var(--color-orange-50);
|
||||||
}
|
}
|
||||||
.bg-orange-100 {
|
.bg-orange-100 {
|
||||||
background-color: var(--color-orange-100);
|
background-color: var(--color-orange-100);
|
||||||
}
|
}
|
||||||
|
.bg-orange-300 {
|
||||||
|
background-color: var(--color-orange-300);
|
||||||
|
}
|
||||||
.bg-orange-400 {
|
.bg-orange-400 {
|
||||||
background-color: var(--color-orange-400);
|
background-color: var(--color-orange-400);
|
||||||
}
|
}
|
||||||
|
@ -1389,6 +1477,9 @@
|
||||||
.bg-slate-100 {
|
.bg-slate-100 {
|
||||||
background-color: var(--color-slate-100);
|
background-color: var(--color-slate-100);
|
||||||
}
|
}
|
||||||
|
.bg-slate-400 {
|
||||||
|
background-color: var(--color-slate-400);
|
||||||
|
}
|
||||||
.bg-transparent {
|
.bg-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -1407,6 +1498,12 @@
|
||||||
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
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 {
|
.bg-yellow-50 {
|
||||||
background-color: var(--color-yellow-50);
|
background-color: var(--color-yellow-50);
|
||||||
}
|
}
|
||||||
|
@ -1439,6 +1536,10 @@
|
||||||
--tw-gradient-from: var(--color-blue-100);
|
--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));
|
--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 {
|
.from-blue-600 {
|
||||||
--tw-gradient-from: var(--color-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));
|
--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-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));
|
--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 {
|
.via-orange-400 {
|
||||||
--tw-gradient-via: var(--color-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);
|
--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-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));
|
--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 {
|
.to-emerald-600 {
|
||||||
--tw-gradient-to: var(--color-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));
|
--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-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));
|
--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 {
|
.to-orange-50 {
|
||||||
--tw-gradient-to: var(--color-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));
|
--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-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));
|
--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 {
|
.to-teal-50 {
|
||||||
--tw-gradient-to: var(--color-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));
|
--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-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));
|
--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-contain {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
@ -1660,6 +1789,9 @@
|
||||||
.py-8 {
|
.py-8 {
|
||||||
padding-block: calc(var(--spacing) * 8);
|
padding-block: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
.py-12 {
|
||||||
|
padding-block: calc(var(--spacing) * 12);
|
||||||
|
}
|
||||||
.py-16 {
|
.py-16 {
|
||||||
padding-block: calc(var(--spacing) * 16);
|
padding-block: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
|
@ -1783,6 +1915,10 @@
|
||||||
.font-mono {
|
.font-mono {
|
||||||
font-family: var(--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 {
|
.text-base {
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
line-height: var(--tw-leading, var(--text-base--line-height));
|
line-height: var(--tw-leading, var(--text-base--line-height));
|
||||||
|
@ -1935,12 +2071,18 @@
|
||||||
.text-slate-500 {
|
.text-slate-500 {
|
||||||
color: var(--color-slate-500);
|
color: var(--color-slate-500);
|
||||||
}
|
}
|
||||||
|
.text-slate-600 {
|
||||||
|
color: var(--color-slate-600);
|
||||||
|
}
|
||||||
.text-slate-700 {
|
.text-slate-700 {
|
||||||
color: var(--color-slate-700);
|
color: var(--color-slate-700);
|
||||||
}
|
}
|
||||||
.text-slate-800 {
|
.text-slate-800 {
|
||||||
color: var(--color-slate-800);
|
color: var(--color-slate-800);
|
||||||
}
|
}
|
||||||
|
.text-transparent {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
.text-white {
|
.text-white {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
@ -1977,9 +2119,15 @@
|
||||||
.opacity-30 {
|
.opacity-30 {
|
||||||
opacity: 30%;
|
opacity: 30%;
|
||||||
}
|
}
|
||||||
|
.opacity-40 {
|
||||||
|
opacity: 40%;
|
||||||
|
}
|
||||||
.opacity-50 {
|
.opacity-50 {
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
}
|
}
|
||||||
|
.opacity-60 {
|
||||||
|
opacity: 60%;
|
||||||
|
}
|
||||||
.opacity-75 {
|
.opacity-75 {
|
||||||
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));
|
--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);
|
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 {
|
.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));
|
--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);
|
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));
|
--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);
|
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 {
|
.ring-2 {
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
--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);
|
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-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
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 {
|
.duration-150 {
|
||||||
--tw-duration: 150ms;
|
--tw-duration: 150ms;
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
|
@ -2104,6 +2270,10 @@
|
||||||
--tw-duration: 300ms;
|
--tw-duration: 300ms;
|
||||||
transition-duration: 300ms;
|
transition-duration: 300ms;
|
||||||
}
|
}
|
||||||
|
.duration-500 {
|
||||||
|
--tw-duration: 500ms;
|
||||||
|
transition-duration: 500ms;
|
||||||
|
}
|
||||||
.ease-in-out {
|
.ease-in-out {
|
||||||
--tw-ease: var(--ease-in-out);
|
--tw-ease: var(--ease-in-out);
|
||||||
transition-timing-function: 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 {
|
.group-hover\:text-orange-500 {
|
||||||
&:is(:where(.group):hover *) {
|
&:is(:where(.group):hover *) {
|
||||||
@media (hover: 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 {
|
.group-hover\:opacity-100 {
|
||||||
&:is(:where(.group):hover *) {
|
&:is(:where(.group):hover *) {
|
||||||
@media (hover: 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\:border-orange-200 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: 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\:bg-white\/10 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: 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\:from-orange-600 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: 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\:to-orange-500 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: 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-orange-500 {
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--color-orange-500);
|
border-color: var(--color-orange-500);
|
||||||
|
@ -2680,11 +2987,27 @@
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@keyframes ping {
|
||||||
|
75%, 100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.5;
|
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 {
|
@layer properties {
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
@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 {
|
*, ::before, ::after, ::backdrop {
|
||||||
|
|
Loading…
Reference in New Issue