feat: landing page
|
|
@ -0,0 +1,77 @@
|
||||||
|
# .NET Core
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.cache
|
||||||
|
*.dll
|
||||||
|
*.pdb
|
||||||
|
*.exe
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Visual Studio
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Node modules
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Compiled CSS (generated by Tailwind)
|
||||||
|
wwwroot/css/site.css
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Publish
|
||||||
|
publish/
|
||||||
|
*.publish.xml
|
||||||
|
|
||||||
|
# Package files
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<WarningsNotAsErrors>NU1608</WarningsNotAsErrors>
|
||||||
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.22.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="appsettings.Development.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using BankSampahApp.Models;
|
||||||
|
using BankSampahApp.Services;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller utama untuk halaman Home dengan .NET 9 best practices
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<HomeController> _logger;
|
||||||
|
private readonly IHomeService _homeService;
|
||||||
|
private readonly IAppConfigurationService _appConfig;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor dengan dependency injection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger untuk logging</param>
|
||||||
|
/// <param name="homeService">Service untuk logic bisnis home</param>
|
||||||
|
/// <param name="appConfig">Application configuration service</param>
|
||||||
|
public HomeController(
|
||||||
|
ILogger<HomeController> logger,
|
||||||
|
IHomeService homeService,
|
||||||
|
IAppConfigurationService appConfig)
|
||||||
|
{
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_homeService = homeService ?? throw new ArgumentNullException(nameof(homeService));
|
||||||
|
_appConfig = appConfig ?? throw new ArgumentNullException(nameof(appConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Halaman utama aplikasi
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>View dengan HomeViewModel</returns>
|
||||||
|
[HttpGet("")]
|
||||||
|
[HttpGet("Index")]
|
||||||
|
[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new string[] { })]
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _logger.BeginScope(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Action"] = nameof(Index),
|
||||||
|
["Controller"] = nameof(HomeController)
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("Loading home page");
|
||||||
|
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var model = await _homeService.GetHomeViewModelAsync();
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
_logger.LogInformation("Home page loaded successfully in {ElapsedMilliseconds}ms",
|
||||||
|
stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
// Add performance metrics to ViewBag for debugging
|
||||||
|
if (_appConfig.DevelopmentSettings.EnablePerformanceMetrics)
|
||||||
|
{
|
||||||
|
ViewBag.LoadTime = stopwatch.ElapsedMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error loading home page");
|
||||||
|
|
||||||
|
// Return error view with fallback data
|
||||||
|
var fallbackModel = CreateFallbackViewModel();
|
||||||
|
return View(fallbackModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Halaman kebijakan privasi
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Privacy view</returns>
|
||||||
|
[HttpGet("Privacy")]
|
||||||
|
[ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
|
||||||
|
public IActionResult Privacy()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Privacy page accessed");
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Halaman error dengan enhanced error handling
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Error view dengan ErrorViewModel</returns>
|
||||||
|
[HttpGet("Error")]
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
public IActionResult Error()
|
||||||
|
{
|
||||||
|
var requestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||||
|
|
||||||
|
// Get exception details from HttpContext items (set by GlobalExceptionFilter)
|
||||||
|
var exception = HttpContext.Items["Exception"] as Exception;
|
||||||
|
var storedRequestId = HttpContext.Items["RequestId"] as string;
|
||||||
|
|
||||||
|
_logger.LogWarning("Error page accessed. RequestId: {RequestId}, HasException: {HasException}",
|
||||||
|
requestId, exception != null);
|
||||||
|
|
||||||
|
var model = new ErrorViewModel
|
||||||
|
{
|
||||||
|
RequestId = storedRequestId ?? requestId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add additional debug info in development
|
||||||
|
if (_appConfig.ShowDetailedExceptions)
|
||||||
|
{
|
||||||
|
ViewBag.ExceptionMessage = exception?.Message;
|
||||||
|
ViewBag.ExceptionType = exception?.GetType().Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API endpoint untuk mendapatkan statistik (untuk AJAX calls)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>JSON dengan statistik terkini</returns>
|
||||||
|
[HttpGet("api/statistics")]
|
||||||
|
[ResponseCache(Duration = 900, Location = ResponseCacheLocation.Any)]
|
||||||
|
public async Task<IActionResult> GetStatistics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("API statistics endpoint called");
|
||||||
|
|
||||||
|
var statistics = await _homeService.GetStatisticsAsync();
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
data = statistics,
|
||||||
|
timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error fetching statistics via API");
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Unable to fetch statistics",
|
||||||
|
timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create fallback view model saat terjadi error
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Fallback HomeViewModel</returns>
|
||||||
|
private HomeViewModel CreateFallbackViewModel()
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Creating fallback view model due to error");
|
||||||
|
|
||||||
|
return new HomeViewModel
|
||||||
|
{
|
||||||
|
Title = _appConfig.ApplicationName,
|
||||||
|
Subtitle = "Layanan sedang dalam pemeliharaan, silakan coba lagi nanti.",
|
||||||
|
Features = new List<FeatureModel>(),
|
||||||
|
Statistics = new StatisticsModel
|
||||||
|
{
|
||||||
|
TotalUsers = 0,
|
||||||
|
WasteCollected = 0,
|
||||||
|
RewardsDistributed = 0,
|
||||||
|
EnvironmentImpact = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Filters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Global exception filter untuk menangani error secara terpusat
|
||||||
|
/// </summary>
|
||||||
|
public class GlobalExceptionFilter : IExceptionFilter
|
||||||
|
{
|
||||||
|
private readonly ILogger<GlobalExceptionFilter> _logger;
|
||||||
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor dengan dependency injection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger untuk logging</param>
|
||||||
|
/// <param name="environment">Environment information</param>
|
||||||
|
public GlobalExceptionFilter(
|
||||||
|
ILogger<GlobalExceptionFilter> logger,
|
||||||
|
IWebHostEnvironment environment)
|
||||||
|
{
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle exception yang terjadi di aplikasi
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Exception context</param>
|
||||||
|
public void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
var exception = context.Exception;
|
||||||
|
var requestId = context.HttpContext.TraceIdentifier;
|
||||||
|
|
||||||
|
// Log the exception with details
|
||||||
|
_logger.LogError(exception,
|
||||||
|
"Unhandled exception occurred. RequestId: {RequestId}, Path: {Path}, Method: {Method}, User: {User}",
|
||||||
|
requestId,
|
||||||
|
context.HttpContext.Request.Path,
|
||||||
|
context.HttpContext.Request.Method,
|
||||||
|
context.HttpContext.User?.Identity?.Name ?? "Anonymous");
|
||||||
|
|
||||||
|
// Determine response based on request type
|
||||||
|
if (IsApiRequest(context.HttpContext.Request))
|
||||||
|
{
|
||||||
|
HandleApiException(context, exception, requestId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleWebException(context, exception, requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle exception untuk API requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Exception context</param>
|
||||||
|
/// <param name="exception">Exception yang terjadi</param>
|
||||||
|
/// <param name="requestId">Request ID</param>
|
||||||
|
private void HandleApiException(ExceptionContext context, Exception exception, string requestId)
|
||||||
|
{
|
||||||
|
var statusCode = GetStatusCodeFromException(exception);
|
||||||
|
var includeDetails = _environment.IsDevelopment();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Error = new
|
||||||
|
{
|
||||||
|
Message = includeDetails ? exception.Message : "An error occurred while processing your request.",
|
||||||
|
RequestId = requestId,
|
||||||
|
Details = includeDetails ? exception.ToString() : null,
|
||||||
|
Type = exception.GetType().Name,
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Result = new JsonResult(response)
|
||||||
|
{
|
||||||
|
StatusCode = (int)statusCode
|
||||||
|
};
|
||||||
|
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle exception untuk Web requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Exception context</param>
|
||||||
|
/// <param name="exception">Exception yang terjadi</param>
|
||||||
|
/// <param name="requestId">Request ID</param>
|
||||||
|
private void HandleWebException(ExceptionContext context, Exception exception, string requestId)
|
||||||
|
{
|
||||||
|
var statusCode = GetStatusCodeFromException(exception);
|
||||||
|
|
||||||
|
// Set status code
|
||||||
|
context.HttpContext.Response.StatusCode = (int)statusCode;
|
||||||
|
|
||||||
|
// Store exception details untuk error page
|
||||||
|
context.HttpContext.Items["Exception"] = exception;
|
||||||
|
context.HttpContext.Items["RequestId"] = requestId;
|
||||||
|
|
||||||
|
// Redirect to error page
|
||||||
|
var routeData = new RouteData();
|
||||||
|
routeData.Values["controller"] = "Home";
|
||||||
|
routeData.Values["action"] = "Error";
|
||||||
|
|
||||||
|
context.Result = new RedirectToRouteResult(routeData);
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine HTTP status code based on exception type
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">Exception</param>
|
||||||
|
/// <returns>HTTP status code</returns>
|
||||||
|
private static HttpStatusCode GetStatusCodeFromException(Exception exception)
|
||||||
|
{
|
||||||
|
return exception switch
|
||||||
|
{
|
||||||
|
ArgumentNullException => HttpStatusCode.BadRequest,
|
||||||
|
ArgumentException => HttpStatusCode.BadRequest,
|
||||||
|
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
|
||||||
|
NotImplementedException => HttpStatusCode.NotImplemented,
|
||||||
|
TimeoutException => HttpStatusCode.RequestTimeout,
|
||||||
|
_ => HttpStatusCode.InternalServerError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if request is API request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">HTTP request</param>
|
||||||
|
/// <returns>True if API request</returns>
|
||||||
|
private static bool IsApiRequest(HttpRequest request)
|
||||||
|
{
|
||||||
|
// Check if request accepts JSON or has API path
|
||||||
|
return request.Headers["Accept"].ToString().Contains("application/json") ||
|
||||||
|
request.Path.StartsWithSegments("/api") ||
|
||||||
|
request.Path.StartsWithSegments("/health");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace BankSampahApp.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Model untuk menampilkan halaman error
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ID unik untuk tracking request yang mengalami error
|
||||||
|
/// </summary>
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Property untuk mengecek apakah RequestId tersedia
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
namespace BankSampahApp.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Model untuk menampilkan fitur-fitur aplikasi Bank Sampah
|
||||||
|
/// </summary>
|
||||||
|
public class FeatureModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Icon atau emoji untuk representasi visual fitur
|
||||||
|
/// </summary>
|
||||||
|
public string Icon { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nama fitur
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deskripsi detail mengenai fitur
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace BankSampahApp.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View model untuk halaman utama aplikasi Bank Sampah
|
||||||
|
/// Berisi data yang akan ditampilkan di landing page
|
||||||
|
/// </summary>
|
||||||
|
public class HomeViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Judul utama aplikasi
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subtitle atau deskripsi singkat aplikasi
|
||||||
|
/// </summary>
|
||||||
|
public string Subtitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Daftar fitur utama aplikasi
|
||||||
|
/// </summary>
|
||||||
|
public List<FeatureModel> Features { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistik dan achievement aplikasi
|
||||||
|
/// </summary>
|
||||||
|
public StatisticsModel Statistics { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
namespace BankSampahApp.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Model untuk menampilkan statistik dan achievement aplikasi Bank Sampah
|
||||||
|
/// </summary>
|
||||||
|
public class StatisticsModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Total jumlah pengguna terdaftar
|
||||||
|
/// </summary>
|
||||||
|
public int TotalUsers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total sampah yang telah dikumpulkan (dalam kilogram)
|
||||||
|
/// </summary>
|
||||||
|
public decimal WasteCollected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total reward yang telah didistribusikan
|
||||||
|
/// </summary>
|
||||||
|
public int RewardsDistributed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dampak terhadap lingkungan (dalam persen)
|
||||||
|
/// </summary>
|
||||||
|
public decimal EnvironmentImpact { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
using BankSampahApp.Services;
|
||||||
|
using BankSampahApp.Filters;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Configure logging dengan .NET 9 improvements
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Logging.AddConsole();
|
||||||
|
builder.Logging.AddDebug();
|
||||||
|
|
||||||
|
if (builder.Environment.IsProduction())
|
||||||
|
{
|
||||||
|
var connectionString = builder.Configuration.GetConnectionString("ApplicationInsights");
|
||||||
|
if (!string.IsNullOrEmpty(connectionString))
|
||||||
|
{
|
||||||
|
builder.Logging.AddApplicationInsights(
|
||||||
|
configureTelemetryConfiguration: (config) =>
|
||||||
|
config.ConnectionString = connectionString,
|
||||||
|
configureApplicationInsightsLoggerOptions: (options) => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add services with .NET 9 enhancements
|
||||||
|
builder.Services.AddControllersWithViews(options =>
|
||||||
|
{
|
||||||
|
// Add global filters
|
||||||
|
options.Filters.Add<GlobalExceptionFilter>();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register application services
|
||||||
|
builder.Services.AddScoped<IHomeService, HomeService>();
|
||||||
|
builder.Services.AddScoped<IStatisticsService, StatisticsService>();
|
||||||
|
|
||||||
|
// Register new optimized services
|
||||||
|
builder.Services.AddSingleton<ICacheService, CacheService>();
|
||||||
|
builder.Services.AddSingleton<IAppConfigurationService, AppConfigurationService>();
|
||||||
|
builder.Services.AddScoped<IValidationService, ValidationService>();
|
||||||
|
|
||||||
|
// Add HTTP client with resilience
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// Configure forwarded headers for reverse proxy scenarios
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add OpenAPI support for .NET 9
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add response compression
|
||||||
|
builder.Services.AddResponseCompression(options =>
|
||||||
|
{
|
||||||
|
options.EnableForHttps = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add response caching
|
||||||
|
builder.Services.AddResponseCaching();
|
||||||
|
|
||||||
|
// Configure security headers
|
||||||
|
builder.Services.AddAntiforgery(options =>
|
||||||
|
{
|
||||||
|
options.HeaderName = "X-CSRF-TOKEN";
|
||||||
|
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add memory cache
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline with .NET 9 patterns
|
||||||
|
app.UseForwardedHeaders();
|
||||||
|
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Home/Error");
|
||||||
|
// Use HSTS with proper configuration
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
// Add OpenAPI in development
|
||||||
|
app.MapOpenApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security headers
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
|
||||||
|
context.Response.Headers.Append("X-Frame-Options", "DENY");
|
||||||
|
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
|
||||||
|
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
|
|
||||||
|
if (context.Request.IsHttps)
|
||||||
|
{
|
||||||
|
context.Response.Headers.Append("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseResponseCompression();
|
||||||
|
app.UseResponseCaching();
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseAntiforgery();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Map routes with .NET 9 improvements
|
||||||
|
app.MapControllerRoute(
|
||||||
|
name: "default",
|
||||||
|
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.MapGet("/health", () => Results.Ok(new {
|
||||||
|
Status = "Healthy",
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Environment = app.Environment.EnvironmentName,
|
||||||
|
Version = typeof(Program).Assembly.GetName().Version?.ToString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add graceful shutdown
|
||||||
|
app.Lifetime.ApplicationStopping.Register(() =>
|
||||||
|
{
|
||||||
|
app.Logger.LogInformation("Application is shutting down gracefully...");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
# 🌱 Bank Sampah Digital
|
||||||
|
|
||||||
|
**Aplikasi ASP.NET Core MVC untuk mengelola bank sampah dengan teknologi modern**
|
||||||
|
|
||||||
|
## 📋 Deskripsi
|
||||||
|
|
||||||
|
Bank Sampah Digital adalah aplikasi web yang memungkinkan pengguna untuk mengelola sampah dengan sistem reward yang menarik. Aplikasi ini dibangun menggunakan ASP.NET Core MVC dengan design system DaisyUI dan Tailwind CSS untuk menciptakan pengalaman pengguna yang modern dan responsif.
|
||||||
|
|
||||||
|
## 📁 Struktur Proyek
|
||||||
|
|
||||||
|
```
|
||||||
|
bank-sampah/
|
||||||
|
├── Controllers/ # MVC Controllers
|
||||||
|
│ └── HomeController.cs # Controller utama dengan .NET 9 best practices
|
||||||
|
├── Services/ # Business Logic Services
|
||||||
|
│ ├── IHomeService.cs # Interface untuk home service
|
||||||
|
│ ├── HomeService.cs # Implementation home service
|
||||||
|
│ ├── IStatisticsService.cs # Interface untuk statistics service
|
||||||
|
│ └── StatisticsService.cs # Implementation statistics service
|
||||||
|
├── Filters/ # Global Filters
|
||||||
|
│ └── GlobalExceptionFilter.cs # Global exception handling
|
||||||
|
├── Models/ # Data Models
|
||||||
|
│ ├── HomeViewModel.cs # ViewModel untuk halaman utama
|
||||||
|
│ ├── FeatureModel.cs # Model untuk fitur aplikasi
|
||||||
|
│ ├── StatisticsModel.cs# Model untuk statistik
|
||||||
|
│ └── ErrorViewModel.cs # Model untuk halaman error
|
||||||
|
├── Views/ # Razor Views
|
||||||
|
│ ├── Home/
|
||||||
|
│ │ ├── Index.cshtml # Landing page utama
|
||||||
|
│ │ └── Privacy.cshtml# Halaman kebijakan privasi
|
||||||
|
│ └── Shared/
|
||||||
|
│ ├── _Layout.cshtml # Layout template utama
|
||||||
|
│ └── Error.cshtml # Halaman error
|
||||||
|
├── wwwroot/ # Static Files
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── input.css # Tailwind CSS input file
|
||||||
|
│ └── js/
|
||||||
|
│ └── site.js # JavaScript utilities
|
||||||
|
├── Program.cs # .NET 9 minimal hosting konfigurasi
|
||||||
|
├── appsettings.json # Configuration utama
|
||||||
|
├── appsettings.Development.json # Development configuration
|
||||||
|
├── BankSampahApp.csproj # .NET 9 project file
|
||||||
|
├── package.json # Dependencies untuk Node.js
|
||||||
|
└── tailwind.config.js # Konfigurasi Tailwind & DaisyUI
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Instalasi & Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- .NET 9.0 SDK atau lebih baru
|
||||||
|
- Node.js 18+ dan npm (untuk Tailwind CSS)
|
||||||
|
- Visual Studio 2022 17.8+ atau VS Code dengan C# extension
|
||||||
|
|
||||||
|
### Langkah Instalasi
|
||||||
|
|
||||||
|
1. **Clone repository**
|
||||||
|
```bash
|
||||||
|
git clone [repository-url]
|
||||||
|
cd bank-sampah
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies Node.js**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build CSS dengan Tailwind**
|
||||||
|
```bash
|
||||||
|
npm run build-css
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Restore .NET packages**
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Jalankan aplikasi**
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Akses aplikasi**
|
||||||
|
- Buka browser dan kunjungi: `https://localhost:5001` atau `http://localhost:5000`
|
||||||
|
|
||||||
|
## 💻 Development
|
||||||
|
|
||||||
|
### Menjalankan dalam Mode Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Watch CSS changes
|
||||||
|
npm run build-css
|
||||||
|
|
||||||
|
# Terminal 2: Run aplikasi dengan hot reload
|
||||||
|
dotnet watch run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Production Deployment
|
||||||
|
|
||||||
|
### Build untuk Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build optimized CSS
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Publish aplikasi .NET
|
||||||
|
dotnet publish -c Release -o ./publish
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of centralized configuration service
|
||||||
|
/// </summary>
|
||||||
|
public class AppConfigurationService : IAppConfigurationService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly CacheSettings _cacheSettings;
|
||||||
|
private readonly StatisticsSettings _statisticsSettings;
|
||||||
|
private readonly DevelopmentSettings _developmentSettings;
|
||||||
|
|
||||||
|
public AppConfigurationService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
|
||||||
|
_cacheSettings = new CacheSettings();
|
||||||
|
_configuration.GetSection("Cache").Bind(_cacheSettings);
|
||||||
|
|
||||||
|
_statisticsSettings = new StatisticsSettings();
|
||||||
|
_configuration.GetSection("Statistics").Bind(_statisticsSettings);
|
||||||
|
|
||||||
|
_developmentSettings = new DevelopmentSettings();
|
||||||
|
_configuration.GetSection("Development").Bind(_developmentSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public T GetValue<T>(string key, T defaultValue)
|
||||||
|
{
|
||||||
|
return _configuration.GetValue<T>(key) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string ApplicationName => GetValue("Application:Name", "Bank Sampah Digital");
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool ShowDetailedExceptions => _developmentSettings.DetailedExceptions;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public CacheSettings CacheSettings => _cacheSettings;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public StatisticsSettings StatisticsSettings => _statisticsSettings;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DevelopmentSettings DevelopmentSettings => _developmentSettings;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base service class yang menyediakan functionality umum untuk semua services
|
||||||
|
/// Menerapkan prinsip DRY dengan mengekstrak common patterns
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseService
|
||||||
|
{
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
protected readonly IMemoryCache _cache;
|
||||||
|
protected readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor untuk base service
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger instance</param>
|
||||||
|
/// <param name="cache">Memory cache instance</param>
|
||||||
|
/// <param name="configuration">Configuration instance</param>
|
||||||
|
protected BaseService(ILogger logger, IMemoryCache cache, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||||
|
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a function with comprehensive error handling and logging
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Return type</typeparam>
|
||||||
|
/// <param name="operation">Function to execute</param>
|
||||||
|
/// <param name="operationName">Name of the operation for logging</param>
|
||||||
|
/// <param name="fallbackValue">Fallback value if operation fails</param>
|
||||||
|
/// <returns>Result of operation or fallback value</returns>
|
||||||
|
protected async Task<T> ExecuteWithErrorHandlingAsync<T>(
|
||||||
|
Func<Task<T>> operation,
|
||||||
|
string operationName,
|
||||||
|
T? fallbackValue = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting operation: {OperationName}", operationName);
|
||||||
|
|
||||||
|
var result = await operation();
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully completed operation: {OperationName}", operationName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in operation: {OperationName}", operationName);
|
||||||
|
|
||||||
|
if (fallbackValue is not null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Returning fallback value for operation: {OperationName}", operationName);
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets cached data with configurable expiration
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of cached data</typeparam>
|
||||||
|
/// <param name="cacheKey">Cache key</param>
|
||||||
|
/// <param name="dataProvider">Function to get data if not cached</param>
|
||||||
|
/// <param name="expiration">Cache expiration time</param>
|
||||||
|
/// <param name="priority">Cache priority</param>
|
||||||
|
/// <returns>Cached or fresh data</returns>
|
||||||
|
protected async Task<T> GetOrSetCacheAsync<T>(
|
||||||
|
string cacheKey,
|
||||||
|
Func<Task<T>> dataProvider,
|
||||||
|
TimeSpan expiration,
|
||||||
|
CacheItemPriority priority = CacheItemPriority.Normal)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(cacheKey, out T? cachedValue))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Retrieved data from cache with key: {CacheKey}", cacheKey);
|
||||||
|
return cachedValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Cache miss for key: {CacheKey}, fetching fresh data", cacheKey);
|
||||||
|
|
||||||
|
var data = await dataProvider();
|
||||||
|
|
||||||
|
var cacheOptions = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = expiration,
|
||||||
|
Priority = priority
|
||||||
|
};
|
||||||
|
|
||||||
|
_cache.Set(cacheKey, data, cacheOptions);
|
||||||
|
|
||||||
|
_logger.LogDebug("Cached data with key: {CacheKey} for {Expiration}", cacheKey, expiration);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets cached data with sliding expiration
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of cached data</typeparam>
|
||||||
|
/// <param name="cacheKey">Cache key</param>
|
||||||
|
/// <param name="dataProvider">Function to get data if not cached</param>
|
||||||
|
/// <param name="slidingExpiration">Sliding expiration time</param>
|
||||||
|
/// <param name="absoluteExpiration">Absolute expiration time</param>
|
||||||
|
/// <param name="priority">Cache priority</param>
|
||||||
|
/// <returns>Cached or fresh data</returns>
|
||||||
|
protected async Task<T> GetOrSetCacheWithSlidingAsync<T>(
|
||||||
|
string cacheKey,
|
||||||
|
Func<Task<T>> dataProvider,
|
||||||
|
TimeSpan slidingExpiration,
|
||||||
|
TimeSpan absoluteExpiration,
|
||||||
|
CacheItemPriority priority = CacheItemPriority.Normal)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(cacheKey, out T? cachedValue))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Retrieved data from cache with key: {CacheKey}", cacheKey);
|
||||||
|
return cachedValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Cache miss for key: {CacheKey}, fetching fresh data", cacheKey);
|
||||||
|
|
||||||
|
var data = await dataProvider();
|
||||||
|
|
||||||
|
var cacheOptions = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = slidingExpiration,
|
||||||
|
AbsoluteExpirationRelativeToNow = absoluteExpiration,
|
||||||
|
Priority = priority
|
||||||
|
};
|
||||||
|
|
||||||
|
_cache.Set(cacheKey, data, cacheOptions);
|
||||||
|
|
||||||
|
_logger.LogDebug("Cached data with key: {CacheKey} with sliding expiration", cacheKey);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes data from cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cacheKey">Cache key to remove</param>
|
||||||
|
protected void RemoveFromCache(string cacheKey)
|
||||||
|
{
|
||||||
|
_cache.Remove(cacheKey);
|
||||||
|
_logger.LogDebug("Removed cache entry with key: {CacheKey}", cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets configuration value with fallback
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of configuration value</typeparam>
|
||||||
|
/// <param name="key">Configuration key</param>
|
||||||
|
/// <param name="defaultValue">Default value if key not found</param>
|
||||||
|
/// <returns>Configuration value or default</returns>
|
||||||
|
protected T GetConfigurationValue<T>(string key, T defaultValue)
|
||||||
|
{
|
||||||
|
return _configuration.GetValue<T>(key) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates arguments for null values
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="arguments">Dictionary of argument names and values</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown if any argument is null</exception>
|
||||||
|
protected static void ValidateArguments(Dictionary<string, object?> arguments)
|
||||||
|
{
|
||||||
|
foreach (var arg in arguments)
|
||||||
|
{
|
||||||
|
if (arg.Value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(arg.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a logging scope with operation context
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="operationName">Name of the operation</param>
|
||||||
|
/// <param name="additionalContext">Additional context data</param>
|
||||||
|
/// <returns>Disposable logging scope</returns>
|
||||||
|
protected IDisposable? CreateLogScope(string operationName, Dictionary<string, object>? additionalContext = null)
|
||||||
|
{
|
||||||
|
var scopeData = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Operation"] = operationName,
|
||||||
|
["Service"] = GetType().Name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalContext != null)
|
||||||
|
{
|
||||||
|
foreach (var item in additionalContext)
|
||||||
|
{
|
||||||
|
scopeData[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _logger.BeginScope(scopeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of cache service dengan enhanced functionality
|
||||||
|
/// </summary>
|
||||||
|
public class CacheService : ICacheService
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly ILogger<CacheService> _logger;
|
||||||
|
private readonly ConcurrentDictionary<string, bool> _cacheKeys;
|
||||||
|
|
||||||
|
public CacheService(IMemoryCache cache, ILogger<CacheService> logger)
|
||||||
|
{
|
||||||
|
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_cacheKeys = new ConcurrentDictionary<string, bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> provider, TimeSpan expiration, CacheItemPriority priority = CacheItemPriority.Normal)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(key, out T? cachedValue))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Cache hit for key: {CacheKey}", key);
|
||||||
|
return cachedValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Cache miss for key: {CacheKey}, fetching fresh data", key);
|
||||||
|
|
||||||
|
var data = await provider();
|
||||||
|
Set(key, data, expiration, priority);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<T> GetOrSetWithSlidingAsync<T>(string key, Func<Task<T>> provider, TimeSpan slidingExpiration, TimeSpan absoluteExpiration, CacheItemPriority priority = CacheItemPriority.Normal)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(key, out T? cachedValue))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Cache hit for key: {CacheKey}", key);
|
||||||
|
return cachedValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Cache miss for key: {CacheKey}, fetching fresh data", key);
|
||||||
|
|
||||||
|
var data = await provider();
|
||||||
|
|
||||||
|
var cacheOptions = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = slidingExpiration,
|
||||||
|
AbsoluteExpirationRelativeToNow = absoluteExpiration,
|
||||||
|
Priority = priority,
|
||||||
|
PostEvictionCallbacks = { new PostEvictionCallbackRegistration
|
||||||
|
{
|
||||||
|
EvictionCallback = (key, value, reason, state) =>
|
||||||
|
{
|
||||||
|
_cacheKeys.TryRemove(key.ToString()!, out _);
|
||||||
|
_logger.LogDebug("Cache entry evicted: {Key}, Reason: {Reason}", key, reason);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
};
|
||||||
|
|
||||||
|
_cache.Set(key, data, cacheOptions);
|
||||||
|
_cacheKeys.TryAdd(key, true);
|
||||||
|
|
||||||
|
_logger.LogDebug("Cached data with sliding expiration for key: {CacheKey}", key);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public T? Get<T>(string key)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(key, out T? value))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Retrieved cached value for key: {CacheKey}", key);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Cache miss for key: {CacheKey}", key);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Set<T>(string key, T value, TimeSpan expiration, CacheItemPriority priority = CacheItemPriority.Normal)
|
||||||
|
{
|
||||||
|
var cacheOptions = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = expiration,
|
||||||
|
Priority = priority,
|
||||||
|
PostEvictionCallbacks = { new PostEvictionCallbackRegistration
|
||||||
|
{
|
||||||
|
EvictionCallback = (key, value, reason, state) =>
|
||||||
|
{
|
||||||
|
_cacheKeys.TryRemove(key.ToString()!, out _);
|
||||||
|
_logger.LogDebug("Cache entry evicted: {Key}, Reason: {Reason}", key, reason);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
};
|
||||||
|
|
||||||
|
_cache.Set(key, value, cacheOptions);
|
||||||
|
_cacheKeys.TryAdd(key, true);
|
||||||
|
|
||||||
|
_logger.LogDebug("Cached value for key: {CacheKey}, Expiration: {Expiration}", key, expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
_cache.Remove(key);
|
||||||
|
_cacheKeys.TryRemove(key, out _);
|
||||||
|
_logger.LogDebug("Removed cache entry for key: {CacheKey}", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RemoveByPattern(string pattern)
|
||||||
|
{
|
||||||
|
var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
var keysToRemove = _cacheKeys.Keys.Where(key => regex.IsMatch(key)).ToList();
|
||||||
|
|
||||||
|
foreach (var key in keysToRemove)
|
||||||
|
{
|
||||||
|
Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Removed {Count} cache entries matching pattern: {Pattern}", keysToRemove.Count, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Exists(string key)
|
||||||
|
{
|
||||||
|
return _cacheKeys.ContainsKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
using BankSampahApp.Models;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service implementation untuk menangani logic bisnis halaman Home
|
||||||
|
/// Menggunakan base service untuk common functionality
|
||||||
|
/// </summary>
|
||||||
|
public class HomeService : BaseService, IHomeService
|
||||||
|
{
|
||||||
|
private readonly IStatisticsService _statisticsService;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
|
private readonly IAppConfigurationService _appConfig;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache keys untuk optimasi performa
|
||||||
|
/// </summary>
|
||||||
|
private static class CacheKeys
|
||||||
|
{
|
||||||
|
public const string Features = "home_features";
|
||||||
|
public const string Statistics = "home_statistics";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor dengan dependency injection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger untuk logging</param>
|
||||||
|
/// <param name="cache">Memory cache untuk caching</param>
|
||||||
|
/// <param name="configuration">Configuration service</param>
|
||||||
|
/// <param name="statisticsService">Service untuk statistik</param>
|
||||||
|
/// <param name="cacheService">Cache service</param>
|
||||||
|
/// <param name="appConfig">Application configuration service</param>
|
||||||
|
public HomeService(
|
||||||
|
ILogger<HomeService> logger,
|
||||||
|
Microsoft.Extensions.Caching.Memory.IMemoryCache cache,
|
||||||
|
IConfiguration configuration,
|
||||||
|
IStatisticsService statisticsService,
|
||||||
|
ICacheService cacheService,
|
||||||
|
IAppConfigurationService appConfig) : base(logger, cache, configuration)
|
||||||
|
{
|
||||||
|
_statisticsService = statisticsService ?? throw new ArgumentNullException(nameof(statisticsService));
|
||||||
|
_cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
|
||||||
|
_appConfig = appConfig ?? throw new ArgumentNullException(nameof(appConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<HomeViewModel> GetHomeViewModelAsync()
|
||||||
|
{
|
||||||
|
return await ExecuteWithErrorHandlingAsync(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var features = await GetFeaturesAsync();
|
||||||
|
var statistics = await GetStatisticsAsync();
|
||||||
|
|
||||||
|
return new HomeViewModel
|
||||||
|
{
|
||||||
|
Title = _appConfig.ApplicationName,
|
||||||
|
Subtitle = "Kelola sampah Anda dengan mudah dan dapatkan reward!",
|
||||||
|
Features = features.ToList(),
|
||||||
|
Statistics = statistics
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"Creating home view model");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<IEnumerable<FeatureModel>> GetFeaturesAsync()
|
||||||
|
{
|
||||||
|
return await _cacheService.GetOrSetAsync(
|
||||||
|
CacheKeys.Features,
|
||||||
|
LoadFeaturesFromSourceAsync,
|
||||||
|
_appConfig.CacheSettings.FeaturesExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads features from source
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List of features</returns>
|
||||||
|
private async Task<IEnumerable<FeatureModel>> LoadFeaturesFromSourceAsync()
|
||||||
|
{
|
||||||
|
using var scope = CreateLogScope("LoadFeaturesFromSource");
|
||||||
|
|
||||||
|
// Simulate async data loading
|
||||||
|
if (_appConfig.DevelopmentSettings.SimulatedDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(_appConfig.DevelopmentSettings.SimulatedDelayMs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Task.Delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var features = new List<FeatureModel>
|
||||||
|
{
|
||||||
|
CreateFeature("♻️", "Kelola Sampah",
|
||||||
|
"Catat dan kelola sampah Anda dengan sistem digital yang mudah"),
|
||||||
|
|
||||||
|
CreateFeature("💰", "Dapatkan Reward",
|
||||||
|
"Tukar poin sampah Anda dengan berbagai hadiah menarik"),
|
||||||
|
|
||||||
|
CreateFeature("🌱", "Go Green",
|
||||||
|
"Berkontribusi untuk lingkungan yang lebih bersih dan sehat"),
|
||||||
|
|
||||||
|
CreateFeature("📊", "Tracking Real-time",
|
||||||
|
"Monitor progress dan statistik sampah Anda secara real-time")
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully loaded {Count} features", features.Count);
|
||||||
|
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<StatisticsModel> GetStatisticsAsync()
|
||||||
|
{
|
||||||
|
return await _cacheService.GetOrSetWithSlidingAsync(
|
||||||
|
CacheKeys.Statistics,
|
||||||
|
() => _statisticsService.GetCurrentStatisticsAsync(),
|
||||||
|
_appConfig.CacheSettings.StatisticsSlidingExpiration,
|
||||||
|
_appConfig.CacheSettings.StatisticsExpiration,
|
||||||
|
Microsoft.Extensions.Caching.Memory.CacheItemPriority.High);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method untuk membuat FeatureModel dengan DRY principle
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="icon">Icon fitur</param>
|
||||||
|
/// <param name="title">Judul fitur</param>
|
||||||
|
/// <param name="description">Deskripsi fitur</param>
|
||||||
|
/// <returns>FeatureModel instance</returns>
|
||||||
|
private static FeatureModel CreateFeature(string icon, string title, string description)
|
||||||
|
{
|
||||||
|
return new FeatureModel
|
||||||
|
{
|
||||||
|
Icon = icon,
|
||||||
|
Title = title,
|
||||||
|
Description = description
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface untuk centralized configuration management
|
||||||
|
/// </summary>
|
||||||
|
public interface IAppConfigurationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets application configuration with fallback
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of configuration value</typeparam>
|
||||||
|
/// <param name="key">Configuration key</param>
|
||||||
|
/// <param name="defaultValue">Default value if key not found</param>
|
||||||
|
/// <returns>Configuration value or default</returns>
|
||||||
|
T GetValue<T>(string key, T defaultValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets application name from configuration
|
||||||
|
/// </summary>
|
||||||
|
string ApplicationName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether detailed exceptions should be shown
|
||||||
|
/// </summary>
|
||||||
|
bool ShowDetailedExceptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets cache configuration settings
|
||||||
|
/// </summary>
|
||||||
|
CacheSettings CacheSettings { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics configuration settings
|
||||||
|
/// </summary>
|
||||||
|
StatisticsSettings StatisticsSettings { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets development configuration settings
|
||||||
|
/// </summary>
|
||||||
|
DevelopmentSettings DevelopmentSettings { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache configuration settings
|
||||||
|
/// </summary>
|
||||||
|
public class CacheSettings
|
||||||
|
{
|
||||||
|
public TimeSpan FeaturesExpiration { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
public TimeSpan StatisticsExpiration { get; set; } = TimeSpan.FromMinutes(30);
|
||||||
|
public TimeSpan StatisticsSlidingExpiration { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
public TimeSpan PrivacyPageExpiration { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics configuration settings
|
||||||
|
/// </summary>
|
||||||
|
public class StatisticsSettings
|
||||||
|
{
|
||||||
|
public int BaseUsers { get; set; } = 1000;
|
||||||
|
public decimal BaseWaste { get; set; } = 15.0m;
|
||||||
|
public int BaseRewards { get; set; } = 890;
|
||||||
|
public decimal BaseEnvironmentImpact { get; set; } = 95.0m;
|
||||||
|
public double GrowthFactorMultiplier { get; set; } = 0.001;
|
||||||
|
public double MaxGrowthFactor { get; set; } = 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Development configuration settings
|
||||||
|
/// </summary>
|
||||||
|
public class DevelopmentSettings
|
||||||
|
{
|
||||||
|
public bool DetailedExceptions { get; set; } = false;
|
||||||
|
public bool EnablePerformanceMetrics { get; set; } = false;
|
||||||
|
public int SimulatedDelayMs { get; set; } = 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface untuk cache service yang mengelola caching operations
|
||||||
|
/// </summary>
|
||||||
|
public interface ICacheService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets cached value or sets it using the provider function
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of cached data</typeparam>
|
||||||
|
/// <param name="key">Cache key</param>
|
||||||
|
/// <param name="provider">Function to provide data if not in cache</param>
|
||||||
|
/// <param name="expiration">Cache expiration time</param>
|
||||||
|
/// <param name="priority">Cache priority</param>
|
||||||
|
/// <returns>Cached or fresh data</returns>
|
||||||
|
Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> provider, TimeSpan expiration, CacheItemPriority priority = CacheItemPriority.Normal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets cached value or sets it using sliding expiration
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of cached data</typeparam>
|
||||||
|
/// <param name="key">Cache key</param>
|
||||||
|
/// <param name="provider">Function to provide data if not in cache</param>
|
||||||
|
/// <param name="slidingExpiration">Sliding expiration time</param>
|
||||||
|
/// <param name="absoluteExpiration">Absolute expiration time</param>
|
||||||
|
/// <param name="priority">Cache priority</param>
|
||||||
|
/// <returns>Cached or fresh data</returns>
|
||||||
|
Task<T> GetOrSetWithSlidingAsync<T>(string key, Func<Task<T>> provider, TimeSpan slidingExpiration, TimeSpan absoluteExpiration, CacheItemPriority priority = CacheItemPriority.Normal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets value from cache
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of cached data</typeparam>
|
||||||
|
/// <param name="key">Cache key</param>
|
||||||
|
/// <returns>Cached value or default</returns>
|
||||||
|
T? Get<T>(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets value in cache
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of data to cache</typeparam>
|
||||||
|
/// <param name="key">Cache key</param>
|
||||||
|
/// <param name="value">Value to cache</param>
|
||||||
|
/// <param name="expiration">Cache expiration time</param>
|
||||||
|
/// <param name="priority">Cache priority</param>
|
||||||
|
void Set<T>(string key, T value, TimeSpan expiration, CacheItemPriority priority = CacheItemPriority.Normal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes value from cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Cache key</param>
|
||||||
|
void Remove(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes multiple cache entries by pattern
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">Pattern to match cache keys</param>
|
||||||
|
void RemoveByPattern(string pattern);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if key exists in cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Cache key</param>
|
||||||
|
/// <returns>True if key exists</returns>
|
||||||
|
bool Exists(string key);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
using BankSampahApp.Models;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface untuk service yang menangani logic bisnis halaman Home
|
||||||
|
/// </summary>
|
||||||
|
public interface IHomeService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mendapatkan data view model untuk halaman utama
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>HomeViewModel yang berisi data untuk ditampilkan</returns>
|
||||||
|
Task<HomeViewModel> GetHomeViewModelAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mendapatkan daftar fitur aplikasi
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List fitur aplikasi</returns>
|
||||||
|
Task<IEnumerable<FeatureModel>> GetFeaturesAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mendapatkan statistik aplikasi
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Model statistik aplikasi</returns>
|
||||||
|
Task<StatisticsModel> GetStatisticsAsync();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
using BankSampahApp.Models;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface untuk service yang menangani statistik aplikasi
|
||||||
|
/// </summary>
|
||||||
|
public interface IStatisticsService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mendapatkan statistik terkini dari aplikasi
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Model statistik terkini</returns>
|
||||||
|
Task<StatisticsModel> GetCurrentStatisticsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mendapatkan statistik berdasarkan periode waktu
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startDate">Tanggal mulai</param>
|
||||||
|
/// <param name="endDate">Tanggal akhir</param>
|
||||||
|
/// <returns>Model statistik untuk periode tertentu</returns>
|
||||||
|
Task<StatisticsModel> GetStatisticsByPeriodAsync(DateTime startDate, DateTime endDate);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update statistik dengan data baru
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statistics">Data statistik baru</param>
|
||||||
|
/// <returns>True jika berhasil update</returns>
|
||||||
|
Task<bool> UpdateStatisticsAsync(StatisticsModel statistics);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
using BankSampahApp.Models;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface untuk common validation logic
|
||||||
|
/// </summary>
|
||||||
|
public interface IValidationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates statistics model
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="statistics">Statistics to validate</param>
|
||||||
|
/// <returns>Validation result</returns>
|
||||||
|
ValidationResult ValidateStatistics(StatisticsModel statistics);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates feature model
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="feature">Feature to validate</param>
|
||||||
|
/// <returns>Validation result</returns>
|
||||||
|
ValidationResult ValidateFeature(FeatureModel feature);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates date range
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startDate">Start date</param>
|
||||||
|
/// <param name="endDate">End date</param>
|
||||||
|
/// <returns>Validation result</returns>
|
||||||
|
ValidationResult ValidateDateRange(DateTime startDate, DateTime endDate);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates required string fields
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value to validate</param>
|
||||||
|
/// <param name="fieldName">Name of the field</param>
|
||||||
|
/// <param name="maxLength">Maximum length allowed</param>
|
||||||
|
/// <returns>Validation result</returns>
|
||||||
|
ValidationResult ValidateRequiredString(string? value, string fieldName, int maxLength = int.MaxValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates numeric range
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Numeric value to validate</param>
|
||||||
|
/// <param name="fieldName">Name of the field</param>
|
||||||
|
/// <param name="minValue">Minimum allowed value</param>
|
||||||
|
/// <param name="maxValue">Maximum allowed value</param>
|
||||||
|
/// <returns>Validation result</returns>
|
||||||
|
ValidationResult ValidateNumericRange(decimal value, string fieldName, decimal minValue = 0, decimal maxValue = decimal.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of validation operation
|
||||||
|
/// </summary>
|
||||||
|
public class ValidationResult
|
||||||
|
{
|
||||||
|
public bool IsValid { get; set; }
|
||||||
|
public List<string> Errors { get; set; } = new();
|
||||||
|
|
||||||
|
public static ValidationResult Success() => new() { IsValid = true };
|
||||||
|
|
||||||
|
public static ValidationResult Failure(string error) => new()
|
||||||
|
{
|
||||||
|
IsValid = false,
|
||||||
|
Errors = new List<string> { error }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ValidationResult Failure(IEnumerable<string> errors) => new()
|
||||||
|
{
|
||||||
|
IsValid = false,
|
||||||
|
Errors = errors.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
using BankSampahApp.Models;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service implementation untuk menangani statistik aplikasi
|
||||||
|
/// Menggunakan base service untuk common functionality
|
||||||
|
/// </summary>
|
||||||
|
public class StatisticsService : BaseService, IStatisticsService
|
||||||
|
{
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
|
private readonly IAppConfigurationService _appConfig;
|
||||||
|
private readonly IValidationService _validationService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache key untuk statistik
|
||||||
|
/// </summary>
|
||||||
|
private const string StatisticsCacheKey = "current_statistics";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor dengan dependency injection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger untuk logging</param>
|
||||||
|
/// <param name="cache">Memory cache untuk caching</param>
|
||||||
|
/// <param name="configuration">Configuration untuk settings</param>
|
||||||
|
/// <param name="cacheService">Cache service</param>
|
||||||
|
/// <param name="appConfig">Application configuration service</param>
|
||||||
|
/// <param name="validationService">Validation service</param>
|
||||||
|
public StatisticsService(
|
||||||
|
ILogger<StatisticsService> logger,
|
||||||
|
Microsoft.Extensions.Caching.Memory.IMemoryCache cache,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ICacheService cacheService,
|
||||||
|
IAppConfigurationService appConfig,
|
||||||
|
IValidationService validationService) : base(logger, cache, configuration)
|
||||||
|
{
|
||||||
|
_cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
|
||||||
|
_appConfig = appConfig ?? throw new ArgumentNullException(nameof(appConfig));
|
||||||
|
_validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<StatisticsModel> GetCurrentStatisticsAsync()
|
||||||
|
{
|
||||||
|
return await ExecuteWithErrorHandlingAsync(
|
||||||
|
async () => await _cacheService.GetOrSetWithSlidingAsync(
|
||||||
|
StatisticsCacheKey,
|
||||||
|
LoadStatisticsFromSourceAsync,
|
||||||
|
_appConfig.CacheSettings.StatisticsSlidingExpiration,
|
||||||
|
_appConfig.CacheSettings.StatisticsExpiration,
|
||||||
|
Microsoft.Extensions.Caching.Memory.CacheItemPriority.High),
|
||||||
|
"Fetching current statistics",
|
||||||
|
GetDefaultStatistics());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<StatisticsModel> GetStatisticsByPeriodAsync(DateTime startDate, DateTime endDate)
|
||||||
|
{
|
||||||
|
return await ExecuteWithErrorHandlingAsync(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var validation = _validationService.ValidateDateRange(startDate, endDate);
|
||||||
|
if (!validation.IsValid)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(string.Join("; ", validation.Errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await LoadStatisticsByPeriodFromSourceAsync(startDate, endDate);
|
||||||
|
},
|
||||||
|
$"Fetching statistics for period {startDate:yyyy-MM-dd} to {endDate:yyyy-MM-dd}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<bool> UpdateStatisticsAsync(StatisticsModel statistics)
|
||||||
|
{
|
||||||
|
return await ExecuteWithErrorHandlingAsync(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(statistics);
|
||||||
|
|
||||||
|
var validation = _validationService.ValidateStatistics(statistics);
|
||||||
|
if (!validation.IsValid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid statistics data: {Errors}", string.Join("; ", validation.Errors));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate async update operation
|
||||||
|
if (_appConfig.DevelopmentSettings.SimulatedDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(_appConfig.DevelopmentSettings.SimulatedDelayMs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Task.Delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache after update
|
||||||
|
_cacheService.Remove(StatisticsCacheKey);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
"Updating statistics",
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load statistics from data source (placeholder for actual implementation)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Current statistics</returns>
|
||||||
|
private async Task<StatisticsModel> LoadStatisticsFromSourceAsync()
|
||||||
|
{
|
||||||
|
// In a real application, this would load from database, API, etc.
|
||||||
|
await Task.CompletedTask;
|
||||||
|
|
||||||
|
// Get base values from configuration
|
||||||
|
var baseUsers = _appConfig.StatisticsSettings.BaseUsers;
|
||||||
|
var baseWaste = _appConfig.StatisticsSettings.BaseWaste;
|
||||||
|
|
||||||
|
// Simulate some growth over time
|
||||||
|
var daysSinceStart = (DateTime.UtcNow - new DateTime(2024, 1, 1)).Days;
|
||||||
|
var growthFactor = Math.Min(_appConfig.StatisticsSettings.MaxGrowthFactor,
|
||||||
|
1.0 + (daysSinceStart * _appConfig.StatisticsSettings.GrowthFactorMultiplier));
|
||||||
|
|
||||||
|
return new StatisticsModel
|
||||||
|
{
|
||||||
|
TotalUsers = (int)(baseUsers * growthFactor),
|
||||||
|
WasteCollected = baseWaste * (decimal)growthFactor,
|
||||||
|
RewardsDistributed = (int)(_appConfig.StatisticsSettings.BaseRewards * growthFactor),
|
||||||
|
EnvironmentImpact = Math.Min(99.9m, _appConfig.StatisticsSettings.BaseEnvironmentImpact + (decimal)(growthFactor * 2))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load statistics for specific period (placeholder)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startDate">Start date</param>
|
||||||
|
/// <param name="endDate">End date</param>
|
||||||
|
/// <returns>Period statistics</returns>
|
||||||
|
private async Task<StatisticsModel> LoadStatisticsByPeriodFromSourceAsync(DateTime startDate, DateTime endDate)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
|
||||||
|
// Calculate period-specific statistics
|
||||||
|
var days = (endDate - startDate).Days + 1;
|
||||||
|
var dailyAverage = await GetDailyAverageAsync();
|
||||||
|
|
||||||
|
return new StatisticsModel
|
||||||
|
{
|
||||||
|
TotalUsers = dailyAverage.TotalUsers * days / 365,
|
||||||
|
WasteCollected = dailyAverage.WasteCollected * days,
|
||||||
|
RewardsDistributed = dailyAverage.RewardsDistributed * days,
|
||||||
|
EnvironmentImpact = dailyAverage.EnvironmentImpact
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get daily average statistics
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Daily average statistics</returns>
|
||||||
|
private async Task<StatisticsModel> GetDailyAverageAsync()
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
|
||||||
|
return new StatisticsModel
|
||||||
|
{
|
||||||
|
TotalUsers = 3, // New users per day
|
||||||
|
WasteCollected = 0.1m, // Tons per day
|
||||||
|
RewardsDistributed = 5, // Rewards per day
|
||||||
|
EnvironmentImpact = 98.5m // Overall impact
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get default statistics when error occurs
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Default statistics</returns>
|
||||||
|
private StatisticsModel GetDefaultStatistics()
|
||||||
|
{
|
||||||
|
return new StatisticsModel
|
||||||
|
{
|
||||||
|
TotalUsers = _appConfig.StatisticsSettings.BaseUsers,
|
||||||
|
WasteCollected = _appConfig.StatisticsSettings.BaseWaste,
|
||||||
|
RewardsDistributed = _appConfig.StatisticsSettings.BaseRewards,
|
||||||
|
EnvironmentImpact = _appConfig.StatisticsSettings.BaseEnvironmentImpact
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
using BankSampahApp.Models;
|
||||||
|
|
||||||
|
namespace BankSampahApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of validation service
|
||||||
|
/// </summary>
|
||||||
|
public class ValidationService : IValidationService
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ValidationResult ValidateStatistics(StatisticsModel statistics)
|
||||||
|
{
|
||||||
|
if (statistics == null)
|
||||||
|
return ValidationResult.Failure("Statistics model is required");
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (statistics.TotalUsers < 0)
|
||||||
|
errors.Add("Total users cannot be negative");
|
||||||
|
|
||||||
|
if (statistics.WasteCollected < 0)
|
||||||
|
errors.Add("Waste collected cannot be negative");
|
||||||
|
|
||||||
|
if (statistics.RewardsDistributed < 0)
|
||||||
|
errors.Add("Rewards distributed cannot be negative");
|
||||||
|
|
||||||
|
if (statistics.EnvironmentImpact < 0 || statistics.EnvironmentImpact > 100)
|
||||||
|
errors.Add("Environment impact must be between 0 and 100");
|
||||||
|
|
||||||
|
return errors.Any() ? ValidationResult.Failure(errors) : ValidationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ValidationResult ValidateFeature(FeatureModel feature)
|
||||||
|
{
|
||||||
|
if (feature == null)
|
||||||
|
return ValidationResult.Failure("Feature model is required");
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
var titleValidation = ValidateRequiredString(feature.Title, "Title", 100);
|
||||||
|
if (!titleValidation.IsValid)
|
||||||
|
errors.AddRange(titleValidation.Errors);
|
||||||
|
|
||||||
|
var descriptionValidation = ValidateRequiredString(feature.Description, "Description", 500);
|
||||||
|
if (!descriptionValidation.IsValid)
|
||||||
|
errors.AddRange(descriptionValidation.Errors);
|
||||||
|
|
||||||
|
var iconValidation = ValidateRequiredString(feature.Icon, "Icon", 10);
|
||||||
|
if (!iconValidation.IsValid)
|
||||||
|
errors.AddRange(iconValidation.Errors);
|
||||||
|
|
||||||
|
return errors.Any() ? ValidationResult.Failure(errors) : ValidationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ValidationResult ValidateDateRange(DateTime startDate, DateTime endDate)
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (startDate > endDate)
|
||||||
|
errors.Add("Start date cannot be greater than end date");
|
||||||
|
|
||||||
|
if (startDate > DateTime.UtcNow)
|
||||||
|
errors.Add("Start date cannot be in the future");
|
||||||
|
|
||||||
|
if (endDate > DateTime.UtcNow)
|
||||||
|
errors.Add("End date cannot be in the future");
|
||||||
|
|
||||||
|
var maxRangeDays = 365; // 1 year maximum
|
||||||
|
if ((endDate - startDate).Days > maxRangeDays)
|
||||||
|
errors.Add($"Date range cannot exceed {maxRangeDays} days");
|
||||||
|
|
||||||
|
return errors.Any() ? ValidationResult.Failure(errors) : ValidationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ValidationResult ValidateRequiredString(string? value, string fieldName, int maxLength = int.MaxValue)
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
errors.Add($"{fieldName} is required");
|
||||||
|
else if (value.Length > maxLength)
|
||||||
|
errors.Add($"{fieldName} cannot exceed {maxLength} characters");
|
||||||
|
|
||||||
|
return errors.Any() ? ValidationResult.Failure(errors) : ValidationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ValidationResult ValidateNumericRange(decimal value, string fieldName, decimal minValue = 0, decimal maxValue = decimal.MaxValue)
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (value < minValue)
|
||||||
|
errors.Add($"{fieldName} cannot be less than {minValue}");
|
||||||
|
|
||||||
|
if (value > maxValue)
|
||||||
|
errors.Add($"{fieldName} cannot be greater than {maxValue}");
|
||||||
|
|
||||||
|
return errors.Any() ? ValidationResult.Failure(errors) : ValidationResult.Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,525 @@
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Beranda";
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="relative min-h-screen bg-cover bg-center overflow-hidden" style="background-image: url('/images/bg-landing-page-header.png');">
|
||||||
|
<!-- Background overlay -->
|
||||||
|
<div class="absolute inset-0 bg-black/50"></div>
|
||||||
|
|
||||||
|
<!-- Hero Content -->
|
||||||
|
<div class="relative z-10 flex items-end min-h-screen px-4 lg:px-28 pt-28 pb-48">
|
||||||
|
<div class="w-full max-w-[1280px] mx-auto flex flex-col justify-start items-start gap-20">
|
||||||
|
<div class="flex justify-start items-center gap-8 w-full">
|
||||||
|
<div class="max-w-[800px] pr-10 flex flex-col justify-start items-start gap-8">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-8 w-full">
|
||||||
|
<!-- Badge -->
|
||||||
|
<div class="px-4 py-2 bg-green-50 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[1.5px] bg-green-900 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-green-900 text-base font-bold font-jakarta">Portal Resmi</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Title -->
|
||||||
|
<div class="flex flex-col justify-start items-start gap-2 w-full">
|
||||||
|
<h1 class="text-white hero-title font-bold font-jakarta w-full">
|
||||||
|
e-Bank Sampah Jakarta
|
||||||
|
</h1>
|
||||||
|
<p class="text-white text-base sm:text-lg font-medium font-jakarta leading-7 w-full">
|
||||||
|
Program dari Dinas Lingkungan Hidup Provinsi Jakarta
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA Buttons -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-start items-start gap-4">
|
||||||
|
<button class="btn-custom px-8 py-3 bg-green-800 hover:bg-green-700 rounded-full flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<span class="text-white text-base font-semibold font-jakarta">Pendaftaran Nasabah</span>
|
||||||
|
<div class="w-5 h-5 relative">
|
||||||
|
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="btn-custom px-8 py-3 bg-white/10 hover:bg-white/20 rounded-full border border-gray-300 flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<span class="text-white text-base font-semibold font-jakarta">Pendaftaran Bank Sampah</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- About Section -->
|
||||||
|
<section class="px-4 lg:px-28 py-24 bg-gray-50">
|
||||||
|
<div class="w-full max-w-[1280px] mx-auto flex flex-col justify-start items-start gap-20">
|
||||||
|
<div class="flex flex-col lg:flex-row justify-start items-center gap-20 w-full">
|
||||||
|
<!-- Image Section -->
|
||||||
|
<div class="w-full lg:w-[592px] h-[400px] lg:h-[592px] relative">
|
||||||
|
<div class="w-60 h-60 absolute bottom-0 left-0 bg-green-800 rounded-full"></div>
|
||||||
|
<img class="w-80 h-80 lg:w-96 lg:h-96 absolute top-12 left-10 rounded-full object-cover" src="/images/trash-rounded.png" alt="Bank Sampah Image" />
|
||||||
|
<div class="w-48 h-48 absolute top-1/2 right-10 lg:right-20 bg-white rounded-full shadow-lg flex items-center justify-center">
|
||||||
|
<img class="w-36 h-36" src="/images/logo.png" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Section -->
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-8">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-4 w-full">
|
||||||
|
<!-- Badge -->
|
||||||
|
<div class="px-4 py-2 bg-gray-300 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[1.5px] bg-green-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-green-800 text-base font-bold font-jakarta">Tentang Kami</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<h2 class="text-gray-900 text-3xl lg:text-4xl font-bold font-jakarta leading-10">e-Bank Sampah</h2>
|
||||||
|
<p class="text-slate-700 text-lg font-normal font-jakarta leading-7">
|
||||||
|
e-Bank Sampah adalah sebuah platform digital berbasis website yang menghimpun data dan informasi mengenai aktivitas di Bank Sampah Induk (BSI) dan Bank Sampah Unit (BSU).
|
||||||
|
</p>
|
||||||
|
<p class="text-slate-700 text-base font-normal font-jakarta leading-normal">
|
||||||
|
Website ini bertujuan mempermudah nasabah, BSI, dan BSU dalam melakukan pencatatan dan pemantauan transaksi yang terjadi di Bank Sampah. e-Bank Sampah juga memberikan informasi mengenai regulasi Bank Sampah, aktivitas Bank Sampah yang menarik dan bermanfaat sehingga dapat menginspirasi pegiat Bank Sampah lainnya. Di dalam E-Bank Sampah, masyarakat dapat juga mendapatkan informasi mengenai daftar lokasi Bank Sampah yang aktif melakukan pengumpulan sampah.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Publications Section -->
|
||||||
|
<section class="px-4 lg:px-28 py-24 bg-gray-100">
|
||||||
|
<div class="w-full max-w-[1280px] mx-auto flex flex-col justify-start items-start gap-20">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-14 w-full">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-8 w-full">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-4 w-full">
|
||||||
|
<!-- Badge -->
|
||||||
|
<div class="px-4 py-2 bg-green-800 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[1.5px] bg-white rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-white text-base font-bold font-jakarta">Posting Terkini</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-gray-900 text-3xl lg:text-4xl font-bold font-jakarta leading-10">Publikasi</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Articles Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full">
|
||||||
|
<!-- Article 1 -->
|
||||||
|
<article class="p-5 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
|
<img class="w-full h-64 rounded-lg object-cover" src="/images/publikasi/image-01.png" alt="Article Image" />
|
||||||
|
<div class="flex flex-col justify-start items-start gap-2 w-full">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">
|
||||||
|
Permudah Warga Jakarta Jadi Nasabah Bank Sampah untuk Pembebasan Retribusi, DLH Luncurkan Platform
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-500 text-sm font-normal font-jakarta leading-tight">
|
||||||
|
JAKARTA — Dinas Lingkungan Hidup (DLH) DKI Jakarta meluncurkan platform e-Bank Sampah Jakarta pada Kamis, (12/12). Platform banksampah.jakarta.go.id ini dirancang untuk mempermudah pengelolaan bank sampah sekaligus mendorong warga Jakarta terdaftar sebagai nasabah bank sampah sebagai upaya pengurangan sampah. Langkah ini merupakan bagian dari upaya mendukung kebijakan pembebasan retribusi kebersihan bagi warga yang aktif memilah sampah dan bertransaksi di bank sampah.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-1 pb-1 flex justify-start items-start w-full">
|
||||||
|
<a href="#" class="flex justify-center items-center gap-2 text-green-800 hover:text-green-700 transition-colors">
|
||||||
|
<span class="text-base font-semibold font-jakarta">Selengkapnya</span>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Article 2 -->
|
||||||
|
<article class="p-5 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
|
<img class="w-full h-64 rounded-lg object-cover" src="/images/publikasi/image-02.png" alt="Article Image" />
|
||||||
|
<div class="flex flex-col justify-start items-start gap-2 w-full">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">
|
||||||
|
Menteri LH Dukung Retribusi Pelayanan Kebersihan di Jakarta
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-500 text-sm font-normal font-jakarta leading-tight">
|
||||||
|
JAKARTA — Menteri Lingkungan Hidup, Hanif Faisol Nurofiq, mendukung penuh langkah Pemerintah Provinsi DKI Jakarta untuk menerapkan Retribusi Pelayanan Kebersihan mulai 1 Januari 2025. Dukungan ini disampaikan Menteri Hanif dalam acara "Kolaborasi Bersih Sampah Jakarta - Indonesia Bersih" yang berlangsung di Hutan Kota GBK, Jakarta, Minggu (17/11).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-1 pb-1 flex justify-start items-start w-full">
|
||||||
|
<a href="#" class="flex justify-center items-center gap-2 text-green-800 hover:text-green-700 transition-colors">
|
||||||
|
<span class="text-base font-semibold font-jakarta">Selengkapnya</span>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Article 3 -->
|
||||||
|
<article class="p-5 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-4">
|
||||||
|
<img class="w-full h-64 rounded-lg object-cover" src="/images/publikasi/image-03.png" alt="Article Image" />
|
||||||
|
<div class="flex flex-col justify-start items-start gap-2 w-full">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">
|
||||||
|
Permudah Warga Jakarta Jadi Nasabah Bank Sampah untuk Pembebasan Retribusi, DLH Luncurkan Platform
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-500 text-sm font-normal font-jakarta leading-tight">
|
||||||
|
JAKARTA — Dinas Lingkungan Hidup (DLH) DKI Jakarta meluncurkan platform e-Bank Sampah Jakarta pada Kamis, (12/12). Platform banksampah.jakarta.go.id ini dirancang untuk mempermudah pengelolaan bank sampah sekaligus mendorong warga Jakarta terdaftar sebagai nasabah bank sampah sebagai upaya pengurangan sampah. Langkah ini merupakan bagian dari upaya mendukung kebijakan pembebasan retribusi kebersihan bagi warga yang aktif memilah sampah dan bertransaksi di bank sampah.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-1 pb-1 flex justify-start items-start w-full">
|
||||||
|
<a href="#" class="flex justify-center items-center gap-2 text-green-800 hover:text-green-700 transition-colors">
|
||||||
|
<span class="text-base font-semibold font-jakarta">Selengkapnya</span>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Regulations Section -->
|
||||||
|
<section class="px-4 lg:px-28 py-24 bg-white">
|
||||||
|
<div class="w-full max-w-[1280px] mx-auto flex flex-col justify-start items-start gap-12">
|
||||||
|
<div class="flex flex-col lg:flex-row justify-start items-center gap-12 w-full">
|
||||||
|
<!-- Content Section -->
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-8">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-4 w-full">
|
||||||
|
<!-- Badge -->
|
||||||
|
<div class="px-4 py-2 bg-gray-300 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[1.5px] bg-green-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-green-800 text-base font-bold font-jakarta">Peraturan</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<h2 class="text-gray-900 text-3xl lg:text-4xl font-bold font-jakarta leading-10">Regulasi Terkait</h2>
|
||||||
|
|
||||||
|
<!-- Regulation Items -->
|
||||||
|
<div class="flex flex-col justify-start items-start gap-4 w-full">
|
||||||
|
<!-- Regulation 1 -->
|
||||||
|
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Permen LHK No 14/2021</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">
|
||||||
|
Perturan Menteri Lingkungan Hidup Nomor 14 Tahun 2021
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="h-14 rounded-full flex flex-col justify-center items-center">
|
||||||
|
<svg class="w-8 h-8 text-gray-500" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-700 text-base font-normal font-jakarta leading-normal w-full">
|
||||||
|
Peraturan Gubernur Nomor 77 Tahun 2020 mengatur tentang pengelolaan sampah dari sumbernya melalui peran aktif warga di tingkat RT dan RW, dengan tujuan mengurangi volume sampah yang masuk ke TPA.
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-start items-start gap-2 w-full flex-wrap">
|
||||||
|
<div class="px-3 py-2 bg-gray-300 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1px] left-[2px] bg-green-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-800 text-sm font-bold font-jakarta">Ditetapkan: 13 Juli 2020</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-2 bg-gray-300 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[1.5px] bg-green-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-800 text-sm font-bold font-jakarta">Status: Berlaku</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-2 bg-gray-300 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[2px] bg-green-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-green-800 text-sm font-bold font-jakarta">Unduh PDF</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Regulation 2 -->
|
||||||
|
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Pergub DKI Jakarta No. 77/2020</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">
|
||||||
|
Peraturan Gubernur Provinsi KDI Jakarta Nomor 77 Tahun 2020
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="h-14 rounded-full flex flex-col justify-center items-center">
|
||||||
|
<svg class="w-8 h-8 text-gray-500" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Regulation 3 -->
|
||||||
|
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Pergub DKI Jakarta No 33/2021</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">
|
||||||
|
Peraturan Gubernur Provinsi KDI Jakarta Nomor 33 Tahun 2021
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="h-14 rounded-full flex flex-col justify-center items-center">
|
||||||
|
<svg class="w-8 h-8 text-gray-500" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Section -->
|
||||||
|
<div class="w-full lg:w-[592px] h-[400px] lg:h-[592px] relative">
|
||||||
|
<div class="w-60 h-[552px] absolute top-0 right-0 bg-green-800"></div>
|
||||||
|
<img class="w-80 h-[552px] lg:w-96 lg:h-[552px] absolute top-0 right-10 object-cover" src="/images/regulasi.png" alt="Regulation Image" />
|
||||||
|
<div class="w-48 h-48 absolute bottom-[176px] left-[30px] bg-white shadow-lg flex items-center justify-center">
|
||||||
|
<img class="w-32 h-32" src="/images/logo.png" alt="Regulation Logo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Location Map Section -->
|
||||||
|
<section class="p-4 lg:p-28 bg-gray-100">
|
||||||
|
<div class="max-w-[1280px] mx-auto flex flex-col justify-start items-start gap-10">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-8 w-full">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-4 w-full">
|
||||||
|
<!-- Badge -->
|
||||||
|
<div class="px-4 py-2 bg-gray-300 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[1.5px] bg-green-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-green-800 text-base font-bold font-jakarta">Bank Sampah</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-gray-900 text-3xl lg:text-4xl font-bold font-jakarta leading-10">Lokasi Bank Sampah</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Container -->
|
||||||
|
<div class="relative h-[400px] lg:h-[624px] w-full flex justify-center items-start gap-4">
|
||||||
|
<div class="flex-1 h-full p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
|
||||||
|
<!-- Map placeholder with markers -->
|
||||||
|
<div class="w-full flex-1 relative bg-gray-200 rounded-lg overflow-hidden">
|
||||||
|
<img class="w-full h-full object-cover" src="/images/maps.png" alt="Jakarta Map" />
|
||||||
|
|
||||||
|
<!-- Map Markers -->
|
||||||
|
<div class="absolute inset-0">
|
||||||
|
<!-- Sample markers positioned across Jakarta -->
|
||||||
|
<div class="absolute top-[30%] left-[15%] p-2 bg-green-800 rounded-full map-marker">
|
||||||
|
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-[25%] left-[35%] p-2 bg-green-800 rounded-full map-marker">
|
||||||
|
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-[40%] left-[50%] p-2 bg-green-800 rounded-full map-marker">
|
||||||
|
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-[55%] left-[25%] p-2 bg-green-800 rounded-full map-marker">
|
||||||
|
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-[35%] left-[70%] p-2 bg-green-800 rounded-full map-marker">
|
||||||
|
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- More markers can be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Help Desk Section -->
|
||||||
|
<section class="px-4 lg:px-28 py-24 bg-gray-50">
|
||||||
|
<div class="w-full max-w-[1280px] mx-auto flex flex-col justify-center items-start gap-20">
|
||||||
|
<div class="flex flex-col lg:flex-row justify-start items-center gap-20 w-full">
|
||||||
|
<!-- Image Section -->
|
||||||
|
<div class="w-full lg:w-[592px] h-[400px] lg:h-[592px] relative">
|
||||||
|
<div class="w-60 h-60 absolute bottom-0 left-0 bg-green-800 rounded-full"></div>
|
||||||
|
<img class="w-80 h-80 lg:w-96 lg:h-96 absolute bottom-12 right-20 rounded-full object-cover" src="/images/call-center.png" alt="Help Desk Image" />
|
||||||
|
<div class="w-48 h-48 absolute top-1/2 right-0 lg:right-0 bg-white rounded-full shadow-lg flex items-center justify-center">
|
||||||
|
<img class="w-36 h-36" src="/images/logo.png" alt="Help Desk Logo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Section -->
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-8">
|
||||||
|
<div class="flex flex-col justify-start items-start gap-4 w-full">
|
||||||
|
<!-- Badge -->
|
||||||
|
<div class="px-4 py-2 bg-gray-300 rounded-3xl flex justify-center items-center gap-2">
|
||||||
|
<div class="w-4 h-4 relative">
|
||||||
|
<div class="w-3 h-3 absolute top-[1.5px] left-[1.5px] bg-green-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-green-800 text-base font-bold font-jakarta">Ada kendala terkait Bank Sampah?</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<h2 class="text-gray-900 text-3xl lg:text-4xl font-bold font-jakarta leading-10">Hubungi Help Desk kami</h2>
|
||||||
|
|
||||||
|
<!-- Contact Grid -->
|
||||||
|
<div class="flex flex-col lg:flex-row justify-start items-start gap-6 w-full">
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-4">
|
||||||
|
<!-- Admin 1 -->
|
||||||
|
<div class="px-6 py-4 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-center gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Admin 1</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">085283788234</p>
|
||||||
|
</div>
|
||||||
|
<button class="p-3 bg-gray-300 hover:bg-gray-400 rounded-full flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-6 h-6 text-green-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin 3 -->
|
||||||
|
<div class="px-6 py-4 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-center gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Admin 3</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">085283788234</p>
|
||||||
|
</div>
|
||||||
|
<button class="p-3 bg-gray-300 hover:bg-gray-400 rounded-full flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-6 h-6 text-green-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin 5 -->
|
||||||
|
<div class="px-6 py-4 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-center gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Admin 5</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">085283788234</p>
|
||||||
|
</div>
|
||||||
|
<button class="p-3 bg-gray-300 hover:bg-gray-400 rounded-full flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-6 h-6 text-green-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-4">
|
||||||
|
<!-- Admin 2 -->
|
||||||
|
<div class="px-6 py-4 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-center gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Admin 2</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">085283788234</p>
|
||||||
|
</div>
|
||||||
|
<button class="p-3 bg-gray-300 hover:bg-gray-400 rounded-full flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-6 h-6 text-green-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin 4 -->
|
||||||
|
<div class="px-6 py-4 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-center gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Admin 4</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">085283788234</p>
|
||||||
|
</div>
|
||||||
|
<button class="p-3 bg-gray-300 hover:bg-gray-400 rounded-full flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-6 h-6 text-green-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin 6 -->
|
||||||
|
<div class="px-6 py-4 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6 w-full">
|
||||||
|
<div class="flex justify-start items-center gap-6 w-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-start items-start gap-0.5">
|
||||||
|
<h3 class="text-gray-900 text-xl font-bold font-jakarta leading-loose">Admin 6</h3>
|
||||||
|
<p class="text-gray-500 text-base font-normal font-jakarta leading-normal">085283788234</p>
|
||||||
|
</div>
|
||||||
|
<button class="p-3 bg-gray-300 hover:bg-gray-400 rounded-full flex justify-center items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-6 h-6 text-green-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Contact Info -->
|
||||||
|
<div class="w-full">
|
||||||
|
<p class="text-slate-700 text-base font-jakarta">
|
||||||
|
<span class="font-semibold">Atau dapat langsung menghubungi:</span><br/>
|
||||||
|
<span class="font-normal">Bidang Pengurangan dan Penanganan Sampah<br/>
|
||||||
|
Dinas Lingkungan Hidup Provinsi DKI Jakarta<br/>
|
||||||
|
Jl. Mandala V Cililitan Kramatjati Jakarta Timur</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
// Smooth scrolling untuk anchor links
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
anchor.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation on scroll
|
||||||
|
const observerOptions = {
|
||||||
|
threshold: 0.1,
|
||||||
|
rootMargin: '0px 0px -50px 0px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(function(entries) {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('animate-fade-in');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, observerOptions);
|
||||||
|
|
||||||
|
// Observe all sections
|
||||||
|
document.querySelectorAll('section').forEach(section => {
|
||||||
|
observer.observe(section);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Kebijakan Privasi";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-16">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h1 class="text-4xl font-bold text-base-content mb-4">Kebijakan Privasi</h1>
|
||||||
|
<p class="text-lg text-base-content opacity-70">
|
||||||
|
Kami menghormati privasi Anda dan berkomitmen melindungi data pribadi Anda
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body prose max-w-none">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Informasi yang Kami Kumpulkan</h2>
|
||||||
|
<p class="text-base-content opacity-80 mb-6">
|
||||||
|
Kami mengumpulkan informasi yang Anda berikan secara langsung kepada kami,
|
||||||
|
seperti saat Anda membuat akun, menggunakan layanan kami, atau menghubungi kami.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Bagaimana Kami Menggunakan Informasi</h2>
|
||||||
|
<ul class="list-disc pl-6 text-base-content opacity-80 mb-6">
|
||||||
|
<li>Menyediakan dan memelihara layanan kami</li>
|
||||||
|
<li>Memproses transaksi dan mengirim pemberitahuan terkait</li>
|
||||||
|
<li>Meningkatkan dan mengembangkan layanan kami</li>
|
||||||
|
<li>Berkomunikasi dengan Anda tentang produk dan layanan</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Keamanan Data</h2>
|
||||||
|
<p class="text-base-content opacity-80 mb-6">
|
||||||
|
Kami menggunakan langkah-langkah keamanan yang wajar untuk melindungi
|
||||||
|
informasi pribadi Anda dari akses, penggunaan, atau pengungkapan yang tidak sah.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Hubungi Kami</h2>
|
||||||
|
<p class="text-base-content opacity-80">
|
||||||
|
Jika Anda memiliki pertanyaan tentang Kebijakan Privasi ini,
|
||||||
|
silakan hubungi kami di <a href="mailto:privacy@banksampah.com" class="link link-primary">privacy@banksampah.com</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info mt-6">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Kebijakan ini terakhir diperbarui pada @DateTime.Now.ToString("dd MMMM yyyy")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
@model ErrorViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="hero min-h-screen bg-base-200">
|
||||||
|
<div class="hero-content text-center">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<div class="text-9xl mb-8">🚫</div>
|
||||||
|
<h1 class="text-5xl font-bold text-error mb-4">Oops!</h1>
|
||||||
|
<h2 class="text-2xl font-semibold mb-6">Terjadi Kesalahan</h2>
|
||||||
|
<p class="text-base-content opacity-70 mb-8">
|
||||||
|
Maaf, terjadi kesalahan yang tidak terduga. Tim kami telah diberitahu dan sedang menangani masalah ini.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (Model.ShowRequestId)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning mb-6">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold">Request ID untuk referensi:</h3>
|
||||||
|
<div class="text-xs opacity-75 font-mono">@Model.RequestId</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<a asp-controller="Home" asp-action="Index" class="btn btn-primary btn-lg">
|
||||||
|
🏠 Kembali ke Beranda
|
||||||
|
</a>
|
||||||
|
<button onclick="window.history.back()" class="btn btn-outline btn-lg">
|
||||||
|
⬅️ Halaman Sebelumnya
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">ATAU</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-base-content opacity-60">
|
||||||
|
<p>Jika masalah terus berlanjut, silakan hubungi:</p>
|
||||||
|
<a href="mailto:support@banksampah.com" class="link link-primary">support@banksampah.com</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id" data-theme="emerald">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>@ViewData["Title"] - Bank Sampah Digital</title>
|
||||||
|
|
||||||
|
<!-- Meta tags untuk SEO -->
|
||||||
|
<meta name="description" content="Aplikasi Bank Sampah Digital untuk mengelola sampah dan mendapatkan reward">
|
||||||
|
<meta name="keywords" content="bank sampah, recycle, environment, go green, sampah">
|
||||||
|
<meta name="author" content="Bank Sampah Digital">
|
||||||
|
|
||||||
|
<!-- Open Graph Meta Tags -->
|
||||||
|
<meta property="og:title" content="@ViewData["Title"] - Bank Sampah Digital">
|
||||||
|
<meta property="og:description" content="Kelola sampah Anda dengan mudah dan dapatkan reward!">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/x-icon" href="~/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-base-100">
|
||||||
|
<!-- Navigation Bar - Overlay on Hero -->
|
||||||
|
<header class="absolute top-0 left-0 right-0 z-50 bg-transparent">
|
||||||
|
<div class="navbar px-4 lg:px-28 py-6">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<div class="dropdown">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden text-white">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
|
<li><a asp-controller="Home" asp-action="Index">Beranda</a></li>
|
||||||
|
<li><a>Tentang</a></li>
|
||||||
|
<li><a>Publikasi</a></li>
|
||||||
|
<li><a>Regulasi</a></li>
|
||||||
|
<li><a>Lokasi</a></li>
|
||||||
|
<li><a>Helpdesk</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a asp-controller="Home" asp-action="Index" class="btn btn-ghost text-white text-xl font-bold">
|
||||||
|
<img class="w-11 h-10" src="/images/logo.png" alt="Logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-center hidden lg:flex">
|
||||||
|
<ul class="menu menu-horizontal px-1 gap-2">
|
||||||
|
<li><a asp-controller="Home" asp-action="Index" class="btn btn-ghost text-white font-semibold text-base font-jakarta">Beranda</a></li>
|
||||||
|
<li><a class="btn btn-ghost text-gray-300 font-semibold text-base font-jakarta">Tentang</a></li>
|
||||||
|
<li><a class="btn btn-ghost text-gray-300 font-semibold text-base font-jakarta">Publikasi</a></li>
|
||||||
|
<li><a class="btn btn-ghost text-gray-300 font-semibold text-base font-jakarta">Regulasi</a></li>
|
||||||
|
<li><a class="btn btn-ghost text-gray-300 font-semibold text-base font-jakarta">Lokasi</a></li>
|
||||||
|
<li><a class="btn btn-ghost text-gray-300 font-semibold text-base font-jakarta">Helpdesk</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
<button class="btn bg-green-800 hover:bg-green-700 text-white border-none px-8 py-2.5 rounded-full font-semibold text-base font-jakarta">Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main role="main">
|
||||||
|
@RenderBody()
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="px-4 lg:px-28 py-20 bg-white">
|
||||||
|
<div class="max-w-[1280px] mx-auto h-14 flex flex-col justify-between items-center">
|
||||||
|
<div class="w-full flex flex-col justify-start items-center gap-8">
|
||||||
|
<div class="w-full h-px bg-gray-200"></div>
|
||||||
|
<div class="w-full flex flex-col lg:flex-row justify-between items-center gap-4">
|
||||||
|
<div class="text-gray-500 text-sm font-normal leading-tight text-center lg:text-left">
|
||||||
|
Jalan Mandala V No. 67, Cililitan Besar, Kramatjati, DKI Jakarta Telp. (021) 8092744 Fax. (021) 8091056
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-500 text-sm font-normal leading-tight">
|
||||||
|
DLH © @DateTime.Now.Year. e-Bank Sampah.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||||
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@using BankSampahApp
|
||||||
|
@using BankSampahApp.Models
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"Microsoft.AspNetCore": "Information",
|
||||||
|
"Microsoft.AspNetCore.Hosting": "Debug",
|
||||||
|
"Microsoft.AspNetCore.Routing": "Debug",
|
||||||
|
"BankSampahApp": "Debug"
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"IncludeScopes": true,
|
||||||
|
"TimestampFormat": "yyyy-MM-dd HH:mm:ss "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Features": {
|
||||||
|
"EnableDetailedErrors": true,
|
||||||
|
"EnableCaching": false
|
||||||
|
},
|
||||||
|
"Security": {
|
||||||
|
"EnableHttps": false,
|
||||||
|
"EnableHsts": false,
|
||||||
|
"EnableCsp": false
|
||||||
|
},
|
||||||
|
"Statistics": {
|
||||||
|
"CacheExpirationMinutes": 1
|
||||||
|
},
|
||||||
|
"Development": {
|
||||||
|
"EnableSwagger": true,
|
||||||
|
"EnableOpenApi": true,
|
||||||
|
"DetailedExceptions": true,
|
||||||
|
"ShowSensitiveDataInLogs": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.AspNetCore.Hosting": "Information",
|
||||||
|
"Microsoft.AspNetCore.Routing": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Statistics": {
|
||||||
|
"BaseUsers": 1250,
|
||||||
|
"BaseWaste": 15.6,
|
||||||
|
"CacheExpirationMinutes": 15
|
||||||
|
},
|
||||||
|
"Application": {
|
||||||
|
"Name": "Bank Sampah Digital",
|
||||||
|
"Version": "1.0.0",
|
||||||
|
"Description": "Aplikasi digital untuk mengelola bank sampah dengan sistem reward"
|
||||||
|
},
|
||||||
|
"Features": {
|
||||||
|
"EnableCaching": true,
|
||||||
|
"EnableCompression": true,
|
||||||
|
"EnableDetailedErrors": false,
|
||||||
|
"MaxCacheSize": "100MB"
|
||||||
|
},
|
||||||
|
"Security": {
|
||||||
|
"EnableHttps": true,
|
||||||
|
"EnableHsts": true,
|
||||||
|
"HstsMaxAge": 31536000,
|
||||||
|
"EnableCsp": true
|
||||||
|
},
|
||||||
|
"Performance": {
|
||||||
|
"ResponseCompression": true,
|
||||||
|
"StaticFilesCaching": true,
|
||||||
|
"CacheDurationMinutes": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "bank-sampah-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Bank Sampah Application",
|
||||||
|
"scripts": {
|
||||||
|
"build-css": "tailwindcss -i ./wwwroot/css/input.css -o ./wwwroot/css/site.css --watch",
|
||||||
|
"build": "tailwindcss -i ./wwwroot/css/input.css -o ./wwwroot/css/site.css --minify"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^3.4.10",
|
||||||
|
"daisyui": "^4.12.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,864 @@
|
||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
devDependencies:
|
||||||
|
daisyui:
|
||||||
|
specifier: ^4.12.10
|
||||||
|
version: 4.12.24(postcss@8.5.6)
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^3.4.10
|
||||||
|
version: 3.4.17
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@alloc/quick-lru@5.2.0':
|
||||||
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
'@isaacs/cliui@8.0.2':
|
||||||
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
'@nodelib/fs.stat@2.0.5':
|
||||||
|
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
'@nodelib/fs.walk@1.2.8':
|
||||||
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-regex@6.2.2:
|
||||||
|
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@6.2.3:
|
||||||
|
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
any-promise@1.3.0:
|
||||||
|
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||||
|
|
||||||
|
anymatch@3.1.3:
|
||||||
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
arg@5.0.2:
|
||||||
|
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||||
|
|
||||||
|
balanced-match@1.0.2:
|
||||||
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
binary-extensions@2.3.0:
|
||||||
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
brace-expansion@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||||
|
|
||||||
|
braces@3.0.3:
|
||||||
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
camelcase-css@2.0.1:
|
||||||
|
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
chokidar@3.6.0:
|
||||||
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
commander@4.1.1:
|
||||||
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
css-selector-tokenizer@0.8.0:
|
||||||
|
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
|
||||||
|
|
||||||
|
cssesc@3.0.0:
|
||||||
|
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
culori@3.3.0:
|
||||||
|
resolution: {integrity: sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
daisyui@4.12.24:
|
||||||
|
resolution: {integrity: sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==}
|
||||||
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
|
didyoumean@1.2.2:
|
||||||
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
|
dlv@1.1.3:
|
||||||
|
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0:
|
||||||
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2:
|
||||||
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
|
fast-glob@3.3.3:
|
||||||
|
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||||
|
engines: {node: '>=8.6.0'}
|
||||||
|
|
||||||
|
fastparse@1.1.2:
|
||||||
|
resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==}
|
||||||
|
|
||||||
|
fastq@1.19.1:
|
||||||
|
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||||
|
|
||||||
|
fill-range@7.1.1:
|
||||||
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
foreground-child@3.3.1:
|
||||||
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
function-bind@1.1.2:
|
||||||
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
glob-parent@5.1.2:
|
||||||
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
glob-parent@6.0.2:
|
||||||
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
glob@10.4.5:
|
||||||
|
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-binary-path@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-core-module@2.16.1:
|
||||||
|
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-extglob@2.1.1:
|
||||||
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-number@7.0.0:
|
||||||
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
|
isexe@2.0.0:
|
||||||
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
jackspeak@3.4.3:
|
||||||
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
|
jiti@1.21.7:
|
||||||
|
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
lilconfig@3.1.3:
|
||||||
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
lines-and-columns@1.2.4:
|
||||||
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
|
lru-cache@10.4.3:
|
||||||
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
|
merge2@1.4.1:
|
||||||
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
micromatch@4.0.8:
|
||||||
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
minimatch@9.0.5:
|
||||||
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
minipass@7.1.2:
|
||||||
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
mz@2.7.0:
|
||||||
|
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||||
|
|
||||||
|
nanoid@3.3.11:
|
||||||
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
normalize-path@3.0.0:
|
||||||
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
object-assign@4.1.1:
|
||||||
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
object-hash@3.0.0:
|
||||||
|
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1:
|
||||||
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
|
path-key@3.1.1:
|
||||||
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-parse@1.0.7:
|
||||||
|
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||||
|
|
||||||
|
path-scurry@1.11.1:
|
||||||
|
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.18'}
|
||||||
|
|
||||||
|
picocolors@1.1.1:
|
||||||
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
picomatch@2.3.1:
|
||||||
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
pify@2.3.0:
|
||||||
|
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
pirates@4.0.7:
|
||||||
|
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
postcss-import@15.1.0:
|
||||||
|
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
postcss: ^8.0.0
|
||||||
|
|
||||||
|
postcss-js@4.0.1:
|
||||||
|
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
|
||||||
|
engines: {node: ^12 || ^14 || >= 16}
|
||||||
|
peerDependencies:
|
||||||
|
postcss: ^8.4.21
|
||||||
|
|
||||||
|
postcss-load-config@4.0.2:
|
||||||
|
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
peerDependencies:
|
||||||
|
postcss: '>=8.0.9'
|
||||||
|
ts-node: '>=9.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
postcss:
|
||||||
|
optional: true
|
||||||
|
ts-node:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
postcss-nested@6.2.0:
|
||||||
|
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
|
||||||
|
engines: {node: '>=12.0'}
|
||||||
|
peerDependencies:
|
||||||
|
postcss: ^8.2.14
|
||||||
|
|
||||||
|
postcss-selector-parser@6.1.2:
|
||||||
|
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
postcss-value-parser@4.2.0:
|
||||||
|
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
queue-microtask@1.2.3:
|
||||||
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
read-cache@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||||
|
|
||||||
|
readdirp@3.6.0:
|
||||||
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
|
resolve@1.22.10:
|
||||||
|
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
reusify@1.1.0:
|
||||||
|
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||||
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
|
run-parallel@1.2.0:
|
||||||
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
shebang-regex@3.0.0:
|
||||||
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
signal-exit@4.1.0:
|
||||||
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
source-map-js@1.2.1:
|
||||||
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
string-width@5.1.2:
|
||||||
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-ansi@7.1.2:
|
||||||
|
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
sucrase@3.35.0:
|
||||||
|
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
supports-preserve-symlinks-flag@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
tailwindcss@3.4.17:
|
||||||
|
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
thenify-all@1.6.0:
|
||||||
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
|
thenify@3.3.1:
|
||||||
|
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||||
|
|
||||||
|
to-regex-range@5.0.1:
|
||||||
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
ts-interface-checker@0.1.13:
|
||||||
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
wrap-ansi@8.1.0:
|
||||||
|
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yaml@2.8.1:
|
||||||
|
resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
|
||||||
|
engines: {node: '>= 14.6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
|
'@isaacs/cliui@8.0.2':
|
||||||
|
dependencies:
|
||||||
|
string-width: 5.1.2
|
||||||
|
string-width-cjs: string-width@4.2.3
|
||||||
|
strip-ansi: 7.1.2
|
||||||
|
strip-ansi-cjs: strip-ansi@6.0.1
|
||||||
|
wrap-ansi: 8.1.0
|
||||||
|
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@nodelib/fs.stat': 2.0.5
|
||||||
|
run-parallel: 1.2.0
|
||||||
|
|
||||||
|
'@nodelib/fs.stat@2.0.5': {}
|
||||||
|
|
||||||
|
'@nodelib/fs.walk@1.2.8':
|
||||||
|
dependencies:
|
||||||
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
|
fastq: 1.19.1
|
||||||
|
|
||||||
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-regex@6.2.2: {}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
ansi-styles@6.2.3: {}
|
||||||
|
|
||||||
|
any-promise@1.3.0: {}
|
||||||
|
|
||||||
|
anymatch@3.1.3:
|
||||||
|
dependencies:
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
arg@5.0.2: {}
|
||||||
|
|
||||||
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
brace-expansion@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
|
braces@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
fill-range: 7.1.1
|
||||||
|
|
||||||
|
camelcase-css@2.0.1: {}
|
||||||
|
|
||||||
|
chokidar@3.6.0:
|
||||||
|
dependencies:
|
||||||
|
anymatch: 3.1.3
|
||||||
|
braces: 3.0.3
|
||||||
|
glob-parent: 5.1.2
|
||||||
|
is-binary-path: 2.1.0
|
||||||
|
is-glob: 4.0.3
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
readdirp: 3.6.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
commander@4.1.1: {}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
path-key: 3.1.1
|
||||||
|
shebang-command: 2.0.0
|
||||||
|
which: 2.0.2
|
||||||
|
|
||||||
|
css-selector-tokenizer@0.8.0:
|
||||||
|
dependencies:
|
||||||
|
cssesc: 3.0.0
|
||||||
|
fastparse: 1.1.2
|
||||||
|
|
||||||
|
cssesc@3.0.0: {}
|
||||||
|
|
||||||
|
culori@3.3.0: {}
|
||||||
|
|
||||||
|
daisyui@4.12.24(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
css-selector-tokenizer: 0.8.0
|
||||||
|
culori: 3.3.0
|
||||||
|
picocolors: 1.1.1
|
||||||
|
postcss-js: 4.0.1(postcss@8.5.6)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- postcss
|
||||||
|
|
||||||
|
didyoumean@1.2.2: {}
|
||||||
|
|
||||||
|
dlv@1.1.3: {}
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
|
fast-glob@3.3.3:
|
||||||
|
dependencies:
|
||||||
|
'@nodelib/fs.stat': 2.0.5
|
||||||
|
'@nodelib/fs.walk': 1.2.8
|
||||||
|
glob-parent: 5.1.2
|
||||||
|
merge2: 1.4.1
|
||||||
|
micromatch: 4.0.8
|
||||||
|
|
||||||
|
fastparse@1.1.2: {}
|
||||||
|
|
||||||
|
fastq@1.19.1:
|
||||||
|
dependencies:
|
||||||
|
reusify: 1.1.0
|
||||||
|
|
||||||
|
fill-range@7.1.1:
|
||||||
|
dependencies:
|
||||||
|
to-regex-range: 5.0.1
|
||||||
|
|
||||||
|
foreground-child@3.3.1:
|
||||||
|
dependencies:
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
glob-parent@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
is-glob: 4.0.3
|
||||||
|
|
||||||
|
glob-parent@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
is-glob: 4.0.3
|
||||||
|
|
||||||
|
glob@10.4.5:
|
||||||
|
dependencies:
|
||||||
|
foreground-child: 3.3.1
|
||||||
|
jackspeak: 3.4.3
|
||||||
|
minimatch: 9.0.5
|
||||||
|
minipass: 7.1.2
|
||||||
|
package-json-from-dist: 1.0.1
|
||||||
|
path-scurry: 1.11.1
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
is-binary-path@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
binary-extensions: 2.3.0
|
||||||
|
|
||||||
|
is-core-module@2.16.1:
|
||||||
|
dependencies:
|
||||||
|
hasown: 2.0.2
|
||||||
|
|
||||||
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
jackspeak@3.4.3:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/cliui': 8.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
|
jiti@1.21.7: {}
|
||||||
|
|
||||||
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
|
micromatch@4.0.8:
|
||||||
|
dependencies:
|
||||||
|
braces: 3.0.3
|
||||||
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
minimatch@9.0.5:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 2.0.2
|
||||||
|
|
||||||
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
|
mz@2.7.0:
|
||||||
|
dependencies:
|
||||||
|
any-promise: 1.3.0
|
||||||
|
object-assign: 4.1.1
|
||||||
|
thenify-all: 1.6.0
|
||||||
|
|
||||||
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
|
object-hash@3.0.0: {}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
path-parse@1.0.7: {}
|
||||||
|
|
||||||
|
path-scurry@1.11.1:
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 10.4.3
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
|
pify@2.3.0: {}
|
||||||
|
|
||||||
|
pirates@4.0.7: {}
|
||||||
|
|
||||||
|
postcss-import@15.1.0(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
postcss: 8.5.6
|
||||||
|
postcss-value-parser: 4.2.0
|
||||||
|
read-cache: 1.0.0
|
||||||
|
resolve: 1.22.10
|
||||||
|
|
||||||
|
postcss-js@4.0.1(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
camelcase-css: 2.0.1
|
||||||
|
postcss: 8.5.6
|
||||||
|
|
||||||
|
postcss-load-config@4.0.2(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
lilconfig: 3.1.3
|
||||||
|
yaml: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
postcss: 8.5.6
|
||||||
|
|
||||||
|
postcss-nested@6.2.0(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
postcss: 8.5.6
|
||||||
|
postcss-selector-parser: 6.1.2
|
||||||
|
|
||||||
|
postcss-selector-parser@6.1.2:
|
||||||
|
dependencies:
|
||||||
|
cssesc: 3.0.0
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
|
postcss-value-parser@4.2.0: {}
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.11
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
read-cache@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
pify: 2.3.0
|
||||||
|
|
||||||
|
readdirp@3.6.0:
|
||||||
|
dependencies:
|
||||||
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
resolve@1.22.10:
|
||||||
|
dependencies:
|
||||||
|
is-core-module: 2.16.1
|
||||||
|
path-parse: 1.0.7
|
||||||
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
|
|
||||||
|
reusify@1.1.0: {}
|
||||||
|
|
||||||
|
run-parallel@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
shebang-regex: 3.0.0
|
||||||
|
|
||||||
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
string-width@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
eastasianwidth: 0.2.0
|
||||||
|
emoji-regex: 9.2.2
|
||||||
|
strip-ansi: 7.1.2
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
|
strip-ansi@7.1.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 6.2.2
|
||||||
|
|
||||||
|
sucrase@3.35.0:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
|
commander: 4.1.1
|
||||||
|
glob: 10.4.5
|
||||||
|
lines-and-columns: 1.2.4
|
||||||
|
mz: 2.7.0
|
||||||
|
pirates: 4.0.7
|
||||||
|
ts-interface-checker: 0.1.13
|
||||||
|
|
||||||
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
|
tailwindcss@3.4.17:
|
||||||
|
dependencies:
|
||||||
|
'@alloc/quick-lru': 5.2.0
|
||||||
|
arg: 5.0.2
|
||||||
|
chokidar: 3.6.0
|
||||||
|
didyoumean: 1.2.2
|
||||||
|
dlv: 1.1.3
|
||||||
|
fast-glob: 3.3.3
|
||||||
|
glob-parent: 6.0.2
|
||||||
|
is-glob: 4.0.3
|
||||||
|
jiti: 1.21.7
|
||||||
|
lilconfig: 3.1.3
|
||||||
|
micromatch: 4.0.8
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
object-hash: 3.0.0
|
||||||
|
picocolors: 1.1.1
|
||||||
|
postcss: 8.5.6
|
||||||
|
postcss-import: 15.1.0(postcss@8.5.6)
|
||||||
|
postcss-js: 4.0.1(postcss@8.5.6)
|
||||||
|
postcss-load-config: 4.0.2(postcss@8.5.6)
|
||||||
|
postcss-nested: 6.2.0(postcss@8.5.6)
|
||||||
|
postcss-selector-parser: 6.1.2
|
||||||
|
resolve: 1.22.10
|
||||||
|
sucrase: 3.35.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- ts-node
|
||||||
|
|
||||||
|
thenify-all@1.6.0:
|
||||||
|
dependencies:
|
||||||
|
thenify: 3.3.1
|
||||||
|
|
||||||
|
thenify@3.3.1:
|
||||||
|
dependencies:
|
||||||
|
any-promise: 1.3.0
|
||||||
|
|
||||||
|
to-regex-range@5.0.1:
|
||||||
|
dependencies:
|
||||||
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
ts-interface-checker@0.1.13: {}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
wrap-ansi@8.1.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 6.2.3
|
||||||
|
string-width: 5.1.2
|
||||||
|
strip-ansi: 7.1.2
|
||||||
|
|
||||||
|
yaml@2.8.1: {}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./Views/**/*.cshtml",
|
||||||
|
"./wwwroot/js/**/*.js"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
'jakarta': ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('daisyui'),
|
||||||
|
],
|
||||||
|
daisyui: {
|
||||||
|
themes: [
|
||||||
|
"light",
|
||||||
|
"dark",
|
||||||
|
"cupcake",
|
||||||
|
"bumblebee",
|
||||||
|
"emerald",
|
||||||
|
"corporate",
|
||||||
|
"synthwave",
|
||||||
|
"retro",
|
||||||
|
"cyberpunk",
|
||||||
|
"valentine",
|
||||||
|
"halloween",
|
||||||
|
"garden",
|
||||||
|
"forest",
|
||||||
|
"aqua",
|
||||||
|
"lofi",
|
||||||
|
"pastel",
|
||||||
|
"fantasy",
|
||||||
|
"wireframe",
|
||||||
|
"black",
|
||||||
|
"luxury",
|
||||||
|
"dracula",
|
||||||
|
"cmyk",
|
||||||
|
"autumn",
|
||||||
|
"business",
|
||||||
|
"acid",
|
||||||
|
"lemonade",
|
||||||
|
"night",
|
||||||
|
"coffee",
|
||||||
|
"winter",
|
||||||
|
"dim",
|
||||||
|
"nord",
|
||||||
|
"sunset",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Custom styles untuk Bank Sampah App */
|
||||||
|
@layer components {
|
||||||
|
.hero-gradient {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New Landing Page Styles */
|
||||||
|
.fade-in-up {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-up.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.8s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom badge styles */
|
||||||
|
.badge-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects for buttons and cards */
|
||||||
|
.btn-custom {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-custom:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map marker animation */
|
||||||
|
.map-marker {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive text sizing */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 4.5rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 7.4 MiB |
|
After Width: | Height: | Size: 694 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 565 KiB |
|
After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 584 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 290 KiB |
|
|
@ -0,0 +1,378 @@
|
||||||
|
/**
|
||||||
|
* Bank Sampah Digital - Main JavaScript File
|
||||||
|
* Optimized dengan modern patterns dan clean code principles
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BankSampahApp {
|
||||||
|
constructor() {
|
||||||
|
this.config = {
|
||||||
|
animationDuration: 600,
|
||||||
|
toastDuration: 3000,
|
||||||
|
counterSpeed: 20,
|
||||||
|
observerThreshold: 0.1,
|
||||||
|
counterThreshold: 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
this.observers = new Map();
|
||||||
|
this.timers = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smooth scroll untuk anchor links
|
||||||
|
*/
|
||||||
|
initSmoothScroll() {
|
||||||
|
const anchors = document.querySelectorAll('a[href^="#"]');
|
||||||
|
anchors.forEach(anchor => {
|
||||||
|
anchor.addEventListener('click', this.handleAnchorClick.bind(this));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle anchor click dengan error handling
|
||||||
|
*/
|
||||||
|
handleAnchorClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const href = event.currentTarget.getAttribute('href');
|
||||||
|
const target = document.querySelector(href);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`Target element ${href} tidak ditemukan`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intersection Observer untuk scroll animations
|
||||||
|
*/
|
||||||
|
initScrollAnimations() {
|
||||||
|
const options = {
|
||||||
|
threshold: this.config.observerThreshold,
|
||||||
|
rootMargin: '0px 0px -50px 0px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
this.handleIntersection.bind(this),
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
this.observers.set('scrollAnimation', observer);
|
||||||
|
this.observeElements(['.card', '.stat'], observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle intersection untuk animasi
|
||||||
|
*/
|
||||||
|
handleIntersection(entries) {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.animateElement(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animasi element dengan transition
|
||||||
|
*/
|
||||||
|
animateElement(element) {
|
||||||
|
element.style.opacity = '1';
|
||||||
|
element.style.transform = 'translateY(0)';
|
||||||
|
element.classList.add('animate-fade-in');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup initial styles untuk animasi
|
||||||
|
*/
|
||||||
|
setupAnimationStyles(element) {
|
||||||
|
Object.assign(element.style, {
|
||||||
|
opacity: '0',
|
||||||
|
transform: 'translateY(20px)',
|
||||||
|
transition: `opacity ${this.config.animationDuration}ms ease, transform ${this.config.animationDuration}ms ease`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observer multiple elements dengan selector
|
||||||
|
*/
|
||||||
|
observeElements(selectors, observer) {
|
||||||
|
selectors.forEach(selector => {
|
||||||
|
document.querySelectorAll(selector).forEach(element => {
|
||||||
|
this.setupAnimationStyles(element);
|
||||||
|
observer.observe(element);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counter animation dengan performance optimization
|
||||||
|
*/
|
||||||
|
initCounterAnimation() {
|
||||||
|
const counters = document.querySelectorAll('.stat-value');
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
this.handleCounterIntersection.bind(this),
|
||||||
|
{ threshold: this.config.counterThreshold }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.observers.set('counter', observer);
|
||||||
|
counters.forEach(counter => observer.observe(counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle counter intersection
|
||||||
|
*/
|
||||||
|
handleCounterIntersection(entries) {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.animateCounter(entry.target);
|
||||||
|
this.observers.get('counter')?.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animasi counter dengan format preservation
|
||||||
|
*/
|
||||||
|
animateCounter(counter) {
|
||||||
|
const originalText = counter.textContent;
|
||||||
|
const target = this.extractNumber(originalText);
|
||||||
|
const isDecimal = originalText.includes('.');
|
||||||
|
const suffix = this.extractSuffix(originalText);
|
||||||
|
|
||||||
|
const increment = target / (this.config.animationDuration / this.config.counterSpeed);
|
||||||
|
let current = 0;
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
current += increment;
|
||||||
|
|
||||||
|
if (current >= target) {
|
||||||
|
current = target;
|
||||||
|
clearInterval(timer);
|
||||||
|
this.timers.delete(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.textContent = this.formatCounter(current, isDecimal, suffix);
|
||||||
|
}, this.config.counterSpeed);
|
||||||
|
|
||||||
|
this.timers.add(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract number dari text
|
||||||
|
*/
|
||||||
|
extractNumber(text) {
|
||||||
|
const match = text.match(/[\d.,]+/);
|
||||||
|
return match ? parseFloat(match[0].replace(',', '')) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract suffix dari text
|
||||||
|
*/
|
||||||
|
extractSuffix(text) {
|
||||||
|
return text.replace(/[\d.,\s]+/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format counter value
|
||||||
|
*/
|
||||||
|
formatCounter(value, isDecimal, suffix) {
|
||||||
|
const formatted = isDecimal
|
||||||
|
? value.toFixed(1)
|
||||||
|
: Math.floor(value).toLocaleString('id-ID');
|
||||||
|
|
||||||
|
return suffix ? `${formatted} ${suffix}` : formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme management
|
||||||
|
*/
|
||||||
|
initThemeSwitch() {
|
||||||
|
const toggle = document.querySelector('#theme-toggle');
|
||||||
|
|
||||||
|
if (toggle) {
|
||||||
|
toggle.addEventListener('change', this.handleThemeChange.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadSavedTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle theme change
|
||||||
|
*/
|
||||||
|
handleThemeChange(event) {
|
||||||
|
const theme = event.target.checked ? 'dark' : 'light';
|
||||||
|
this.setTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set theme dan save ke localStorage
|
||||||
|
*/
|
||||||
|
setTheme(theme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load saved theme
|
||||||
|
*/
|
||||||
|
loadSavedTheme() {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'emerald';
|
||||||
|
this.setTheme(savedTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading state management
|
||||||
|
*/
|
||||||
|
toggleLoading(show = false) {
|
||||||
|
const loading = document.querySelector('.loading-overlay');
|
||||||
|
if (loading) {
|
||||||
|
loading.classList.toggle('hidden', !show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toast notification system dengan auto-remove
|
||||||
|
*/
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
const toast = this.createToastElement(message, type);
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.removeToast(toast);
|
||||||
|
this.timers.delete(timer);
|
||||||
|
}, this.config.toastDuration);
|
||||||
|
|
||||||
|
this.timers.add(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create toast element
|
||||||
|
*/
|
||||||
|
createToastElement(message, type) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast toast-top toast-end';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="alert alert-${type}">
|
||||||
|
<span>${this.escapeHtml(message)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return toast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove toast dengan animation
|
||||||
|
*/
|
||||||
|
removeToast(toast) {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transform = 'translateX(100%)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML untuk security
|
||||||
|
*/
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form validation dengan detailed feedback
|
||||||
|
*/
|
||||||
|
validateForm(formId) {
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
if (!form) {
|
||||||
|
console.warn(`Form dengan ID '${formId}' tidak ditemukan`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredInputs = form.querySelectorAll('input[required], select[required], textarea[required]');
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
requiredInputs.forEach(input => {
|
||||||
|
const isFieldValid = this.validateField(input);
|
||||||
|
if (!isFieldValid) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate individual field
|
||||||
|
*/
|
||||||
|
validateField(input) {
|
||||||
|
const isValid = input.value.trim() !== '';
|
||||||
|
input.classList.toggle('input-error', !isValid);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
this.showFieldError(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show field error
|
||||||
|
*/
|
||||||
|
showFieldError(input) {
|
||||||
|
const fieldName = input.getAttribute('placeholder') || input.getAttribute('name') || 'Field';
|
||||||
|
this.showToast(`${fieldName} harus diisi`, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup resources
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
// Clear all timers
|
||||||
|
this.timers.forEach(timer => clearInterval(timer));
|
||||||
|
this.timers.clear();
|
||||||
|
|
||||||
|
// Disconnect all observers
|
||||||
|
this.observers.forEach(observer => observer.disconnect());
|
||||||
|
this.observers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize aplikasi
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
try {
|
||||||
|
this.initSmoothScroll();
|
||||||
|
this.initScrollAnimations();
|
||||||
|
this.initCounterAnimation();
|
||||||
|
this.initThemeSwitch();
|
||||||
|
|
||||||
|
// Setup cleanup pada page unload
|
||||||
|
window.addEventListener('beforeunload', () => this.cleanup());
|
||||||
|
|
||||||
|
// Hide loading setelah load complete
|
||||||
|
window.addEventListener('load', () => this.toggleLoading(false));
|
||||||
|
|
||||||
|
console.log('🌱 Bank Sampah Digital - Aplikasi siap digunakan!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing BankSampahApp:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global instance
|
||||||
|
const BankSampah = new BankSampahApp();
|
||||||
|
|
||||||
|
// Initialize saat DOM ready
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
BankSampah.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export untuk penggunaan di file lain
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = BankSampah;
|
||||||
|
}
|
||||||