feat: landing page

main-dlh
Rohmad Eko Wahyudi 2025-09-16 14:21:25 +07:00
commit 0e432f7510
No known key found for this signature in database
GPG Key ID: 4CCEDA68CB778BAF
49 changed files with 4514 additions and 0 deletions

74
.dockerignore 100644
View File

@ -0,0 +1,74 @@
# .dockerignore - Exclude unnecessary files from Docker build context
# Git
.git
.gitignore
.gitattributes
# Documentation
*.md
README*
# IDE and editor files
.vscode/
.vs/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Build outputs
bin/
obj/
out/
dist/
# Node modules (will be installed in container)
node_modules/
npm-debug.log*
# .NET
*.user
*.userosscache
*.sln.docstates
*.userprefs
# Test results
TestResults/
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
# Package files
*.nupkg
*.snupkg
# Logs
logs
*.log
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Temporary files
*.tmp
*.temp

13
.env.example 100644
View File

@ -0,0 +1,13 @@
# Environment variables for Docker Compose
# Copy this file to .env and update the values
# Application Configuration
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:8080
# Logging
LOG_LEVEL=Information
# Optional: External service configurations
# API_KEY=your_api_key_here
# EXTERNAL_SERVICE_URL=https://api.example.com

77
.gitignore vendored 100644
View File

@ -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

View File

@ -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>

View File

@ -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
}
};
}
}

67
Dockerfile 100644
View File

@ -0,0 +1,67 @@
# Multi-stage Dockerfile for .NET 9 ASP.NET Core application with pnpm
# Stage 1: Base runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
# Create non-root user for security
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
# Stage 2: Node.js build environment for frontend assets
FROM node:20-alpine AS node-build
WORKDIR /src
# Install pnpm globally
RUN npm install -g pnpm
# Copy package files and install dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy source files and build CSS
COPY . .
RUN pnpm run build
# Stage 3: .NET SDK for building the application
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
# Copy project file and restore dependencies
COPY *.csproj .
RUN dotnet restore "BankSampahApp.csproj"
# Copy source code
COPY . .
# Copy built frontend assets from node-build stage
COPY --from=node-build /src/wwwroot/css/site.css ./wwwroot/css/
# Build the application
RUN dotnet build "BankSampahApp.csproj" -c Release -o /app/build
# Stage 4: Publish the application
FROM build AS publish
RUN dotnet publish "BankSampahApp.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 5: Final runtime image
FROM base AS final
WORKDIR /app
# Copy published application
COPY --from=publish /app/publish .
# Change ownership to non-root user
RUN chown -R appuser:appgroup /app
USER appuser
# Configure environment
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Entry point
ENTRYPOINT ["dotnet", "BankSampahApp.dll"]

View File

@ -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");
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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; }
}

136
Program.cs 100644
View File

@ -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();

107
README.md 100644
View File

@ -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 pnpm (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
pnpm install
```
3. **Build CSS dengan Tailwind**
```bash
pnpm 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
pnpm run build-css
# Terminal 2: Run aplikasi dengan hot reload
dotnet watch run
```
## 🚀 Production Deployment
### Build untuk Production
```bash
# Build optimized CSS
pnpm run build
# Publish aplikasi .NET
dotnet publish -c Release -o ./publish
```

BIN
README.pdf 100644

Binary file not shown.

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
};
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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()
};
}

View File

@ -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
};
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,724 @@
@{
ViewData["Title"] = "BPS-RW Jakarta";
}
<!-- Hero Section -->
<section class="w-full h-[720px] p-4 lg:p-28 relative flex justify-center items-center gap-20 overflow-hidden">
<!-- Background gradient -->
<img src="/images/hero-bg.png" class="w-full h-full left-0 top-0 absolute bg-gradient-to-l from-green-800/0 to-green-900" alt="Hero BG"/>
<!-- Content -->
<div class="flex-1 max-w-[1280px] relative z-10 flex flex-col justify-start items-start gap-20">
<div class="self-stretch flex justify-start items-center gap-8">
<div class="w-full max-w-[800px] pr-0 lg:pr-10 flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-8">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-900 rounded-full"></div>
</div>
<div class="text-green-900 text-base font-bold font-jakarta leading-normal">BPS-RW Jakarta</div>
</div>
<!-- Title and Description -->
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h1 class="self-stretch text-white text-4xl lg:text-7xl font-bold font-jakarta leading-[50px] lg:leading-[90px]">
Pengelolaan Sampah Berbasis RW
</h1>
<p class="self-stretch text-white text-lg font-medium font-jakarta leading-7">
Program dari Dinas Lingkungan Hidup Provinsi Jakarta yang bertujuan untuk mengoptimalkan pengelolaan sampah berbasis komunitas di tingkat RW.
</p>
</div>
</div>
<!-- CTA Buttons -->
<div class="flex flex-col sm:flex-row justify-start items-start gap-4">
<button class="px-8 py-3 bg-white hover:bg-gray-100 rounded-full flex justify-center items-center gap-2 transition-colors">
<span class="text-green-800 text-base font-semibold font-jakarta leading-normal">Pelajari Selengkapnya</span>
<div class="w-5 h-5 relative">
<svg class="w-5 h-5 text-green-800" 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="px-8 py-3 rounded-full border border-gray-300 hover:bg-white/10 flex justify-center items-center gap-2 transition-colors">
<span class="text-white text-base font-semibold font-jakarta leading-normal">Hubungi Kami</span>
</button>
</div>
</div>
</div>
</div>
<!-- Right side icons (hidden on mobile) -->
<div class="hidden lg:flex absolute right-28 top-1/2 transform -translate-y-1/2 flex-col justify-start items-start gap-4">
<button class="p-3 bg-white/20 hover:bg-white/30 rounded-full border border-white flex justify-center items-center gap-2 transition-colors">
<i class="ph ph-caret-left text-white"></i>
</button>
<button class="p-3 bg-white hover:bg-gray-100 rounded-full border border-white flex justify-center items-center gap-2 transition-colors">
<i class="ph ph-caret-right"></i>
</button>
</div>
</section>
<!-- About BPS-RW Section -->
<section class="w-full px-4 lg:px-28 py-24 bg-white flex flex-col justify-start items-center gap-20 overflow-hidden">
<div class="w-full max-w-[1280px] flex flex-col justify-start items-start gap-20">
<div class="self-stretch flex flex-col lg:flex-row justify-start items-center gap-20">
<!-- Image Section -->
<div class="w-full lg:w-[592px] h-[400px] lg:h-[592px] relative">
<div class="w-60 h-[552px] left-0 top-[40px] absolute bg-green-800"></div>
<img class="w-80 lg:w-96 h-[400px] lg:h-[552px] left-[40px] top-0 absolute object-cover" src="/images/hero-plastics.png" alt="BPS-RW Image" />
<img class="w-48 lg:w-64 h-48 lg:h-64 right-0 lg:left-[322px] top-[161px] absolute border-8 border-white object-cover" src="/images/hero-truck.png" alt="Logo" />
</div>
<!-- Content Section -->
<div class="flex-1 flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Tentang Kami</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-6">
<h2 class="self-stretch text-gray-900 text-4xl font-bold font-jakarta leading-10">Tentang BPS-RW</h2>
<p class="self-stretch text-slate-700 text-lg font-normal font-jakarta leading-7">
BPS-RW (Bank Pengelolaan Sampah Rukun Warga) adalah Program dari Dinas Lingkungan Hidup Provinsi DKI Jakarta yang bertujuan untuk mengoptimalkan pengelolaan sampah berbasis komunitas di tingkat RW. Program ini hadir untuk meningkatkan kesadaran masyarakat dalam memilah, mengelola, serta mendaur ulang sampah guna menciptakan lingkungan yang lebih bersih dan sehat.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Statistics Section -->
<section class="w-full p-4 lg:p-28 relative bg-green-950 flex flex-col justify-start items-center gap-20 overflow-hidden">
<!-- Background overlay -->
<div class="w-full h-full left-0 top-0 absolute opacity-60 overflow-hidden">
<div class="w-full h-full absolute bg-green-950"></div>
</div>
<!-- Content -->
<div class="w-full max-w-[1280px] relative z-10 flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex flex-col lg:flex-row justify-start items-center gap-8">
<!-- Left Content -->
<div class="flex-1 flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Statistik</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-6">
<h2 class="self-stretch text-white text-3xl lg:text-5xl font-bold font-jakarta leading-[40px] lg:leading-[60px]">
Jumlah Rumah Memilah
</h2>
<p class="self-stretch text-white text-lg font-normal font-jakarta leading-7">
Rumah Memilah adalah program dari BPSRW DKI Jakarta yang mendorong partisipasi aktif warga dalam memilah sampah rumah tangga menjadi beberapa kategori.
</p>
</div>
</div>
</div>
<!-- Statistics Grid -->
<div class="flex-1 flex flex-col justify-start items-start gap-12">
<div class="self-stretch flex flex-col lg:flex-row justify-start items-start gap-8">
<!-- Stat 1 -->
<div class="flex-1 pl-8 border-l-4 border-white flex flex-col justify-start items-start gap-1">
<div class="self-stretch text-white text-3xl lg:text-5xl font-bold font-jakarta leading-[40px] lg:leading-[60px]">1.522.356</div>
<div class="self-stretch text-white text-xl font-normal font-jakarta leading-loose">Jumlah Rumah</div>
</div>
<!-- Stat 2 -->
<div class="flex-1 pl-8 border-l-4 border-white flex flex-col justify-start items-start gap-1">
<div class="self-stretch text-white text-3xl lg:text-5xl font-bold font-jakarta leading-[40px] lg:leading-[60px]">2.755</div>
<div class="self-stretch text-white text-xl font-normal font-jakarta leading-loose">Jumlah RW</div>
</div>
</div>
<div class="self-stretch flex flex-col lg:flex-row justify-start items-start gap-8">
<!-- Stat 3 -->
<div class="flex-1 pl-8 border-l-4 border-white flex flex-col justify-start items-start gap-1">
<div class="self-stretch text-white text-3xl lg:text-5xl font-bold font-jakarta leading-[40px] lg:leading-[60px]">203.511</div>
<div class="self-stretch text-white text-xl font-normal font-jakarta leading-loose">Jumlah Rumah Memilah</div>
</div>
<!-- Stat 4 -->
<div class="flex-1 pl-8 border-l-4 border-white flex flex-col justify-start items-start gap-1">
<div class="self-stretch text-white text-3xl lg:text-5xl font-bold font-jakarta leading-[40px] lg:leading-[60px]">126.023</div>
<div class="self-stretch text-white text-xl font-normal font-jakarta leading-loose">Jumlah Nasabah</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- What is Rumah Memilah Section -->
<section class="w-full px-4 lg:px-28 py-24 bg-white flex flex-col justify-start items-center gap-20 overflow-hidden">
<div class="w-full max-w-[1280px] flex flex-col justify-start items-start gap-20">
<div class="self-stretch flex flex-col lg:flex-row justify-start items-center gap-20">
<!-- Image Section -->
<div class="w-full lg:w-[592px] h-[400px] lg:h-[592px] relative">
<div class="w-60 h-[552px] left-0 top-[40px] absolute bg-green-800"></div>
<img class="w-80 lg:w-96 h-[400px] lg:h-[552px] left-[40px] top-0 absolute object-cover" src="/images/hero-choosen.png" alt="Rumah Memilah Image" />
<img class="w-48 lg:w-64 h-48 lg:h-64 right-0 lg:left-[322px] top-[161px] absolute border-8 border-white object-cover" src="/images/hero-choosen-trash.png" alt="Logo" />
</div>
<!-- Content Section -->
<div class="flex-1 flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Rumah Memilah</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-6">
<h2 class="self-stretch text-gray-900 text-4xl font-bold font-jakarta leading-10">Apa Itu Rumah Memilah</h2>
<div class="self-stretch text-slate-700 text-lg font-normal font-jakarta leading-7">
Rumah Memilah adalah program dari BPSRW DKI Jakarta yang mendorong partisipasi aktif warga dalam memilah sampah rumah tangga menjadi beberapa kategori. Program ini bertujuan untuk mengurangi volume sampah yang masuk ke TPA (Tempat Pembuangan Akhir) dan meningkatkan tingkat daur ulang di Jakarta.<br/><br/>
Dengan bergabung menjadi Rumah Memilah, Anda berkontribusi langsung pada kebersihan lingkungan dan ekonomi sirkular di Jakarta, serta mendapatkan berbagai keuntungan menarik.
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- How Rumah Memilah Works Section -->
<section class="w-full px-4 lg:px-28 py-24 bg-gray-100 flex flex-col justify-start items-center gap-20 overflow-hidden">
<div class="w-full max-w-[1280px] flex flex-col justify-start items-start gap-20">
<div class="self-stretch flex flex-col justify-start items-start gap-14">
<div class="self-stretch flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-white rounded-full"></div>
</div>
<div class="text-white text-base font-bold font-jakarta leading-normal">Rumah Memilah</div>
</div>
<h2 class="self-stretch text-gray-900 text-4xl font-bold font-jakarta leading-10">Bagaimana Cara Kerja Rumah Memilah</h2>
</div>
</div>
<!-- Steps Grid -->
<div class="self-stretch grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Step 1 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex justify-between items-center">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eee8e8" viewBox="0 0 256 256"><path d="M230.93,220a8,8,0,0,1-6.93,4H32a8,8,0,0,1-6.92-12c15.23-26.33,38.7-45.21,66.09-54.16a72,72,0,1,1,73.66,0c27.39,8.95,50.86,27.83,66.09,54.16A8,8,0,0,1,230.93,220Z"></path></svg>
</div>
<div class="text-[56px] font-bold leading-[72px] capitalize font-inter text-transparent stroke-[#91B998] stroke-2" style="-webkit-text-stroke-width:2px;-webkit-text-stroke-color:#91B998;">
01
</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Daftar</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Mendaftar sebagai peserta program Rumah Memilah
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<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="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
<!-- Step 2 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex justify-between items-center">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eee8e8" viewBox="0 0 256 256"><path d="M227.81,66.76l-.08.09L160,139.17v55.49A16,16,0,0,1,152.87,208l-32,21.34A16,16,0,0,1,96,216V139.17L28.27,66.85l-.08-.09A16,16,0,0,1,40,40H216a16,16,0,0,1,11.84,26.76Z"></path></svg>
</div>
<div class="text-[56px] font-bold leading-[72px] capitalize font-inter text-transparent stroke-[#91B998] stroke-2" style="-webkit-text-stroke-width:2px;-webkit-text-stroke-color:#91B998;">
02
</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Pilah</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Memilah sampah organik, anorganik dan B3 di rumah
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<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="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
<!-- Step 3 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex justify-between items-center">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eee8e8" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM112,168a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm0-120H96V40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8Z"></path></svg>
</div>
<div class="text-[56px] font-bold leading-[72px] capitalize font-inter text-transparent stroke-[#91B998] stroke-2" style="-webkit-text-stroke-width:2px;-webkit-text-stroke-color:#91B998;">
03
</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Kumpulkan</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Mengumpulkan sampah terpilah ke bank sampah atau drop box
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<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="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
<!-- Step 4 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex justify-between items-center">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eee8e8" viewBox="0 0 256 256"><path d="M244.24,60a8,8,0,0,0-7.75-.4c-42.93,21-73.59,11.16-106,.78C96.4,49.53,61.2,38.28,12.49,62.06A8,8,0,0,0,8,69.24V189.17a8,8,0,0,0,11.51,7.19c42.93-21,73.59-11.16,106.05-.78,19.24,6.15,38.84,12.42,61,12.42,17.09,0,35.73-3.72,56.91-14.06a8,8,0,0,0,4.49-7.18V66.83A8,8,0,0,0,244.24,60ZM48,152a8,8,0,0,1-16,0V88a8,8,0,0,1,16,0Zm80,8a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm96,8a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
</div>
<div class="text-[56px] font-bold leading-[72px] capitalize font-inter text-transparent stroke-[#91B998] stroke-2" style="-webkit-text-stroke-width:2px;-webkit-text-stroke-color:#91B998;">
04
</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Dapatkan Manfaat</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Menerima insentif dan manfaat sebagai Rumah Memilah
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<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="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Benefits Section -->
<section class="w-full p-4 lg:p-28 bg-white flex flex-col justify-start items-center gap-20 overflow-hidden">
<div class="w-full max-w-[1280px] flex flex-col justify-start items-center gap-20">
<!-- Header -->
<div class="w-full max-w-[800px] flex flex-col justify-start items-center gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Keuntungan</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-6">
<h2 class="self-stretch text-center text-gray-900 text-4xl font-bold font-jakarta leading-10">
Keuntungan Menjadi Rumah Memilah
</h2>
<p class="self-stretch text-center text-slate-600 text-lg font-normal font-jakarta leading-7">
Bergabung dengan program Rumah Memilah memberi Anda berbagai manfaat ekonomi, sosial, dan lingkungan yang signifikan. Berikut adalah keuntungan yang akan Anda dapatkan:
</p>
</div>
</div>
<!-- Benefits Grid -->
<div class="self-stretch grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Benefit 1 - Economic -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-amber-100 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#DC6803" viewBox="0 0 256 256"><path d="M216,64H56a8,8,0,0,1,0-16H192a8,8,0,0,0,0-16H56A24,24,0,0,0,32,56V184a24,24,0,0,0,24,24H216a16,16,0,0,0,16-16V80A16,16,0,0,0,216,64Zm-36,80a12,12,0,1,1,12-12A12,12,0,0,1,180,144Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Insentif Ekonomi</h3>
<div class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Poin JakOne yang dapat ditukarkan dengan berbagai kebutuhan<br/>
Penghasilan tambahan dari penjualan sampah daur ulang<br/>
Potongan retribusi sampah bulanan hingga 20%<br/>
Kupon belanja di pasar rakyat dan mini market mitra
</div>
</div>
</div>
<!-- Benefit 2 - Environmental -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-[#D3E3D6] rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#247332" viewBox="0 0 256 256"><path d="M208,48a87.48,87.48,0,0,0-35.36,7.43c-15.1-25.37-39.92-38-41.06-38.59a8,8,0,0,0-7.16,0c-1.14.58-26,13.22-41.06,38.59A87.48,87.48,0,0,0,48,48a8,8,0,0,0-8,8V96a88.11,88.11,0,0,0,80,87.63v35.43L83.58,200.84a8,8,0,1,0-7.16,14.32l48,24a8,8,0,0,0,7.16,0l48-24a8,8,0,0,0-7.16-14.32L136,219.06V183.63A88.11,88.11,0,0,0,216,96V56A8,8,0,0,0,208,48ZM56,96V64.44A72.1,72.1,0,0,1,120,136v31.56A72.1,72.1,0,0,1,56,96Zm144,0a72.1,72.1,0,0,1-64,71.56V136a72.1,72.1,0,0,1,64-71.56Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Manfaat Lingkungan</h3>
<div class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Mengurangi sampah yang berakhir di TPA hingga 30%<br/>
Mengurangi emisi gas rumah kaca dari timbunan sampah<br/>
Penanganan sampah B3 yang lebih aman dan bertanggung jawab<br/>
Lingkungan rumah dan sekitar yang lebih bersih dan sehat
</div>
</div>
</div>
<!-- Benefit 3 - Social & Educational -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-gray-100 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#667085" viewBox="0 0 256 256"><path d="M230.4,219.19A8,8,0,0,1,224,232H32a8,8,0,0,1-6.4-12.8A67.88,67.88,0,0,1,53,197.51a40,40,0,1,1,53.93,0,67.42,67.42,0,0,1,21,14.29,67.42,67.42,0,0,1,21-14.29,40,40,0,1,1,53.93,0A67.85,67.85,0,0,1,230.4,219.19ZM27.2,126.4a8,8,0,0,0,11.2-1.6,52,52,0,0,1,83.2,0,8,8,0,0,0,12.8,0,52,52,0,0,1,83.2,0,8,8,0,0,0,12.8-9.61A67.85,67.85,0,0,0,203,93.51a40,40,0,1,0-53.93,0,67.42,67.42,0,0,0-21,14.29,67.42,67.42,0,0,0-21-14.29,40,40,0,1,0-53.93,0A67.88,67.88,0,0,0,25.6,115.2,8,8,0,0,0,27.2,126.4Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Manfaat Sosial & Pendidikan</h3>
<div class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Sertifikat Rumah Memilah yang diakui pemerintah DKI Jakarta<br/>
Akses ke workshop dan pelatihan pengelolaan sampah<br/>
Kunjungan edukatif ke fasilitas pengelolaan sampah<br/>
Menjadi bagian dari komunitas peduli lingkungan di Jakarta
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Regulation Section -->
<section class="w-full px-4 lg:px-28 py-24 bg-gray-100 flex flex-col justify-start items-center gap-20 overflow-hidden">
<div class="w-full max-w-[1280px] flex flex-col justify-start items-start gap-12">
<div class="self-stretch flex flex-col lg:flex-row justify-start items-center gap-12">
<!-- Content Section -->
<div class="flex-1 flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Pergub DKI Jakarta</div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-6">
<h2 class="self-stretch text-gray-900 text-4xl font-bold font-jakarta leading-10">Peraturan Gubernur DKI Jakarta</h2>
<!-- Regulation Card -->
<div class="w-full p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">No. 77 Tahun 2020</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Pengelolaan Sampah di Lingkungan RT/RW
</p>
</div>
<p class="self-stretch text-slate-700 text-base font-normal font-jakarta leading-normal">
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="self-stretch flex flex-wrap justify-start items-start gap-2">
<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 left-[2px] top-[1px] absolute 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 left-[1.50px] top-[1.50px] absolute 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<span class="text-green-800 text-sm font-bold font-jakarta">Unduh PDF</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Image Section -->
<div class="w-full lg:w-[592px] h-[400px] lg:h-[592px] relative transform">
<div class="w-60 h-[552px] absolute right-0 bottom-0 bg-green-800"></div>
<img class="w-80 lg:w-96 h-[400px] lg:h-[552px] absolute right-10 bottom-0 object-cover transform" src="/images/gubernur.png" alt="Regulation Image" />
<img class="w-48 lg:w-64 h-48 lg:h-64 left-0 top-[161px] absolute border-8 border-white object-cover" src="/images/person-01.png" alt="Logo" />
</div>
</div>
</div>
</section>
<!-- Goals Section -->
<section class="w-full p-4 lg:p-28 relative bg-green-950 flex flex-col justify-start items-center gap-20 overflow-hidden">
<!-- Background overlay -->
<div class="w-full h-full left-0 top-0 absolute opacity-60 overflow-hidden">
<div class="w-full h-full absolute bg-green-950"></div>
</div>
<!-- Content -->
<div class="w-full max-w-[1280px] relative z-10 flex flex-col justify-start items-start gap-10">
<div class="self-stretch flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Tujuan</div>
</div>
<h2 class="self-stretch text-white text-4xl font-bold font-jakarta leading-10">Tujuan Utama Pergub 77/2020</h2>
</div>
</div>
<!-- Goals Grid -->
<div class="self-stretch grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- Goal 1 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M96,208a8,8,0,0,1-8,8H40a24,24,0,0,1-20.77-36l28-48.3-13.82-8A8,8,0,0,1,35.33,109l32.77-8.77a8,8,0,0,1,9.8,5.66l8.79,32.77a8,8,0,0,1-11.73,9l-13.88-8L33.11,188A8,8,0,0,0,40,200H88A8,8,0,0,1,96,208ZM128,32a7.85,7.85,0,0,1,6.92,4l28,48.3-13.82,8A8,8,0,0,0,151,106.92l32.78,8.79a8.23,8.23,0,0,0,2.07.27,8,8,0,0,0,7.72-5.93l8.79-32.79a8,8,0,0,0-11.72-9l-13.89,8L148.77,28a24,24,0,0,0-41.54,0L84.07,68a8,8,0,0,0,13.85,8l23.16-40A7.85,7.85,0,0,1,128,32ZM236.73,180l-23.14-40a8,8,0,0,0-13.84,8l23.14,40A8,8,0,0,1,216,200H160V184a8,8,0,0,0-13.66-5.66l-24,24a8,8,0,0,0,0,11.32l24,24A8,8,0,0,0,160,232V216h56a24,24,0,0,0,20.77-36Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Pengurangan Sampah</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Mengurangi volume sampah dari sumbernya melalui pemilahan dan pengolahan di tingkat RT/RW.
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<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="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
<!-- Goal 2 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M230.4,219.19A8,8,0,0,1,224,232H32a8,8,0,0,1-6.4-12.8A67.88,67.88,0,0,1,53,197.51a40,40,0,1,1,53.93,0,67.42,67.42,0,0,1,21,14.29,67.42,67.42,0,0,1,21-14.29,40,40,0,1,1,53.93,0A67.85,67.85,0,0,1,230.4,219.19ZM27.2,126.4a8,8,0,0,0,11.2-1.6,52,52,0,0,1,83.2,0,8,8,0,0,0,12.8,0,52,52,0,0,1,83.2,0,8,8,0,0,0,12.8-9.61A67.85,67.85,0,0,0,203,93.51a40,40,0,1,0-53.93,0,67.42,67.42,0,0,0-21,14.29,67.42,67.42,0,0,0-21-14.29,40,40,0,1,0-53.93,0A67.88,67.88,0,0,0,25.6,115.2,8,8,0,0,0,27.2,126.4Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Partisipasi Masyarakat</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Meningkatkan keterlibatan warga dalam pengelolaan sampah di lingkungan tempat tinggal
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<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="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
<!-- Goal 3 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M223.45,40.07a8,8,0,0,0-7.52-7.52C139.8,28.08,78.82,51,52.82,94a87.09,87.09,0,0,0-12.76,49A101.72,101.72,0,0,0,46.7,175.2a4,4,0,0,0,6.61,1.43l85-86.3a8,8,0,0,1,11.32,11.32L56.74,195.94,42.55,210.13a8.2,8.2,0,0,0-.6,11.1,8,8,0,0,0,11.71.43l16.79-16.79c14.14,6.84,28.41,10.57,42.56,11.07q1.67.06,3.33.06A86.93,86.93,0,0,0,162,203.18C205,177.18,227.93,116.21,223.45,40.07Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Lingkungan Berkelanjutan</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Mewujudkan Jakarta yang bersih, sehat, dan memiliki pengelolaan sampah yang berkelanjutan
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<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="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section class="w-full px-4 lg:px-28 py-24 bg-white flex flex-col justify-start items-center gap-20 overflow-hidden">
<div class="w-full max-w-[1280px] flex flex-col justify-start items-start gap-20">
<div class="self-stretch flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Hubungi Kami</div>
</div>
<h2 class="self-stretch text-slate-800 text-4xl font-bold font-jakarta leading-10">Informasi Kontak</h2>
</div>
</div>
<!-- Contact Grid -->
<div class="self-stretch grid grid-cols-1 lg:grid-cols-3 gap-12">
<!-- Phone -->
<div class="flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg class="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/>
</svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h3 class="self-stretch text-gray-900 text-2xl font-bold font-jakarta leading-loose">Phone</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Senin-Jumat, 08.00-16.00 WIB
</p>
</div>
<a href="tel:02138657455" class="self-stretch text-green-800 text-base font-normal underline leading-normal">
(021) 3865745
</a>
</div>
</div>
<!-- Email -->
<div class="flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg class="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.89 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
</svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h3 class="self-stretch text-gray-900 text-2xl font-bold font-jakarta leading-loose">Email</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Respon dalam 1-2 hari kerja
</p>
</div>
<a href="mailto:sampah@jakarta.go.id" class="self-stretch text-green-800 text-base font-normal underline leading-normal">
sampah@jakarta.go.id
</a>
</div>
</div>
<!-- Office -->
<div class="flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg class="w-8 h-8 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="self-stretch flex flex-col justify-start items-start gap-6">
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h3 class="self-stretch text-gray-900 text-2xl font-bold font-jakarta leading-loose">Office</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Dinas Lingkungan Hidup DKI Jakarta
</p>
</div>
<div class="self-stretch text-green-800 text-base font-normal underline leading-normal">
Jl. Mandala V No. 67, Cililitan, Jakarta Timur
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Documents Section -->
<section class="w-full p-4 lg:p-28 bg-gray-100 flex flex-col justify-start items-center gap-20 overflow-hidden">
<div class="w-full max-w-[1280px] flex flex-col justify-start items-start gap-10">
<div class="self-stretch flex flex-col justify-start items-start gap-8">
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<!-- 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 left-[1.50px] top-[1.50px] absolute bg-green-800 rounded-full"></div>
</div>
<div class="text-green-800 text-base font-bold font-jakarta leading-normal">Dokumen</div>
</div>
<h2 class="self-stretch text-gray-900 text-4xl font-bold font-jakarta leading-10">Unduh Dokumen Pendukung</h2>
</div>
</div>
<!-- Documents Grid -->
<div class="self-stretch grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- Document 1 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-green-800 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M44,120H212a4,4,0,0,0,4-4V88a8,8,0,0,0-2.34-5.66l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v76A4,4,0,0,0,44,120ZM152,44l44,44H152Zm72,108.53a8.18,8.18,0,0,1-8.25,7.47H192v16h15.73a8.17,8.17,0,0,1,8.25,7.47,8,8,0,0,1-8,8.53H192v15.73a8.17,8.17,0,0,1-7.47,8.25,8,8,0,0,1-8.53-8V152a8,8,0,0,1,8-8h32A8,8,0,0,1,224,152.53ZM64,144H48a8,8,0,0,0-8,8v55.73A8.17,8.17,0,0,0,47.47,216,8,8,0,0,0,56,208v-8h7.4c15.24,0,28.14-11.92,28.59-27.15A28,28,0,0,0,64,144Zm-.35,40H56V160h8a12,12,0,0,1,12,13.16A12.25,12.25,0,0,1,63.65,184ZM128,144H112a8,8,0,0,0-8,8v56a8,8,0,0,0,8,8h15.32c19.66,0,36.21-15.48,36.67-35.13A36,36,0,0,0,128,144Zm-.49,56H120V160h8a20,20,0,0,1,20,20.77C147.58,191.59,138.34,200,127.51,200Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Pergub 77 Tahun 2020</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Dokumen lengkap peraturan
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<a href="#" class="rounded-full flex justify-center items-center gap-2 text-green-800 hover:text-green-700 transition-colors">
<span class="text-base font-semibold font-jakarta leading-normal">Download</span>
</a>
</div>
</div>
<!-- Document 2 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-amber-500 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M224,152.53a8.17,8.17,0,0,1-8.25,7.47H204v47.73a8.17,8.17,0,0,1-7.47,8.25,8,8,0,0,1-8.53-8V160H176.27a8.17,8.17,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h40A8,8,0,0,1,224,152.53ZM92,172.85C91.54,188.08,78.64,200,63.4,200H56v7.73A8.17,8.17,0,0,1,48.53,216,8,8,0,0,1,40,208V152a8,8,0,0,1,8-8H64A28,28,0,0,1,92,172.85Zm-16-2A12.25,12.25,0,0,0,63.65,160H56v24h8A12,12,0,0,0,76,170.84Zm84,2C159.54,188.08,146.64,200,131.4,200H124v7.73a8.17,8.17,0,0,1-7.47,8.25,8,8,0,0,1-8.53-8V152a8,8,0,0,1,8-8h16A28,28,0,0,1,160,172.85Zm-16-2A12.25,12.25,0,0,0,131.65,160H124v24h8A12,12,0,0,0,144,170.84ZM40,116V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v28a4,4,0,0,1-4,4H44A4,4,0,0,1,40,116ZM152,88h44L152,44Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Presentasi Sosialisasi</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Bahan presentasi untuk warga
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<a href="#" class="rounded-full flex justify-center items-center gap-2 text-green-800 hover:text-green-700 transition-colors">
<span class="text-base font-semibold font-jakarta leading-normal">Download</span>
</a>
</div>
</div>
<!-- Document 3 -->
<div class="p-6 bg-white rounded-2xl border border-gray-200 flex flex-col justify-start items-start gap-6">
<div class="w-16 h-16 bg-slate-700 rounded-full flex flex-col justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#ffffff" viewBox="0 0 256 256"><path d="M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,176H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Zm0-32H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Zm-8-56V44l44,44Z"></path></svg>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-2">
<h3 class="text-gray-900 text-2xl font-bold font-jakarta leading-loose">Panduan Praktis</h3>
<p class="self-stretch text-gray-500 text-base font-normal font-jakarta leading-normal">
Langkah-langkah implementasi untuk RT/RW
</p>
</div>
<div class="self-stretch pt-[3px] pb-1 flex justify-start items-start">
<a href="#" class="rounded-full flex justify-center items-center gap-2 text-green-800 hover:text-green-700 transition-colors">
<span class="text-base font-semibold font-jakarta leading-normal">Download</span>
</a>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<script>
// Smooth scrolling for 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>
}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,140 @@
<!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">
<!-- Phosphor Icons -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@@phosphor-icons/web@2.1.2/src/regular/style.css" />
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@@phosphor-icons/web@2.1.2/src/fill/style.css" />
<!-- 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 - White Background -->
<header class="w-full h-20 px-4 lg:px-28 bg-white flex flex-col justify-center items-center shadow-sm">
<div class="self-stretch flex justify-center items-center gap-8">
<!-- Logo -->
<div class="flex-1 flex justify-start items-center gap-2">
<img class="w-10 h-10" src="/images/logo.png" alt="Logo" />
</div>
<!-- Navigation Menu - Desktop -->
<div class="hidden lg:flex justify-start items-center gap-8">
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-gray-100 px-4 py-2 transition-colors">
<div class="text-green-800 text-base font-semibold font-jakarta leading-normal">Beranda</div>
</a>
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-gray-100 px-4 py-2 transition-colors">
<div class="text-slate-600 text-base font-semibold font-jakarta leading-normal">Tentang</div>
</a>
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-gray-100 px-4 py-2 transition-colors">
<div class="text-slate-600 text-base font-semibold font-jakarta leading-normal">Regulasi</div>
</a>
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-gray-100 px-4 py-2 transition-colors">
<div class="text-slate-600 text-base font-semibold font-jakarta leading-normal">Edukasi</div>
</a>
</div>
<!-- Login Button -->
<div class="flex-1 flex justify-end items-center gap-4">
<button class="px-8 py-2.5 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 leading-normal">Login</span>
</button>
</div>
<!-- Mobile Menu Button -->
<div class="dropdown lg:hidden">
<div tabindex="0" role="button" class="btn btn-ghost">
<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 href="#">Beranda</a></li>
<li><a href="#">Tentang</a></li>
<li><a href="#">Regulasi</a></li>
<li><a href="#">Edukasi</a></li>
</ul>
</div>
</div>
</header>
<!-- Main Content -->
<main role="main">
@RenderBody()
</main>
<!-- Footer -->
<footer class="w-full px-4 lg:px-28 py-20 relative bg-green-950 flex flex-col justify-start items-center gap-20 overflow-hidden">
<!-- Background overlay -->
<div class="w-full h-full left-0 top-0 absolute overflow-hidden">
<img src="/images/leaf.svg" class="w-full h-full absolute bg-green-950" alt="Leaf" />
</div>
<!-- Footer Content -->
<div class="w-full max-w-[1280px] relative z-10 flex flex-col justify-start items-start gap-20">
<div class="self-stretch flex flex-col lg:flex-row justify-start items-start gap-8">
<!-- Logo -->
<div class="flex-1 flex justify-start items-center gap-2 overflow-hidden">
<div class="w-16 h-16 relative">
<img class="w-16 h-16" src="/images/logo-white.png" alt="Logo" />
</div>
</div>
<!-- Navigation Menu -->
<div class="flex justify-start items-center gap-8">
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-green-800/20 px-4 py-2 transition-colors">
<div class="text-white text-base font-semibold font-jakarta leading-normal">Beranda</div>
</a>
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-green-800/20 px-4 py-2 transition-colors">
<div class="text-white text-base font-semibold font-jakarta leading-normal">Tentang</div>
</a>
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-green-800/20 px-4 py-2 transition-colors">
<div class="text-white text-base font-semibold font-jakarta leading-normal">Regulasi</div>
</a>
<a href="#" class="rounded-full flex justify-center items-center gap-2 hover:bg-green-800/20 px-4 py-2 transition-colors">
<div class="text-white text-base font-semibold font-jakarta leading-normal">Edukasi</div>
</a>
</div>
<!-- Right space -->
<div class="flex-1 flex justify-end items-center gap-3"></div>
</div>
<!-- Footer Bottom -->
<div class="self-stretch flex flex-col justify-start items-center gap-8">
<div class="self-stretch h-px bg-green-950"></div>
<div class="flex justify-start items-start gap-6">
<div class="text-white text-sm font-normal leading-tight">
Copyright © @DateTime.Now.Year Dinas Lingkungan Hidup Provinsi DKI Jakarta.
</div>
</div>
</div>
</div>
</footer>
<!-- Scripts -->
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@ -0,0 +1,3 @@
@using BankSampahApp
@using BankSampahApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@ -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
}
}

38
appsettings.json 100644
View File

@ -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
}
}

51
docker-compose.yml 100644
View File

@ -0,0 +1,51 @@
version: '3.8'
services:
# Main application service
bank-sampah-app:
build:
context: .
dockerfile: Dockerfile
target: final
container_name: bank-sampah-app
ports:
- "8080:8080"
- "8081:8081"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
volumes:
# Mount configuration files (optional)
- ./appsettings.Production.json:/app/appsettings.Production.json:ro
# Mount logs directory
- ./logs:/app/logs
networks:
- bank-sampah-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Nginx reverse proxy (optional)
nginx:
image: nginx:alpine
container_name: bank-sampah-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./logs/nginx:/var/log/nginx
networks:
- bank-sampah-network
restart: unless-stopped
depends_on:
- bank-sampah-app
networks:
bank-sampah-network:
driver: bridge

13
package.json 100644
View File

@ -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"
}
}

864
pnpm-lock.yaml 100644
View File

@ -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: {}

54
tailwind.config.js 100644
View File

@ -0,0 +1,54 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./Views/**/*.cshtml",
"./wwwroot/js/**/*.js"
],
theme: {
extend: {
fontFamily: {
'jakarta': ['Plus Jakarta Sans', 'sans-serif'],
'inter': ['Inter', '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",
],
},
}

View File

@ -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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

378
wwwroot/js/site.js 100644
View File

@ -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;
}