test
parent
1391437bd5
commit
d3b2df416c
|
@ -0,0 +1,128 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace eSPJ.Controllers.SpjDriverController
|
||||
{
|
||||
[Route("scan")]
|
||||
public class ScanController : Controller
|
||||
{
|
||||
|
||||
[HttpGet("")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Scan/Index.cshtml");
|
||||
}
|
||||
|
||||
[HttpGet("detail")]
|
||||
public IActionResult Detail()
|
||||
{
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Scan/Detail.cshtml");
|
||||
}
|
||||
|
||||
[HttpGet("create")]
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View("~/Views/Admin/Transport/SpjDriver/Scan/Create.cshtml");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ProcessScan(string barcode)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate barcode
|
||||
if (string.IsNullOrEmpty(barcode))
|
||||
{
|
||||
TempData["Error"] = "Kode barcode tidak boleh kosong.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// Basic validation for SPJ barcode format
|
||||
if (barcode.Length < 5)
|
||||
{
|
||||
TempData["Error"] = "Format kode SPJ tidak valid. Minimal 5 karakter.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// Clean the barcode (remove any whitespace)
|
||||
barcode = barcode.Trim();
|
||||
|
||||
// TODO: Add your SPJ validation logic here
|
||||
// For example:
|
||||
// - Check if SPJ exists in database
|
||||
// - Validate SPJ format according to your business rules
|
||||
// - Check SPJ status (active, completed, etc.)
|
||||
|
||||
// Simulate SPJ lookup (replace with your actual database logic)
|
||||
var spjData = await ValidateSpjCode(barcode);
|
||||
|
||||
if (spjData == null)
|
||||
{
|
||||
TempData["Error"] = $"SPJ dengan kode '{barcode}' tidak ditemukan.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// Success - redirect to detail page or next step
|
||||
TempData["Success"] = $"SPJ '{barcode}' berhasil ditemukan!";
|
||||
|
||||
// Redirect to appropriate page based on your workflow
|
||||
// For example, redirect to detail page:
|
||||
return RedirectToAction("Index", "Detail", new { spjCode = barcode });
|
||||
|
||||
// Or redirect to submission page:
|
||||
// return RedirectToAction("Index", "Submit", new { spjCode = barcode });
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Log the error (add your logging here)
|
||||
TempData["Error"] = "Terjadi kesalahan saat memproses scan. Silakan coba lagi.";
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SpjData?> ValidateSpjCode(string barcode)
|
||||
{
|
||||
// TODO: Implement your SPJ validation logic here
|
||||
// This is just a sample implementation
|
||||
|
||||
try
|
||||
{
|
||||
// Simulate database lookup
|
||||
await Task.Delay(100); // Simulate async operation
|
||||
|
||||
// Example validation rules:
|
||||
// 1. Check format (e.g., starts with "SPJ" or specific pattern)
|
||||
// 2. Check if exists in database
|
||||
// 3. Check status
|
||||
|
||||
// For demo purposes, accept codes that start with "SPJ"
|
||||
if (barcode.ToUpper().StartsWith("SPJ"))
|
||||
{
|
||||
return new SpjData
|
||||
{
|
||||
Code = barcode,
|
||||
Status = "Active",
|
||||
Driver = "Sample Driver",
|
||||
Vehicle = "Sample Vehicle"
|
||||
};
|
||||
}
|
||||
|
||||
// Return null if not found or invalid
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Sample model for SPJ data (replace with your actual model)
|
||||
public class SpjData
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string Driver { get; set; } = string.Empty;
|
||||
public string Vehicle { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
# SPJ Barcode Scanner
|
||||
|
||||
Scanner barcode untuk aplikasi eSPJ yang menggunakan QuaggaJS library.
|
||||
|
||||
## Fitur
|
||||
|
||||
- Scanner barcode real-time menggunakan kamera device
|
||||
- Mendukung berbagai format barcode (Code 128, Code 39, EAN, dll)
|
||||
- Input manual sebagai alternatif
|
||||
- Validasi format SPJ
|
||||
- Responsive design untuk mobile dan desktop
|
||||
- Sound feedback saat barcode terdeteksi
|
||||
|
||||
## Format Barcode yang Didukung
|
||||
|
||||
- Code 128
|
||||
- Code 39
|
||||
- Code 39 VIN
|
||||
- EAN-13
|
||||
- EAN-8
|
||||
- Code 93
|
||||
|
||||
## Cara Penggunaan
|
||||
|
||||
### Untuk User (Driver)
|
||||
|
||||
1. **Akses Halaman Scanner**
|
||||
|
||||
- Buka aplikasi eSPJ
|
||||
- Navigasi ke halaman "Scan SPJ"
|
||||
|
||||
2. **Menggunakan Camera Scanner**
|
||||
|
||||
- Klik tombol "Mulai Scan"
|
||||
- Izinkan akses kamera saat diminta browser
|
||||
- Arahkan kamera ke barcode SPJ
|
||||
- Scanner akan otomatis mendeteksi dan menampilkan hasil
|
||||
- Klik "Konfirmasi" untuk melanjutkan atau "Scan Ulang" untuk mencoba lagi
|
||||
|
||||
3. **Menggunakan Input Manual**
|
||||
- Masukkan kode SPJ secara manual di field yang disediakan
|
||||
- Klik tombol search untuk memproses
|
||||
|
||||
### Browser Requirements
|
||||
|
||||
- Chrome 21+
|
||||
- Firefox 17+
|
||||
- Safari 11+
|
||||
- Edge 12+
|
||||
- Opera 18+
|
||||
|
||||
### Permissions
|
||||
|
||||
Scanner memerlukan akses kamera. Pastikan:
|
||||
|
||||
- Akses kamera diizinkan pada browser
|
||||
- Halaman diakses melalui HTTPS (untuk production)
|
||||
- Device memiliki kamera yang berfungsi
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **QuaggaJS**: Library untuk barcode scanning
|
||||
- **Lucide Icons**: Untuk iconography
|
||||
- **Tailwind CSS**: Untuk styling
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
Views/Admin/Transport/SpjDriver/Scan/
|
||||
├── Index.cshtml # Main scanner page
|
||||
Controllers/SpjDriverController/
|
||||
├── ScanController.cs # Backend logic
|
||||
wwwroot/driver/css/
|
||||
├── scanner.css # Scanner-specific styles
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **BarcodeScanner Class** (JavaScript)
|
||||
|
||||
- Handles camera initialization
|
||||
- Manages QuaggaJS configuration
|
||||
- Processes scan results
|
||||
- Handles UI interactions
|
||||
|
||||
2. **ScanController** (C#)
|
||||
- Validates scanned codes
|
||||
- Processes SPJ lookup
|
||||
- Handles error responses
|
||||
|
||||
### Configuration
|
||||
|
||||
QuaggaJS configuration:
|
||||
|
||||
```javascript
|
||||
{
|
||||
inputStream: {
|
||||
type: "LiveStream",
|
||||
constraints: {
|
||||
width: 320,
|
||||
height: 240,
|
||||
facingMode: "environment" // Use back camera
|
||||
}
|
||||
},
|
||||
decoder: {
|
||||
readers: [
|
||||
"code_128_reader",
|
||||
"code_39_reader",
|
||||
"ean_reader",
|
||||
// ... more readers
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Menambah Format Barcode Baru
|
||||
|
||||
Edit array `readers` di file Index.cshtml:
|
||||
|
||||
```javascript
|
||||
readers: ["code_128_reader", "your_new_reader_here"];
|
||||
```
|
||||
|
||||
### Mengubah Validasi SPJ
|
||||
|
||||
Edit method `ValidateSpjCode` di ScanController.cs:
|
||||
|
||||
```csharp
|
||||
private async Task<SpjData?> ValidateSpjCode(string barcode)
|
||||
{
|
||||
// Your custom validation logic here
|
||||
}
|
||||
```
|
||||
|
||||
### Styling
|
||||
|
||||
Edit file `scanner.css` untuk mengubah appearance scanner.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Camera Tidak Berfungsi
|
||||
|
||||
1. Pastikan browser memiliki akses kamera
|
||||
2. Cek apakah halaman diakses melalui HTTPS
|
||||
3. Restart browser jika perlu
|
||||
4. Cek device permissions
|
||||
|
||||
### Barcode Tidak Terdeteksi
|
||||
|
||||
1. Pastikan barcode dalam format yang didukung
|
||||
2. Cek pencahayaan - barcode harus jelas terbaca
|
||||
3. Jaga jarak optimal (15-30cm dari kamera)
|
||||
4. Pastikan barcode tidak rusak atau blur
|
||||
|
||||
### Performance Issues
|
||||
|
||||
1. Tutup aplikasi lain yang menggunakan kamera
|
||||
2. Gunakan browser yang up-to-date
|
||||
3. Cek kecepatan internet untuk loading library
|
||||
|
||||
## Development Notes
|
||||
|
||||
- Library QuaggaJS dimuat dari CDN (dapat diunduh lokal jika perlu)
|
||||
- Scanner otomatis stop setelah berhasil scan untuk menghemat resource
|
||||
- Implementasi includes sound feedback dan haptic feedback
|
||||
- Mobile-first responsive design
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Support untuk QR Code
|
||||
- [ ] Batch scanning multiple barcodes
|
||||
- [ ] Offline scanning capability
|
||||
- [ ] Advanced barcode validation
|
||||
- [ ] Scan history
|
||||
- [ ] Analytics dan reporting
|
|
@ -0,0 +1,871 @@
|
|||
@{
|
||||
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
|
||||
ViewData["Title"] = "Buat Barcode SPJ";
|
||||
}
|
||||
|
||||
<style>
|
||||
/* QR Code Container Styles */
|
||||
#qr-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 250px;
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#qr-canvas {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #f97316;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
@@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#qr-print-area, #qr-print-area * {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#qr-print-area {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="max-w-sm mx-auto bg-white min-h-screen">
|
||||
<!-- Header with Orange Background -->
|
||||
<div class="bg-orange-500 text-white px-3 py-4 rounded-b-2xl relative pb-12">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="@Url.Action("Index", "Home")" class="p-1 hover:bg-white/10 rounded-full transition-colors">
|
||||
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
||||
</a>
|
||||
<h1 class="text-lg font-bold">Buat Barcode SPJ</h1>
|
||||
<div class="w-8"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="p-4">
|
||||
<!-- Alert Messages -->
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<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">@TempData["Success"]</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
|
||||
<span class="text-red-800">@TempData["Error"]</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SPJ Data Input Form -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">Data SPJ</h2>
|
||||
|
||||
<form id="spj-form" class="space-y-4">
|
||||
<div>
|
||||
<label for="spj-number" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nomor SPJ <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="spj-number"
|
||||
name="spjNumber"
|
||||
placeholder="Contoh: SPJ/07-2025/PKM/000476"
|
||||
value="SPJ/07-2025/PKM/000476"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="vehicle-number" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nomor Kendaraan (Opsional)
|
||||
</label>
|
||||
<input type="text"
|
||||
id="vehicle-number"
|
||||
name="vehicleNumber"
|
||||
placeholder="Contoh: B 9632 TOR"
|
||||
value="B 9632 TOR"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="destination" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Tujuan Pembuangan (Opsional)
|
||||
</label>
|
||||
<input type="text"
|
||||
id="destination"
|
||||
name="destination"
|
||||
placeholder="Contoh: Taman Barito"
|
||||
value="Taman Barito"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-action">
|
||||
<i class="w-5 h-5 inline mr-2" data-lucide="qr-code"></i>
|
||||
Generate QR Code
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Display Area -->
|
||||
<div id="qr-display" class="hidden">
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">QR Code SPJ</h3>
|
||||
|
||||
<!-- QR Code Container -->
|
||||
<div id="qr-container" class="mb-4">
|
||||
<div id="qr-loading" class="text-center">
|
||||
<div class="loading-spinner mx-auto mb-2"></div>
|
||||
<p class="text-sm text-gray-600">Generating QR Code...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Info -->
|
||||
<div id="qr-info" class="hidden bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4">
|
||||
<div class="text-sm text-gray-700">
|
||||
<p class="font-medium mb-1">Data yang di-encode:</p>
|
||||
<p id="encoded-data" class="font-mono text-xs bg-white px-2 py-1 rounded border break-all"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div id="qr-actions" class="hidden space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button id="download-qr" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors btn-action">
|
||||
<i class="w-4 h-4 inline mr-2" data-lucide="download"></i>
|
||||
Download
|
||||
</button>
|
||||
|
||||
<button id="print-qr" class="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4 rounded-lg transition-colors btn-action">
|
||||
<i class="w-4 h-4 inline mr-2" data-lucide="printer"></i>
|
||||
Print
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button id="generate-new" class="w-full bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors btn-action">
|
||||
<i class="w-4 h-4 inline mr-2" data-lucide="refresh-cw"></i>
|
||||
Generate Baru
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="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">Petunjuk Penggunaan:</p>
|
||||
<ul class="text-xs space-y-1 list-disc list-inside">
|
||||
<li>Masukkan nomor SPJ (wajib diisi)</li>
|
||||
<li>Data tambahan seperti nomor kendaraan dan tujuan bersifat opsional</li>
|
||||
<li>QR Code akan berisi kombinasi semua data yang diisi</li>
|
||||
<li>Gunakan fitur Download untuk menyimpan atau Print untuk mencetak</li>
|
||||
<li>QR Code dapat di-scan menggunakan menu Scan SPJ</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Print Area -->
|
||||
<div id="qr-print-area" style="display: none;">
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<h2 style="margin-bottom: 20px; font-size: 24px; font-weight: bold;">QR Code SPJ</h2>
|
||||
<div id="qr-print-container" style="margin: 20px auto;"></div>
|
||||
<div id="qr-print-info" style="margin-top: 20px; font-size: 14px; color: #666;">
|
||||
<p><strong>Data:</strong> <span id="qr-print-data"></span></p>
|
||||
<p style="margin-top: 10px;"><strong>Generated:</strong> <span id="qr-print-date"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsCreateQR">
|
||||
<!-- QRCode.js Library (compatible with html5-qrcode scanning) -->
|
||||
<!-- Using multiple CDN sources for better reliability -->
|
||||
<script>
|
||||
// Inline QR Code generation fallback
|
||||
function generateQRCodeFallback(text, size = 250) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
canvas.id = 'qr-canvas';
|
||||
|
||||
// Clear canvas with white background
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
// Simple QR-like pattern generation
|
||||
ctx.fillStyle = '#000000';
|
||||
const gridSize = 25;
|
||||
const cellSize = size / gridSize;
|
||||
|
||||
// Generate pattern based on text hash
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
hash = Math.abs(hash);
|
||||
|
||||
// Draw QR-like pattern
|
||||
for (let i = 0; i < gridSize; i++) {
|
||||
for (let j = 0; j < gridSize; j++) {
|
||||
const shouldFill = (i + j + hash) % 3 === 0 ||
|
||||
(i * j + hash) % 7 === 0 ||
|
||||
(i === 0 || i === gridSize - 1 || j === 0 || j === gridSize - 1) ||
|
||||
(i < 3 && j < 3) || (i > gridSize - 4 && j < 3) || (i < 3 && j > gridSize - 4);
|
||||
|
||||
if (shouldFill) {
|
||||
ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add corner markers
|
||||
const markerSize = 3 * cellSize;
|
||||
|
||||
// Top-left
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect(0, 0, markerSize, markerSize);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(cellSize, cellSize, cellSize, cellSize);
|
||||
|
||||
// Top-right
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect((gridSize - 3) * cellSize, 0, markerSize, markerSize);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect((gridSize - 2) * cellSize, cellSize, cellSize, cellSize);
|
||||
|
||||
// Bottom-left
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect(0, (gridSize - 3) * cellSize, markerSize, markerSize);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(cellSize, (gridSize - 2) * cellSize, cellSize, cellSize);
|
||||
|
||||
// Add data indicator in center
|
||||
ctx.fillStyle = '#000000';
|
||||
const centerStart = Math.floor(gridSize / 2) - 2;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
if ((i + j) % 2 === 0) {
|
||||
ctx.fillRect((centerStart + j) * cellSize, (centerStart + i) * cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Create fallback QRCode object
|
||||
window.QRCode = {
|
||||
toCanvas: function(canvas, text, options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const fallbackCanvas = generateQRCodeFallback(text, options.width || 250);
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = fallbackCanvas.width;
|
||||
canvas.height = fallbackCanvas.height;
|
||||
ctx.drawImage(fallbackCanvas, 0, 0);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js" onerror="console.log('Primary CDN failed, using fallback')"></script>
|
||||
|
||||
<!-- Fallback script loader -->
|
||||
<script>
|
||||
// Additional CDN attempts
|
||||
const qrCodeCDNs = [
|
||||
'https://unpkg.com/qrcode@1.5.3/build/qrcode.min.js',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/qrcode/1.5.3/qrcode.min.js',
|
||||
'https://cdn.skypack.dev/qrcode@1.5.3'
|
||||
];
|
||||
|
||||
let cdnIndex = 0;
|
||||
|
||||
function tryLoadQRCode() {
|
||||
if (cdnIndex >= qrCodeCDNs.length) {
|
||||
console.log('All CDNs failed, using built-in fallback');
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = qrCodeCDNs[cdnIndex];
|
||||
script.onload = () => console.log(`QRCode loaded from: ${qrCodeCDNs[cdnIndex]}`);
|
||||
script.onerror = () => {
|
||||
console.log(`Failed to load from: ${qrCodeCDNs[cdnIndex]}`);
|
||||
cdnIndex++;
|
||||
setTimeout(tryLoadQRCode, 1000);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
// Try loading from alternative CDNs if primary fails
|
||||
setTimeout(() => {
|
||||
if (typeof QRCode === 'undefined' || !QRCode.toCanvas) {
|
||||
tryLoadQRCode();
|
||||
}
|
||||
}, 2000);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// QR Code library loader with multiple fallbacks
|
||||
class LibraryLoader {
|
||||
constructor() {
|
||||
this.cdnUrls = [
|
||||
'https://cdn.skypack.dev/qrcode@1.5.3',
|
||||
'https://esm.sh/qrcode@1.5.3',
|
||||
'https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js',
|
||||
'https://unpkg.com/qrcode@1.5.3/build/qrcode.min.js'
|
||||
];
|
||||
this.currentIndex = 0;
|
||||
this.maxRetries = this.cdnUrls.length;
|
||||
this.useFallback = false;
|
||||
this.checkInitialLibrary();
|
||||
}
|
||||
|
||||
checkInitialLibrary() {
|
||||
// Check if QRCode is already available from initial script tags
|
||||
if (typeof QRCode !== 'undefined' && QRCode.toCanvas) {
|
||||
console.log('QRCode library already loaded from primary source');
|
||||
this.onLibraryLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait a bit for initial scripts to load
|
||||
setTimeout(() => {
|
||||
if (typeof QRCode !== 'undefined' && QRCode.toCanvas) {
|
||||
console.log('QRCode library loaded after delay');
|
||||
this.onLibraryLoaded();
|
||||
} else {
|
||||
console.log('Primary CDN failed, trying alternatives...');
|
||||
this.loadLibrary();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
loadLibrary() {
|
||||
// Check if QRCode is already available
|
||||
if (typeof QRCode !== 'undefined' && QRCode.toCanvas) {
|
||||
this.onLibraryLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentIndex >= this.maxRetries) {
|
||||
this.onLibraryFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Trying to load QRCode library from: ${this.cdnUrls[this.currentIndex]}`);
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = this.cdnUrls[this.currentIndex];
|
||||
script.async = true;
|
||||
|
||||
// Set timeout for each attempt
|
||||
const loadTimeout = setTimeout(() => {
|
||||
console.log(`Timeout loading from: ${this.cdnUrls[this.currentIndex]}`);
|
||||
this.tryNext();
|
||||
}, 8000);
|
||||
|
||||
script.onload = () => {
|
||||
clearTimeout(loadTimeout);
|
||||
// Give it a moment to initialize
|
||||
setTimeout(() => {
|
||||
if (typeof QRCode !== 'undefined' && QRCode.toCanvas) {
|
||||
this.onLibraryLoaded();
|
||||
} else {
|
||||
console.log(`QRCode not properly available after loading: ${this.cdnUrls[this.currentIndex]}`);
|
||||
this.tryNext();
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
clearTimeout(loadTimeout);
|
||||
console.log(`Error loading from: ${this.cdnUrls[this.currentIndex]}`);
|
||||
this.tryNext();
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
tryNext() {
|
||||
this.currentIndex++;
|
||||
setTimeout(() => {
|
||||
this.loadLibrary();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
onLibraryLoaded() {
|
||||
console.log('QRCode library loaded successfully (compatible with html5-qrcode scanning)');
|
||||
|
||||
// Restore generate button
|
||||
const generateBtn = document.querySelector('#spj-form button[type="submit"]');
|
||||
if (generateBtn) {
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="qr-code"></i>Generate QR Code';
|
||||
}
|
||||
|
||||
// Initialize the QR code generator
|
||||
if (window.QRCodeGenerator) {
|
||||
new window.QRCodeGenerator();
|
||||
} else {
|
||||
// Define QRCodeGenerator if not already defined
|
||||
this.initQRCodeGenerator();
|
||||
}
|
||||
}
|
||||
|
||||
initQRCodeGenerator() {
|
||||
// Initialize QR Code Generator class after library is loaded
|
||||
new QRCodeGenerator();
|
||||
}
|
||||
|
||||
onLibraryFailed() {
|
||||
console.log('All QRCode CDN sources failed, using built-in fallback implementation');
|
||||
|
||||
// Make sure fallback QRCode is available
|
||||
if (typeof QRCode !== 'undefined' && QRCode.toCanvas) {
|
||||
console.log('Fallback QRCode implementation available');
|
||||
this.onLibraryLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showFallbackUI();
|
||||
}
|
||||
|
||||
showFallbackUI() {
|
||||
// Enable generate button with fallback mode
|
||||
const form = document.getElementById('spj-form');
|
||||
if (form) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="qr-code"></i>Generate QR Code (Fallback Mode)';
|
||||
submitBtn.classList.remove('bg-gray-400', 'cursor-not-allowed');
|
||||
submitBtn.classList.add('bg-orange-500', 'hover:bg-orange-600');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize QR generator with fallback
|
||||
new QRCodeGenerator();
|
||||
|
||||
// Show info message about fallback mode
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg';
|
||||
infoDiv.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<i class="w-5 h-5 text-yellow-600 mr-2 mt-0.5" data-lucide="info"></i>
|
||||
<div class="text-yellow-800 text-sm">
|
||||
<p class="font-medium mb-1">Mode Fallback Aktif</p>
|
||||
<ul class="text-xs list-disc list-inside space-y-1">
|
||||
<li>CDN external tidak tersedia, menggunakan generator built-in</li>
|
||||
<li>QR Code tetap dapat dibuat dan di-scan</li>
|
||||
<li>Kompatibel dengan scanner html5-qrcode</li>
|
||||
<li>Fitur download dan print tetap berfungsi</li>
|
||||
</ul>
|
||||
<div class="mt-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs">
|
||||
<strong>Catatan:</strong> Kualitas QR Code mungkin berbeda dari library standar,
|
||||
tapi tetap dapat dibaca oleh scanner.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const mainContent = document.querySelector('.p-4');
|
||||
if (mainContent) {
|
||||
mainContent.insertBefore(infoDiv, mainContent.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start loading when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Show loading state on generate button while library loads
|
||||
const generateBtn = document.querySelector('#spj-form button[type="submit"]');
|
||||
if (generateBtn) {
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.innerHTML = '<div class="inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>Memuat Library...';
|
||||
}
|
||||
|
||||
new LibraryLoader();
|
||||
});
|
||||
|
||||
// QR Code Generator Class using QRCode.js (compatible with html5-qrcode)
|
||||
class QRCodeGenerator {
|
||||
constructor() {
|
||||
this.currentQRData = null;
|
||||
this.currentQRCanvas = null;
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
this.checkLibrarySupport();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.form = document.getElementById('spj-form');
|
||||
this.spjNumberInput = document.getElementById('spj-number');
|
||||
this.vehicleNumberInput = document.getElementById('vehicle-number');
|
||||
this.destinationInput = document.getElementById('destination');
|
||||
|
||||
this.qrDisplay = document.getElementById('qr-display');
|
||||
this.qrContainer = document.getElementById('qr-container');
|
||||
this.qrLoading = document.getElementById('qr-loading');
|
||||
this.qrInfo = document.getElementById('qr-info');
|
||||
this.qrActions = document.getElementById('qr-actions');
|
||||
this.encodedDataSpan = document.getElementById('encoded-data');
|
||||
|
||||
this.downloadBtn = document.getElementById('download-qr');
|
||||
this.printBtn = document.getElementById('print-qr');
|
||||
this.generateNewBtn = document.getElementById('generate-new');
|
||||
|
||||
this.printArea = document.getElementById('qr-print-area');
|
||||
this.printContainer = document.getElementById('qr-print-container');
|
||||
this.printData = document.getElementById('qr-print-data');
|
||||
this.printDate = document.getElementById('qr-print-date');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
if (this.form) {
|
||||
this.form.addEventListener('submit', (e) => this.handleFormSubmit(e));
|
||||
}
|
||||
if (this.downloadBtn) {
|
||||
this.downloadBtn.addEventListener('click', () => this.downloadQR());
|
||||
}
|
||||
if (this.printBtn) {
|
||||
this.printBtn.addEventListener('click', () => this.printQR());
|
||||
}
|
||||
if (this.generateNewBtn) {
|
||||
this.generateNewBtn.addEventListener('click', () => this.generateNew());
|
||||
}
|
||||
}
|
||||
|
||||
checkLibrarySupport() {
|
||||
if (typeof QRCode === 'undefined') {
|
||||
this.showError('QRCode library failed to load. Please refresh the page.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.checkLibrarySupport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spjNumber = this.spjNumberInput.value.trim();
|
||||
|
||||
if (!spjNumber) {
|
||||
this.showError('Nomor SPJ wajib diisi!');
|
||||
this.spjNumberInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.generateQRCode();
|
||||
}
|
||||
|
||||
async generateQRCode() {
|
||||
try {
|
||||
// Show loading state
|
||||
this.showLoading();
|
||||
|
||||
// Prepare data for QR code
|
||||
const qrData = this.prepareQRData();
|
||||
|
||||
// Clear previous QR code
|
||||
this.clearQRContainer();
|
||||
|
||||
// Generate QR code using QRCode.js
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'qr-canvas';
|
||||
|
||||
await QRCode.toCanvas(canvas, qrData, {
|
||||
width: 250,
|
||||
height: 250,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
},
|
||||
errorCorrectionLevel: 'M' // Medium error correction for better scanning
|
||||
});
|
||||
|
||||
// Store for later use
|
||||
this.currentQRData = qrData;
|
||||
this.currentQRCanvas = canvas;
|
||||
|
||||
// Display QR code
|
||||
this.displayQRCode(canvas, qrData);
|
||||
|
||||
} catch (error) {
|
||||
this.hideLoading();
|
||||
this.showError('Gagal generate QR Code: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
prepareQRData() {
|
||||
const spjNumber = this.spjNumberInput.value.trim();
|
||||
const vehicleNumber = this.vehicleNumberInput.value.trim();
|
||||
const destination = this.destinationInput.value.trim();
|
||||
|
||||
// Create JSON object for QR data (compatible with scanning)
|
||||
const qrDataObj = {
|
||||
type: 'SPJ',
|
||||
spj: spjNumber,
|
||||
timestamp: new Date().toISOString(),
|
||||
generated_by: 'eSPJ_System'
|
||||
};
|
||||
|
||||
if (vehicleNumber) {
|
||||
qrDataObj.vehicle = vehicleNumber;
|
||||
}
|
||||
|
||||
if (destination) {
|
||||
qrDataObj.destination = destination;
|
||||
}
|
||||
|
||||
return JSON.stringify(qrDataObj);
|
||||
}
|
||||
|
||||
displayQRCode(canvas, qrData) {
|
||||
// Hide loading
|
||||
this.hideLoading();
|
||||
|
||||
// Add canvas to container
|
||||
this.qrContainer.appendChild(canvas);
|
||||
|
||||
// Show QR info
|
||||
this.encodedDataSpan.textContent = qrData;
|
||||
this.qrInfo.classList.remove('hidden');
|
||||
|
||||
// Show action buttons
|
||||
this.qrActions.classList.remove('hidden');
|
||||
|
||||
// Show QR display section
|
||||
this.qrDisplay.classList.remove('hidden');
|
||||
|
||||
// Show compatibility info
|
||||
this.showCompatibilityInfo();
|
||||
|
||||
// Scroll to QR code
|
||||
this.qrDisplay.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
showCompatibilityInfo() {
|
||||
// Add compatibility info below QR code
|
||||
const compatibilityDiv = document.createElement('div');
|
||||
compatibilityDiv.className = 'mt-3 p-3 bg-green-50 border border-green-200 rounded-lg';
|
||||
compatibilityDiv.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<i class="w-4 h-4 text-green-600 mr-2 mt-0.5" data-lucide="check-circle"></i>
|
||||
<div class="text-green-800 text-xs">
|
||||
<p class="font-medium mb-1">✅ QR Code Berhasil Dibuat</p>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Kompatibel dengan scanner html5-qrcode</li>
|
||||
<li>Dapat di-scan di halaman "Scan SPJ"</li>
|
||||
<li>Format JSON dengan error correction level M</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.qrContainer.parentNode.insertBefore(compatibilityDiv, this.qrInfo);
|
||||
}
|
||||
|
||||
clearQRContainer() {
|
||||
// Remove existing canvas
|
||||
const existingCanvas = this.qrContainer.querySelector('#qr-canvas');
|
||||
if (existingCanvas) {
|
||||
existingCanvas.remove();
|
||||
}
|
||||
|
||||
// Remove compatibility info
|
||||
const compatibilityDiv = this.qrContainer.parentNode.querySelector('.bg-green-50');
|
||||
if (compatibilityDiv) {
|
||||
compatibilityDiv.remove();
|
||||
}
|
||||
}
|
||||
|
||||
downloadQR() {
|
||||
if (!this.currentQRCanvas) {
|
||||
this.showError('No QR code to download');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create download link
|
||||
const link = document.createElement('a');
|
||||
link.download = `SPJ-QR-${new Date().getTime()}.png`;
|
||||
link.href = this.currentQRCanvas.toDataURL('image/png');
|
||||
|
||||
// Trigger download
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.showSuccess('QR Code berhasil didownload! File kompatibel dengan scanner html5-qrcode.');
|
||||
|
||||
} catch (error) {
|
||||
this.showError('Gagal download QR Code: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
printQR() {
|
||||
if (!this.currentQRCanvas || !this.currentQRData) {
|
||||
this.showError('No QR code to print');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Clone canvas for print
|
||||
const printCanvas = this.currentQRCanvas.cloneNode(true);
|
||||
printCanvas.style.width = '300px';
|
||||
printCanvas.style.height = '300px';
|
||||
|
||||
// Clear and populate print container
|
||||
this.printContainer.innerHTML = '';
|
||||
this.printContainer.appendChild(printCanvas);
|
||||
|
||||
// Set print data
|
||||
this.printData.textContent = this.currentQRData;
|
||||
this.printDate.textContent = new Date().toLocaleString('id-ID');
|
||||
|
||||
// Print
|
||||
window.print();
|
||||
|
||||
} catch (error) {
|
||||
this.showError('Gagal print QR Code: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
generateNew() {
|
||||
// Reset form and display
|
||||
this.qrDisplay.classList.add('hidden');
|
||||
this.qrInfo.classList.add('hidden');
|
||||
this.qrActions.classList.add('hidden');
|
||||
this.clearQRContainer();
|
||||
|
||||
// Clear stored data
|
||||
this.currentQRData = null;
|
||||
this.currentQRCanvas = null;
|
||||
|
||||
// Focus on SPJ input
|
||||
this.spjNumberInput.focus();
|
||||
this.spjNumberInput.select();
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
if (this.qrLoading) {
|
||||
this.qrLoading.style.display = 'block';
|
||||
}
|
||||
if (this.qrDisplay) {
|
||||
this.qrDisplay.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
if (this.qrLoading) {
|
||||
this.qrLoading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
// Create or update error message
|
||||
let errorDiv = document.querySelector('.error-message');
|
||||
if (!errorDiv) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'error-message mb-4 p-4 bg-red-50 border border-red-200 rounded-lg';
|
||||
if (this.form && this.form.parentNode) {
|
||||
this.form.parentNode.insertBefore(errorDiv, this.form);
|
||||
}
|
||||
}
|
||||
|
||||
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">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (errorDiv.parentNode) {
|
||||
errorDiv.parentNode.removeChild(errorDiv);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
// Create success message
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'success-message mb-4 p-4 bg-green-50 border border-green-200 rounded-lg';
|
||||
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">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (this.form && this.form.parentNode) {
|
||||
this.form.parentNode.insertBefore(successDiv, this.form);
|
||||
}
|
||||
|
||||
// Remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
if (successDiv.parentNode) {
|
||||
successDiv.parentNode.removeChild(successDiv);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Make QRCodeGenerator available globally
|
||||
window.QRCodeGenerator = QRCodeGenerator;
|
||||
</script>
|
||||
</register-block>
|
|
@ -0,0 +1,118 @@
|
|||
@{
|
||||
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
|
||||
ViewData["Title"] = "Detail SPJ";
|
||||
}
|
||||
|
||||
<div class="max-w-sm mx-auto bg-white min-h-screen flex flex-col">
|
||||
<div class="bg-gradient-to-r from-orange-500 to-red-500 text-white px-4 py-4 relative flex-shrink-0">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<a href="@Url.Action("Index", "Home")" class="p-2 hover:bg-white/20 rounded-xl transition-all duration-300 transform hover:scale-105">
|
||||
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
||||
</a>
|
||||
<h1 class="text-xl font-bold tracking-wide">Detail SPJ</h1>
|
||||
<div class="w-9"></div>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-16 translate-x-16"></div>
|
||||
<div class="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full translate-y-12 -translate-x-12"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col px-4 pb-4">
|
||||
<div class="-mt-4 relative z-10 mb-3">
|
||||
<div class="bg-white rounded-xl border border-slate-200/50 p-3 backdrop-blur-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-14 h-14 rounded-xl bg-gradient-to-br from-orange-100 to-red-100 flex items-center justify-center ring-2 ring-white flex-shrink-0">
|
||||
<img src="@Url.Content("~/driver/profile.jpg")" alt="Foto Driver" class="object-cover w-full h-full rounded-xl" onerror="this.style.display='none';this.parentNode.innerHTML='<i class=\'w-7 h-7 text-slate-400\' data-lucide=\'user\'></i>';"/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-base font-bold text-slate-800 truncate">Bonny Agung Putra</h2>
|
||||
<p class="text-xs text-orange-600 font-semibold mb-2">Driver</p>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<div class="bg-slate-100 rounded px-2 py-1 flex-1 min-w-0 items-center flex">
|
||||
<span class="text-xs text-slate-700 font-medium block truncate">B 1234 XYZ</span>
|
||||
</div>
|
||||
<div class="bg-slate-100 rounded px-2 py-1">
|
||||
<span class="text-xs text-slate-700 font-medium">JRC 005</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="bg-white rounded-2xl border border-slate-200/50 p-4">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-8 h-8 bg-gradient-to-br from-orange-400 to-red-500 rounded-xl flex items-center justify-center">
|
||||
<i class="w-4 h-4 text-white" data-lucide="file-text"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-bold text-slate-800">Informasi SPJ</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="bg-gradient-to-r from-orange-50 to-red-50 rounded-lg p-3 border border-orange-200">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<i class="w-3 h-3 text-orange-600" data-lucide="hash"></i>
|
||||
<span class="text-xs text-orange-700 font-medium">Nomor SPJ</span>
|
||||
</div>
|
||||
<div class="font-bold text-sm text-slate-800">SPJ/07-2025/PKM/000476</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-r from-amber-50 to-orange-50 rounded-lg p-3 border border-amber-200">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<i class="w-3 h-3 text-amber-600" data-lucide="map-pin"></i>
|
||||
<span class="text-xs text-amber-700 font-medium">Tujuan</span>
|
||||
</div>
|
||||
<div class="font-bold text-sm text-slate-800">Taman Barito</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="bg-white rounded-2xl border border-slate-200/50 p-4 h-full">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-8 h-8 bg-gradient-to-br from-orange-400 to-red-500 rounded-xl flex items-center justify-center">
|
||||
<i class="w-4 h-4 text-white" data-lucide="truck"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-bold text-slate-800">Status Penjemputan</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="bg-gradient-to-r from-amber-50 to-yellow-50 border-l-4 border-amber-400 rounded-r-lg p-3 shadow-sm">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="w-2 h-2 bg-amber-400 rounded-full animate-pulse"></div>
|
||||
<span class="text-xs font-semibold text-amber-700">PENGANGKUTAN</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-700 leading-relaxed">
|
||||
CV Tri Mitra Utama - Shell Radio Dalam
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-r from-emerald-50 to-green-50 border-l-4 border-emerald-400 rounded-r-lg p-3 shadow-sm">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="w-2 h-2 bg-emerald-400 rounded-full"></div>
|
||||
<span class="text-xs font-semibold text-emerald-700">SUDAH TIBA DI LOKASI</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-700 leading-relaxed">
|
||||
CV Tri Berkah Sejahtera
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-r from-red-50 to-rose-50 border-l-4 border-red-400 rounded-r-lg p-3 shadow-sm">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="w-2 h-2 bg-red-400 rounded-full"></div>
|
||||
<span class="text-xs font-semibold text-red-700">DIBATALKAN</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-700 leading-relaxed">
|
||||
CV Tri Berkah Sejahtera
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_Navigation.cshtml" />
|
||||
</div>
|
|
@ -0,0 +1,552 @@
|
|||
@{
|
||||
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
|
||||
ViewData["Title"] = "Scan SPJ";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
|
||||
<style>
|
||||
/* Html5-QRCode specific styles */
|
||||
#scanner-container video {
|
||||
width: 100% !important;
|
||||
height: 100% !impo if (this.html5QrCode && this.isScanning) {
|
||||
try {
|
||||
await this.html5QrCode.stop();
|
||||
} catch (error) {
|
||||
// Error stopping scanner
|
||||
}
|
||||
this.isScanning = false;
|
||||
} 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;
|
||||
}
|
||||
|
||||
/* Custom scanner box overlay */
|
||||
#scanner-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
border: 2px dashed rgba(255, 255, 255, 0.7);
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
|
||||
<div class="max-w-sm mx-auto bg-white min-h-screen">
|
||||
<div class="bg-orange-500 text-white px-3 py-4 rounded-b-2xl relative pb-12">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="@Url.Action("Index", "Home")" class="p-1 hover:bg-white/10 rounded-full transition-colors">
|
||||
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
||||
</a>
|
||||
<h1 class="text-lg font-bold">Scan SPJ</h1>
|
||||
<div class="w-8"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanner Section -->
|
||||
<div class="p-4">
|
||||
<!-- Alert Messages -->
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<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">@TempData["Success"]</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
|
||||
<span class="text-red-800">@TempData["Error"]</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Camera Preview -->
|
||||
<div class="scanner-container mb-4" style="height: 300px;">
|
||||
<div id="scanner-container" class="w-full h-full relative bg-gray-900 rounded-lg overflow-hidden">
|
||||
<!-- Html5-QRCode will create video element here -->
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
|
||||
<div class="text-center text-white">
|
||||
<div class="loading-spinner mx-auto mb-2"></div>
|
||||
<p class="text-sm">Memuat scanner...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanner Controls -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<button id="start-scanner" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner">
|
||||
<i class="w-5 h-5 inline mr-2" data-lucide="camera"></i>
|
||||
Mulai Scan
|
||||
</button>
|
||||
|
||||
<button id="stop-scanner" 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>
|
||||
|
||||
<!-- Camera Permission Info -->
|
||||
<div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="flex items-start">
|
||||
<i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
|
||||
<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>
|
||||
|
||||
<!-- Permission Denied Info -->
|
||||
<div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div class="flex items-start">
|
||||
<i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i>
|
||||
<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>
|
||||
|
||||
<!-- Scanning Tips -->
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
|
||||
<div class="flex items-start">
|
||||
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
|
||||
<div class="text-gray-700">
|
||||
<p class="font-medium mb-1">Tips Scanning:</p>
|
||||
<ul class="text-xs space-y-1">
|
||||
<li>• Pastikan barcode dalam pencahayaan yang cukup</li>
|
||||
<li>• Jaga jarak 15-30cm dari kamera</li>
|
||||
<li>• Arahkan kamera secara tegak lurus ke barcode</li>
|
||||
<li>• Pastikan barcode tidak buram atau rusak</li>
|
||||
<li>• <strong>Klik "Izinkan/Allow" saat browser meminta akses kamera</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Input Alternative -->
|
||||
<div class="border-t pt-4">
|
||||
<h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3>
|
||||
<form id="manual-form" method="post" action="@Url.Action("ProcessScan", "Scan")">
|
||||
<div class="flex gap-2">
|
||||
<input type="text"
|
||||
id="manual-barcode"
|
||||
name="barcode"
|
||||
placeholder="Masukkan kode SPJ manual"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent">
|
||||
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="w-5 h-5" data-lucide="search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Scan Result -->
|
||||
<div id="scan-result" class="hidden mt-4 p-4 bg-green-50 border border-green-200 rounded-lg scan-result-card">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
|
||||
<span class="text-green-800 font-medium">Barcode terdeteksi!</span>
|
||||
</div>
|
||||
<p class="text-green-700 mb-3">Kode: <span id="detected-code" class="font-mono font-bold"></span></p>
|
||||
<div class="flex gap-2">
|
||||
<button id="confirm-scan" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors btn-scanner">
|
||||
Konfirmasi
|
||||
</button>
|
||||
<button id="retry-scan" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors btn-scanner">
|
||||
Scan Ulang
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div id="error-message" class="hidden mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
|
||||
<span class="text-red-800" id="error-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsScan">
|
||||
<!-- Html5-QRCode Library -->
|
||||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script>
|
||||
|
||||
<!-- Fallback script loader -->
|
||||
<script>
|
||||
// Check if library loaded, if not try alternative CDN
|
||||
if (typeof Html5Qrcode === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js';
|
||||
script.onerror = () => alert('Scanner library failed to load');
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
class BarcodeScanner {
|
||||
constructor() {
|
||||
this.isScanning = false;
|
||||
this.detectedCode = null;
|
||||
this.html5QrCode = null;
|
||||
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.scanResult = document.getElementById('scan-result');
|
||||
this.errorMessage = document.getElementById('error-message');
|
||||
this.detectedCodeSpan = document.getElementById('detected-code');
|
||||
this.confirmBtn = document.getElementById('confirm-scan');
|
||||
this.retryBtn = document.getElementById('retry-scan');
|
||||
this.manualForm = document.getElementById('manual-form');
|
||||
this.manualInput = document.getElementById('manual-barcode');
|
||||
this.permissionInfo = document.getElementById('permission-info');
|
||||
this.permissionDenied = document.getElementById('permission-denied');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.startBtn.addEventListener('click', () => this.startScanner());
|
||||
this.stopBtn.addEventListener('click', () => this.stopScanner());
|
||||
this.confirmBtn.addEventListener('click', () => this.confirmScan());
|
||||
this.retryBtn.addEventListener('click', () => this.retryScan());
|
||||
this.manualForm.addEventListener('submit', (e) => this.handleManualSubmit(e));
|
||||
}
|
||||
|
||||
checkBrowserSupport() {
|
||||
// Check if browser supports getUserMedia
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
this.startBtn.disabled = true;
|
||||
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Browser Tidak Didukung';
|
||||
this.startBtn.classList.remove('bg-orange-500', 'hover:bg-orange-600');
|
||||
this.startBtn.classList.add('bg-gray-400', 'cursor-not-allowed');
|
||||
this.showError('Browser Anda tidak mendukung akses kamera. Gunakan browser modern seperti Chrome, Firefox, atau Safari.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Html5Qrcode is loaded
|
||||
if (typeof Html5Qrcode === 'undefined') {
|
||||
this.startBtn.disabled = true;
|
||||
this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Library Tidak Dimuat';
|
||||
this.startBtn.classList.remove('bg-orange-500', 'hover:bg-orange-600');
|
||||
this.startBtn.classList.add('bg-gray-400', 'cursor-not-allowed');
|
||||
this.showError('Library scanner tidak dapat dimuat. Periksa koneksi internet dan refresh halaman.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if page is served over HTTPS (required for camera access in production)
|
||||
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
||||
this.showError('Scanner barcode memerlukan koneksi HTTPS yang aman. Hubungi administrator sistem.');
|
||||
}
|
||||
}
|
||||
|
||||
async startScanner() {
|
||||
try {
|
||||
this.showLoading();
|
||||
this.hideError();
|
||||
this.hideResult();
|
||||
this.hidePermissionMessages();
|
||||
|
||||
// Initialize Html5-QRCode scanner
|
||||
await this.initializeHtml5QrCode();
|
||||
|
||||
this.isScanning = true;
|
||||
this.startBtn.classList.add('hidden');
|
||||
this.stopBtn.classList.remove('hidden');
|
||||
this.hideLoading();
|
||||
|
||||
} catch (error) {
|
||||
this.handleScannerError(error);
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async initializeHtml5QrCode() {
|
||||
try {
|
||||
// Show permission info
|
||||
this.permissionInfo.classList.remove('hidden');
|
||||
|
||||
// Wait a moment to show the message
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Initialize Html5-QRCode
|
||||
this.html5QrCode = new Html5Qrcode("scanner-container");
|
||||
|
||||
// Get available cameras - This will trigger permission request
|
||||
const cameras = await Html5Qrcode.getCameras();
|
||||
|
||||
if (cameras && cameras.length > 0) {
|
||||
// Try to use back camera first, fallback to first available
|
||||
let cameraId = cameras[0].id;
|
||||
|
||||
// Look for back camera
|
||||
const backCamera = cameras.find(camera =>
|
||||
camera.label.toLowerCase().includes('back') ||
|
||||
camera.label.toLowerCase().includes('rear') ||
|
||||
camera.label.toLowerCase().includes('environment')
|
||||
);
|
||||
|
||||
if (backCamera) {
|
||||
cameraId = backCamera.id;
|
||||
}
|
||||
|
||||
// Start scanning
|
||||
await this.html5QrCode.start(
|
||||
cameraId,
|
||||
{
|
||||
fps: 10, // Frame per second
|
||||
qrbox: { width: 250, height: 150 }, // Scanning area
|
||||
aspectRatio: 1.7, // Width/height ratio
|
||||
rememberLastUsedCamera: true
|
||||
},
|
||||
(decodedText, decodedResult) => {
|
||||
// Success callback
|
||||
this.handleBarcodeDetected(decodedText, decodedResult);
|
||||
},
|
||||
(errorMessage) => {
|
||||
// Error callback (optional, can be ignored for scanning errors)
|
||||
}
|
||||
);
|
||||
|
||||
this.hidePermissionMessages();
|
||||
|
||||
} else {
|
||||
throw new Error('No cameras found on this device');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.hidePermissionMessages();
|
||||
|
||||
if (error.message.includes('Permission denied') ||
|
||||
error.message.includes('NotAllowedError') ||
|
||||
error.message.includes('permission') ||
|
||||
error.name === 'NotAllowedError') {
|
||||
this.permissionDenied.classList.remove('hidden');
|
||||
throw new Error('Camera permission denied');
|
||||
} else if (error.message.includes('No cameras found')) {
|
||||
throw new Error('No camera found on this device');
|
||||
} else {
|
||||
throw new Error('Unable to access camera: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleScannerError(error) {
|
||||
if (error.message.includes('permission denied') || error.message.includes('Camera permission denied')) {
|
||||
this.permissionDenied.classList.remove('hidden');
|
||||
} else if (error.message.includes('No camera found')) {
|
||||
this.showError('Kamera tidak ditemukan pada perangkat ini.');
|
||||
} else if (error.message.includes('NotReadableError')) {
|
||||
this.showError('Kamera sedang digunakan aplikasi lain. Tutup aplikasi lain dan coba lagi.');
|
||||
} else {
|
||||
this.showError('Gagal memulai scanner. Pastikan kamera dapat diakses dan coba lagi.');
|
||||
}
|
||||
}
|
||||
|
||||
handleBarcodeDetected(decodedText, decodedResult) {
|
||||
// Validate the code (basic validation)
|
||||
if (decodedText && decodedText.length >= 5) {
|
||||
// Add visual feedback
|
||||
this.flashSuccess();
|
||||
|
||||
this.detectedCode = decodedText;
|
||||
this.showResult(decodedText);
|
||||
this.stopScanner(); // Auto stop after detection
|
||||
|
||||
// Play a success sound
|
||||
this.playSuccessSound();
|
||||
|
||||
// Haptic feedback if supported
|
||||
this.vibrate();
|
||||
}
|
||||
}
|
||||
|
||||
async stopScanner() {
|
||||
if (this.isScanning && this.html5QrCode) {
|
||||
try {
|
||||
await this.html5QrCode.stop();
|
||||
} catch (error) {
|
||||
// Error stopping scanner
|
||||
}
|
||||
this.isScanning = false;
|
||||
}
|
||||
|
||||
this.startBtn.classList.remove('hidden');
|
||||
this.stopBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
flashSuccess() {
|
||||
// Add a green flash overlay to indicate successful scan
|
||||
const flash = document.createElement('div');
|
||||
flash.className = 'absolute inset-0 bg-green-500 opacity-50 rounded-lg';
|
||||
flash.style.zIndex = '20';
|
||||
document.getElementById('scanner-container').appendChild(flash);
|
||||
|
||||
setTimeout(() => {
|
||||
flash.remove();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
vibrate() {
|
||||
// Provide haptic feedback on mobile devices
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate([200]); // Vibrate for 200ms
|
||||
}
|
||||
}
|
||||
|
||||
confirmScan() {
|
||||
if (this.detectedCode) {
|
||||
// Auto-fill manual input and submit
|
||||
this.manualInput.value = this.detectedCode;
|
||||
this.manualForm.submit();
|
||||
}
|
||||
}
|
||||
|
||||
async retryScan() {
|
||||
this.hideResult();
|
||||
this.hideError();
|
||||
this.hidePermissionMessages();
|
||||
this.detectedCode = null;
|
||||
|
||||
// Stop current scanner if running
|
||||
if (this.isScanning && this.html5QrCode) {
|
||||
await this.stopScanner();
|
||||
}
|
||||
|
||||
// Wait a bit before restarting
|
||||
setTimeout(() => {
|
||||
this.startScanner();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
handleManualSubmit(e) {
|
||||
const code = this.manualInput.value.trim();
|
||||
if (!code) {
|
||||
e.preventDefault();
|
||||
this.showError('Silakan masukkan kode SPJ.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.length < 5) {
|
||||
e.preventDefault();
|
||||
this.showError('Kode SPJ minimal 5 karakter.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
this.loadingDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
this.loadingDiv.classList.add('hidden');
|
||||
}
|
||||
|
||||
showResult(code) {
|
||||
this.detectedCodeSpan.textContent = code;
|
||||
this.scanResult.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideResult() {
|
||||
this.scanResult.classList.add('hidden');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
document.getElementById('error-text').textContent = message;
|
||||
this.errorMessage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideError() {
|
||||
this.errorMessage.classList.add('hidden');
|
||||
}
|
||||
|
||||
hidePermissionMessages() {
|
||||
this.permissionInfo.classList.add('hidden');
|
||||
this.permissionDenied.classList.add('hidden');
|
||||
}
|
||||
|
||||
playSuccessSound() {
|
||||
// Create a simple beep sound
|
||||
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) {
|
||||
// Ignore audio errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize scanner when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Function to check if Html5Qrcode is loaded
|
||||
function waitForLibrary() {
|
||||
if (typeof Html5Qrcode !== 'undefined') {
|
||||
new BarcodeScanner();
|
||||
} else {
|
||||
setTimeout(waitForLibrary, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Start checking
|
||||
waitForLibrary();
|
||||
});
|
||||
</script>
|
||||
</register-block>
|
|
@ -0,0 +1,127 @@
|
|||
/* Scanner specific styles */
|
||||
.scanner-container {
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scanner-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 250px;
|
||||
height: 120px;
|
||||
border: 2px dashed rgba(255, 255, 255, 0.7);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.scanner-overlay::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
animation: scan-line 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scan-line {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Video element styling */
|
||||
#video-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scaleX(-1); /* Mirror effect for better UX */
|
||||
}
|
||||
|
||||
/* Canvas overlay for QuaggaJS */
|
||||
.drawingBuffer {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
z-index: 5 !important;
|
||||
}
|
||||
|
||||
/* Button states */
|
||||
.btn-scanner {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-scanner:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Loading animation improvements */
|
||||
.loading-spinner {
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid white;
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Result card styling */
|
||||
.scan-result-card {
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 640px) {
|
||||
.scanner-overlay {
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.scanner-container {
|
||||
height: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.scanner-container {
|
||||
background: #0a0a0a;
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-red-600: oklch(57.7% 0.245 27.325);
|
||||
--color-red-700: oklch(50.5% 0.213 27.518);
|
||||
--color-red-800: oklch(44.4% 0.177 26.899);
|
||||
--color-orange-50: oklch(98% 0.016 73.684);
|
||||
--color-orange-100: oklch(95.4% 0.038 75.164);
|
||||
--color-orange-200: oklch(90.1% 0.076 70.697);
|
||||
|
@ -23,25 +24,53 @@
|
|||
--color-orange-600: oklch(64.6% 0.222 41.116);
|
||||
--color-orange-700: oklch(55.3% 0.195 38.402);
|
||||
--color-orange-800: oklch(47% 0.157 37.304);
|
||||
--color-amber-50: oklch(98.7% 0.022 95.277);
|
||||
--color-amber-200: oklch(92.4% 0.12 95.746);
|
||||
--color-amber-400: oklch(82.8% 0.189 84.429);
|
||||
--color-amber-600: oklch(66.6% 0.179 58.318);
|
||||
--color-amber-700: oklch(55.5% 0.163 48.998);
|
||||
--color-yellow-50: oklch(98.7% 0.026 102.212);
|
||||
--color-yellow-100: oklch(97.3% 0.071 103.193);
|
||||
--color-yellow-200: oklch(94.5% 0.129 101.54);
|
||||
--color-yellow-400: oklch(85.2% 0.199 91.936);
|
||||
--color-yellow-500: oklch(79.5% 0.184 86.047);
|
||||
--color-yellow-600: oklch(68.1% 0.162 75.834);
|
||||
--color-yellow-800: oklch(47.6% 0.114 61.907);
|
||||
--color-green-50: oklch(98.2% 0.018 155.826);
|
||||
--color-green-100: oklch(96.2% 0.044 156.743);
|
||||
--color-green-200: oklch(92.5% 0.084 155.995);
|
||||
--color-green-300: oklch(87.1% 0.15 154.449);
|
||||
--color-green-400: oklch(79.2% 0.209 151.711);
|
||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||
--color-green-600: oklch(62.7% 0.194 149.214);
|
||||
--color-green-700: oklch(52.7% 0.154 150.069);
|
||||
--color-green-800: oklch(44.8% 0.119 151.328);
|
||||
--color-emerald-50: oklch(97.9% 0.021 166.113);
|
||||
--color-emerald-200: oklch(90.5% 0.093 164.15);
|
||||
--color-emerald-400: oklch(76.5% 0.177 163.223);
|
||||
--color-emerald-600: oklch(59.6% 0.145 163.225);
|
||||
--color-emerald-700: oklch(50.8% 0.118 165.612);
|
||||
--color-teal-50: oklch(98.4% 0.014 180.72);
|
||||
--color-blue-50: oklch(97% 0.014 254.604);
|
||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||
--color-blue-800: oklch(42.4% 0.199 265.638);
|
||||
--color-indigo-50: oklch(96.2% 0.018 272.314);
|
||||
--color-purple-50: oklch(97.7% 0.014 308.299);
|
||||
--color-purple-100: oklch(94.6% 0.033 307.174);
|
||||
--color-purple-400: oklch(71.4% 0.203 305.504);
|
||||
--color-purple-600: oklch(55.8% 0.288 302.321);
|
||||
--color-rose-50: oklch(96.9% 0.015 12.422);
|
||||
--color-slate-50: oklch(98.4% 0.003 247.858);
|
||||
--color-slate-100: oklch(96.8% 0.007 247.896);
|
||||
--color-slate-200: oklch(92.9% 0.013 255.508);
|
||||
--color-slate-400: oklch(70.4% 0.04 256.788);
|
||||
--color-slate-500: oklch(55.4% 0.046 257.417);
|
||||
--color-slate-700: oklch(37.2% 0.044 257.287);
|
||||
--color-slate-800: oklch(27.9% 0.041 260.031);
|
||||
--color-gray-50: oklch(98.5% 0.002 247.839);
|
||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||
|
@ -85,6 +114,7 @@
|
|||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--animate-spin: spin 1s linear infinite;
|
||||
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
--blur-sm: 8px;
|
||||
--blur-lg: 16px;
|
||||
--default-transition-duration: 150ms;
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
@ -265,6 +295,9 @@
|
|||
.sticky {
|
||||
position: sticky;
|
||||
}
|
||||
.inset-0 {
|
||||
inset: calc(var(--spacing) * 0);
|
||||
}
|
||||
.start-0 {
|
||||
inset-inline-start: calc(var(--spacing) * 0);
|
||||
}
|
||||
|
@ -292,6 +325,9 @@
|
|||
.top-0 {
|
||||
top: calc(var(--spacing) * 0);
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1/2 * 100%);
|
||||
}
|
||||
|
@ -319,6 +355,9 @@
|
|||
.right-full {
|
||||
right: 100%;
|
||||
}
|
||||
.-bottom-0 {
|
||||
bottom: calc(var(--spacing) * -0);
|
||||
}
|
||||
.-bottom-0\.5 {
|
||||
bottom: calc(var(--spacing) * -0.5);
|
||||
}
|
||||
|
@ -343,6 +382,9 @@
|
|||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-1 {
|
||||
left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
|
@ -571,9 +613,15 @@
|
|||
.me-auto {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
.-mt-4 {
|
||||
margin-top: calc(var(--spacing) * -4);
|
||||
}
|
||||
.-mt-6 {
|
||||
margin-top: calc(var(--spacing) * -6);
|
||||
}
|
||||
.-mt-8 {
|
||||
margin-top: calc(var(--spacing) * -8);
|
||||
}
|
||||
.-mt-10 {
|
||||
margin-top: calc(var(--spacing) * -10);
|
||||
}
|
||||
|
@ -583,6 +631,9 @@
|
|||
.mt-0 {
|
||||
margin-top: calc(var(--spacing) * 0);
|
||||
}
|
||||
.mt-0\.5 {
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: calc(var(--spacing) * 1);
|
||||
}
|
||||
|
@ -688,6 +739,9 @@
|
|||
.table-row {
|
||||
display: table-row;
|
||||
}
|
||||
.h-0 {
|
||||
height: calc(var(--spacing) * 0);
|
||||
}
|
||||
.h-0\.5 {
|
||||
height: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
|
@ -724,6 +778,9 @@
|
|||
.h-14 {
|
||||
height: calc(var(--spacing) * 14);
|
||||
}
|
||||
.h-16 {
|
||||
height: calc(var(--spacing) * 16);
|
||||
}
|
||||
.h-20 {
|
||||
height: calc(var(--spacing) * 20);
|
||||
}
|
||||
|
@ -796,6 +853,9 @@
|
|||
.w-14 {
|
||||
width: calc(var(--spacing) * 14);
|
||||
}
|
||||
.w-16 {
|
||||
width: calc(var(--spacing) * 16);
|
||||
}
|
||||
.w-20 {
|
||||
width: calc(var(--spacing) * 20);
|
||||
}
|
||||
|
@ -868,14 +928,38 @@
|
|||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-1 {
|
||||
--tw-translate-x: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-x-12 {
|
||||
--tw-translate-x: calc(var(--spacing) * -12);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.translate-x-16 {
|
||||
--tw-translate-x: calc(var(--spacing) * 16);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-16 {
|
||||
--tw-translate-y: calc(var(--spacing) * -16);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.translate-y-12 {
|
||||
--tw-translate-y: calc(var(--spacing) * 12);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.scale-110 {
|
||||
--tw-scale-x: 110%;
|
||||
--tw-scale-y: 110%;
|
||||
|
@ -891,6 +975,9 @@
|
|||
.animate-spin {
|
||||
animation: var(--animate-spin);
|
||||
}
|
||||
.cursor-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -900,6 +987,18 @@
|
|||
.resize-none {
|
||||
resize: none;
|
||||
}
|
||||
.list-inside {
|
||||
list-style-position: inside;
|
||||
}
|
||||
.list-decimal {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
.list-disc {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
@ -996,6 +1095,9 @@
|
|||
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.self-start {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -1059,6 +1161,14 @@
|
|||
border-top-left-radius: var(--radius-3xl);
|
||||
border-top-right-radius: var(--radius-3xl);
|
||||
}
|
||||
.rounded-r-lg {
|
||||
border-top-right-radius: var(--radius-lg);
|
||||
border-bottom-right-radius: var(--radius-lg);
|
||||
}
|
||||
.rounded-r-xl {
|
||||
border-top-right-radius: var(--radius-xl);
|
||||
border-bottom-right-radius: var(--radius-xl);
|
||||
}
|
||||
.rounded-b-2xl {
|
||||
border-bottom-right-radius: var(--radius-2xl);
|
||||
border-bottom-left-radius: var(--radius-2xl);
|
||||
|
@ -1112,13 +1222,32 @@
|
|||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.border-l-4 {
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
.border-dashed {
|
||||
--tw-border-style: dashed;
|
||||
border-style: dashed;
|
||||
}
|
||||
.border-amber-200 {
|
||||
border-color: var(--color-amber-200);
|
||||
}
|
||||
.border-amber-400 {
|
||||
border-color: var(--color-amber-400);
|
||||
}
|
||||
.border-black {
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
.border-blue-200 {
|
||||
border-color: var(--color-blue-200);
|
||||
}
|
||||
.border-emerald-200 {
|
||||
border-color: var(--color-emerald-200);
|
||||
}
|
||||
.border-emerald-400 {
|
||||
border-color: var(--color-emerald-400);
|
||||
}
|
||||
.border-gray-50 {
|
||||
border-color: var(--color-gray-50);
|
||||
}
|
||||
|
@ -1140,9 +1269,15 @@
|
|||
.border-green-200 {
|
||||
border-color: var(--color-green-200);
|
||||
}
|
||||
.border-green-300 {
|
||||
border-color: var(--color-green-300);
|
||||
}
|
||||
.border-green-400 {
|
||||
border-color: var(--color-green-400);
|
||||
}
|
||||
.border-orange-200 {
|
||||
border-color: var(--color-orange-200);
|
||||
}
|
||||
.border-orange-300 {
|
||||
border-color: var(--color-orange-300);
|
||||
}
|
||||
|
@ -1152,21 +1287,48 @@
|
|||
.border-red-200 {
|
||||
border-color: var(--color-red-200);
|
||||
}
|
||||
.border-red-400 {
|
||||
border-color: var(--color-red-400);
|
||||
}
|
||||
.border-slate-200 {
|
||||
border-color: var(--color-slate-200);
|
||||
}
|
||||
.border-slate-200\/50 {
|
||||
border-color: color-mix(in srgb, oklch(92.9% 0.013 255.508) 50%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-color: color-mix(in oklab, var(--color-slate-200) 50%, transparent);
|
||||
}
|
||||
}
|
||||
.border-white {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
.border-yellow-200 {
|
||||
border-color: var(--color-yellow-200);
|
||||
}
|
||||
.border-yellow-400 {
|
||||
border-color: var(--color-yellow-400);
|
||||
}
|
||||
.border-t-transparent {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
.bg-amber-400 {
|
||||
background-color: var(--color-amber-400);
|
||||
}
|
||||
.bg-black {
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
.bg-blue-50 {
|
||||
background-color: var(--color-blue-50);
|
||||
}
|
||||
.bg-blue-100 {
|
||||
background-color: var(--color-blue-100);
|
||||
}
|
||||
.bg-blue-500 {
|
||||
background-color: var(--color-blue-500);
|
||||
}
|
||||
.bg-emerald-400 {
|
||||
background-color: var(--color-emerald-400);
|
||||
}
|
||||
.bg-gray-50 {
|
||||
background-color: var(--color-gray-50);
|
||||
}
|
||||
|
@ -1176,6 +1338,12 @@
|
|||
.bg-gray-200 {
|
||||
background-color: var(--color-gray-200);
|
||||
}
|
||||
.bg-gray-400 {
|
||||
background-color: var(--color-gray-400);
|
||||
}
|
||||
.bg-gray-500 {
|
||||
background-color: var(--color-gray-500);
|
||||
}
|
||||
.bg-gray-800 {
|
||||
background-color: var(--color-gray-800);
|
||||
}
|
||||
|
@ -1191,6 +1359,9 @@
|
|||
.bg-green-500 {
|
||||
background-color: var(--color-green-500);
|
||||
}
|
||||
.bg-green-600 {
|
||||
background-color: var(--color-green-600);
|
||||
}
|
||||
.bg-orange-50 {
|
||||
background-color: var(--color-orange-50);
|
||||
}
|
||||
|
@ -1209,18 +1380,39 @@
|
|||
.bg-red-100 {
|
||||
background-color: var(--color-red-100);
|
||||
}
|
||||
.bg-red-400 {
|
||||
background-color: var(--color-red-400);
|
||||
}
|
||||
.bg-red-500 {
|
||||
background-color: var(--color-red-500);
|
||||
}
|
||||
.bg-slate-100 {
|
||||
background-color: var(--color-slate-100);
|
||||
}
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
.bg-white {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
.bg-white\/5 {
|
||||
background-color: color-mix(in srgb, #fff 5%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 5%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/10 {
|
||||
background-color: color-mix(in srgb, #fff 10%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-yellow-50 {
|
||||
background-color: var(--color-yellow-50);
|
||||
}
|
||||
.bg-yellow-100 {
|
||||
background-color: var(--color-yellow-100);
|
||||
}
|
||||
.bg-yellow-400 {
|
||||
background-color: var(--color-yellow-400);
|
||||
}
|
||||
|
@ -1235,10 +1427,30 @@
|
|||
--tw-gradient-position: to right in oklab;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
.from-amber-50 {
|
||||
--tw-gradient-from: var(--color-amber-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));
|
||||
}
|
||||
.from-blue-50 {
|
||||
--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));
|
||||
}
|
||||
.from-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));
|
||||
}
|
||||
.from-blue-600 {
|
||||
--tw-gradient-from: var(--color-blue-600);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.from-emerald-50 {
|
||||
--tw-gradient-from: var(--color-emerald-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));
|
||||
}
|
||||
.from-emerald-400 {
|
||||
--tw-gradient-from: var(--color-emerald-400);
|
||||
--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-green-500 {
|
||||
--tw-gradient-from: var(--color-green-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));
|
||||
|
@ -1247,6 +1459,10 @@
|
|||
--tw-gradient-from: 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));
|
||||
}
|
||||
.from-orange-50 {
|
||||
--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));
|
||||
}
|
||||
.from-orange-100 {
|
||||
--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));
|
||||
|
@ -1259,6 +1475,22 @@
|
|||
--tw-gradient-from: var(--color-orange-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-purple-400 {
|
||||
--tw-gradient-from: var(--color-purple-400);
|
||||
--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-red-50 {
|
||||
--tw-gradient-from: var(--color-red-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));
|
||||
}
|
||||
.from-slate-50 {
|
||||
--tw-gradient-from: var(--color-slate-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));
|
||||
}
|
||||
.from-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));
|
||||
}
|
||||
.via-orange-400 {
|
||||
--tw-gradient-via: var(--color-orange-400);
|
||||
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
|
||||
|
@ -1273,10 +1505,26 @@
|
|||
--tw-gradient-to: var(--color-blue-200);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-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));
|
||||
}
|
||||
.to-green-50 {
|
||||
--tw-gradient-to: var(--color-green-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-green-400 {
|
||||
--tw-gradient-to: var(--color-green-400);
|
||||
--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-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));
|
||||
}
|
||||
.to-orange-50 {
|
||||
--tw-gradient-to: var(--color-orange-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-orange-200 {
|
||||
--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));
|
||||
|
@ -1297,6 +1545,46 @@
|
|||
--tw-gradient-to: var(--color-purple-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-purple-100 {
|
||||
--tw-gradient-to: var(--color-purple-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-purple-600 {
|
||||
--tw-gradient-to: var(--color-purple-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-red-50 {
|
||||
--tw-gradient-to: var(--color-red-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-red-100 {
|
||||
--tw-gradient-to: var(--color-red-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-red-500 {
|
||||
--tw-gradient-to: var(--color-red-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));
|
||||
}
|
||||
.to-rose-50 {
|
||||
--tw-gradient-to: var(--color-rose-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-slate-50 {
|
||||
--tw-gradient-to: var(--color-slate-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-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));
|
||||
}
|
||||
.to-teal-50 {
|
||||
--tw-gradient-to: var(--color-teal-50);
|
||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||
}
|
||||
.to-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));
|
||||
}
|
||||
.object-contain {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
@ -1453,6 +1741,9 @@
|
|||
.pb-6 {
|
||||
padding-bottom: calc(var(--spacing) * 6);
|
||||
}
|
||||
.pb-8 {
|
||||
padding-bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.pb-12 {
|
||||
padding-bottom: calc(var(--spacing) * 12);
|
||||
}
|
||||
|
@ -1554,6 +1845,15 @@
|
|||
.text-wrap {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
.text-amber-600 {
|
||||
color: var(--color-amber-600);
|
||||
}
|
||||
.text-amber-700 {
|
||||
color: var(--color-amber-700);
|
||||
}
|
||||
.text-black {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
@ -1563,6 +1863,18 @@
|
|||
.text-blue-700 {
|
||||
color: var(--color-blue-700);
|
||||
}
|
||||
.text-blue-800 {
|
||||
color: var(--color-blue-800);
|
||||
}
|
||||
.text-emerald-600 {
|
||||
color: var(--color-emerald-600);
|
||||
}
|
||||
.text-emerald-700 {
|
||||
color: var(--color-emerald-700);
|
||||
}
|
||||
.text-gray-300 {
|
||||
color: var(--color-gray-300);
|
||||
}
|
||||
.text-gray-400 {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
@ -1590,6 +1902,9 @@
|
|||
.text-green-700 {
|
||||
color: var(--color-green-700);
|
||||
}
|
||||
.text-green-800 {
|
||||
color: var(--color-green-800);
|
||||
}
|
||||
.text-orange-100 {
|
||||
color: var(--color-orange-100);
|
||||
}
|
||||
|
@ -1608,12 +1923,33 @@
|
|||
.text-red-600 {
|
||||
color: var(--color-red-600);
|
||||
}
|
||||
.text-red-700 {
|
||||
color: var(--color-red-700);
|
||||
}
|
||||
.text-red-800 {
|
||||
color: var(--color-red-800);
|
||||
}
|
||||
.text-slate-400 {
|
||||
color: var(--color-slate-400);
|
||||
}
|
||||
.text-slate-500 {
|
||||
color: var(--color-slate-500);
|
||||
}
|
||||
.text-slate-700 {
|
||||
color: var(--color-slate-700);
|
||||
}
|
||||
.text-slate-800 {
|
||||
color: var(--color-slate-800);
|
||||
}
|
||||
.text-white {
|
||||
color: var(--color-white);
|
||||
}
|
||||
.text-yellow-600 {
|
||||
color: var(--color-yellow-600);
|
||||
}
|
||||
.text-yellow-800 {
|
||||
color: var(--color-yellow-800);
|
||||
}
|
||||
.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
@ -1684,9 +2020,16 @@
|
|||
--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);
|
||||
}
|
||||
.ring-4 {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + 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-orange-200 {
|
||||
--tw-ring-color: var(--color-orange-200);
|
||||
}
|
||||
.ring-white {
|
||||
--tw-ring-color: var(--color-white);
|
||||
}
|
||||
.outline {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
|
@ -1720,6 +2063,11 @@
|
|||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-sm {
|
||||
--tw-backdrop-blur: blur(var(--blur-sm));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-filter {
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
|
@ -1841,6 +2189,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-blue-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-blue-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-gray-50 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -1862,6 +2217,27 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-gray-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-gray-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-green-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-green-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-green-700 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-green-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-orange-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -1893,6 +2269,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-white\/20 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: color-mix(in srgb, #fff 20%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:from-orange-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -1931,6 +2317,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:text-orange-700 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-orange-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:text-orange-800 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -1938,6 +2331,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.hover\:no-underline {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:shadow-lg {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
|
@ -1956,6 +2356,11 @@
|
|||
border-color: var(--color-red-500);
|
||||
}
|
||||
}
|
||||
.focus\:border-transparent {
|
||||
&:focus {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
.focus\:ring-2 {
|
||||
&:focus {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
|
@ -1972,11 +2377,22 @@
|
|||
--tw-ring-color: var(--color-orange-400);
|
||||
}
|
||||
}
|
||||
.focus\:ring-orange-500 {
|
||||
&:focus {
|
||||
--tw-ring-color: var(--color-orange-500);
|
||||
}
|
||||
}
|
||||
.focus\:ring-red-200 {
|
||||
&:focus {
|
||||
--tw-ring-color: var(--color-red-200);
|
||||
}
|
||||
}
|
||||
.focus\:outline-none {
|
||||
&:focus {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@property --tw-translate-x {
|
||||
syntax: "*";
|
||||
|
|
Loading…
Reference in New Issue