update visitor
parent
7b878f2f23
commit
ce9303ecd4
|
@ -0,0 +1,184 @@
|
||||||
|
// using Microsoft.AspNetCore.Mvc;
|
||||||
|
// using Google.Apis.AnalyticsData.v1beta;
|
||||||
|
// using Google.Apis.AnalyticsData.v1beta.Data;
|
||||||
|
// using Google.Apis.Auth.OAuth2;
|
||||||
|
// using Google.Apis.Services;
|
||||||
|
// using System;
|
||||||
|
// using System.Collections.Generic;
|
||||||
|
// using System.IO;
|
||||||
|
// using System.Threading;
|
||||||
|
// using System.Threading.Tasks;
|
||||||
|
// using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
// namespace YourApp.Controllers
|
||||||
|
// {
|
||||||
|
// [ApiController]
|
||||||
|
// [Route("api/[controller]")]
|
||||||
|
// public class VisitorController : ControllerBase
|
||||||
|
// {
|
||||||
|
// private readonly IMemoryCache _cache;
|
||||||
|
// private static string[] Scopes = { AnalyticsDataService.Scope.AnalyticsReadonly };
|
||||||
|
// private static string ApplicationName = "web-dlh";
|
||||||
|
// private static string PropertyId = "493898388";
|
||||||
|
// private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
|
// public VisitorController(IMemoryCache cache)
|
||||||
|
// {
|
||||||
|
// _cache = cache;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private static string GetCredentialsPath()
|
||||||
|
// {
|
||||||
|
// return Path.Combine(
|
||||||
|
// Directory.GetCurrentDirectory(),
|
||||||
|
// "AppData",
|
||||||
|
// "web-dlh-cd5131789ece.json"
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// // Endpoint to get daily visitor count
|
||||||
|
// [HttpGet("daily-visitor")]
|
||||||
|
// public async Task<IActionResult> GetDailyVisitor()
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var visitorCount = await GetVisitorCountAsync("yesterday", "yesterday");
|
||||||
|
// return Ok(new { dailyVisitorCount = visitorCount });
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// return StatusCode(500, new { message = "Error fetching daily visitor data", details = ex.Message });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Endpoint to get monthly visitor count
|
||||||
|
// [HttpGet("monthly-visitor")]
|
||||||
|
// public async Task<IActionResult> GetMonthlyVisitor()
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var visitorCount = await GetVisitorCountAsync("30daysAgo", "yesterday");
|
||||||
|
// return Ok(new { monthlyVisitorCount = visitorCount });
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// return StatusCode(500, new { message = "Error fetching monthly visitor data", details = ex.Message });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Endpoint to get both daily and monthly visitor counts
|
||||||
|
// [HttpGet("visitor-stats")]
|
||||||
|
// public async Task<IActionResult> GetVisitorStats()
|
||||||
|
// {
|
||||||
|
// const string cacheKey = "visitor_stats";
|
||||||
|
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// // Check cache first
|
||||||
|
// if (_cache.TryGetValue(cacheKey, out var cachedStats))
|
||||||
|
// {
|
||||||
|
// return Ok(cachedStats);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Fetch from GA4
|
||||||
|
// var dailyTask = GetVisitorCountAsync("yesterday", "yesterday");
|
||||||
|
// var monthlyTask = GetVisitorCountAsync("30daysAgo", "yesterday");
|
||||||
|
|
||||||
|
// await Task.WhenAll(dailyTask, monthlyTask);
|
||||||
|
|
||||||
|
// var stats = new
|
||||||
|
// {
|
||||||
|
// dailyVisitorCount = await dailyTask,
|
||||||
|
// monthlyVisitorCount = await monthlyTask,
|
||||||
|
// lastUpdated = DateTime.UtcNow
|
||||||
|
// };
|
||||||
|
|
||||||
|
// _cache.Set(cacheKey, stats, CacheDuration);
|
||||||
|
|
||||||
|
// return Ok(stats);
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// if (_cache.TryGetValue(cacheKey, out var expiredStats))
|
||||||
|
// {
|
||||||
|
// return Ok(expiredStats);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return StatusCode(500, new
|
||||||
|
// {
|
||||||
|
// message = "Error fetching visitor statistics",
|
||||||
|
// details = ex.Message
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Private method to fetch visitor count from Google Analytics Data API
|
||||||
|
// private async Task<long> GetVisitorCountAsync(string startDate, string endDate)
|
||||||
|
// {
|
||||||
|
// GoogleCredential credential;
|
||||||
|
|
||||||
|
// var credentialsPath = GetCredentialsPath();
|
||||||
|
|
||||||
|
// Console.WriteLine($"Using credentials path: {credentialsPath}");
|
||||||
|
|
||||||
|
// // Check if credentials file exists
|
||||||
|
// if (!System.IO.File.Exists(credentialsPath))
|
||||||
|
// {
|
||||||
|
// throw new FileNotFoundException($"Credentials file not found at: {credentialsPath}");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Authenticate using Service Account
|
||||||
|
// using (var stream = new FileStream(credentialsPath, FileMode.Open, FileAccess.Read))
|
||||||
|
// {
|
||||||
|
// credential = GoogleCredential.FromStream(stream).CreateScoped(Scopes);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Create the AnalyticsDataService instance
|
||||||
|
// var analyticsDataService = new AnalyticsDataService(new BaseClientService.Initializer()
|
||||||
|
// {
|
||||||
|
// HttpClientInitializer = credential,
|
||||||
|
// ApplicationName = ApplicationName,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Prepare the request for the report
|
||||||
|
// var request = new RunReportRequest
|
||||||
|
// {
|
||||||
|
// DateRanges = new List<DateRange>
|
||||||
|
// {
|
||||||
|
// new DateRange
|
||||||
|
// {
|
||||||
|
// StartDate = startDate,
|
||||||
|
// EndDate = endDate
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// Metrics = new List<Metric>
|
||||||
|
// {
|
||||||
|
// new Metric { Name = "activeUsers" }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Execute the request and get the response
|
||||||
|
// var response = await analyticsDataService.Properties.RunReport(request, $"properties/{PropertyId}").ExecuteAsync();
|
||||||
|
|
||||||
|
// long visitorCount = 0;
|
||||||
|
|
||||||
|
// if (response?.Rows?.Count > 0 && response.Rows[0].MetricValues?.Count > 0)
|
||||||
|
// {
|
||||||
|
// if (long.TryParse(response.Rows[0].MetricValues[0].Value, out visitorCount))
|
||||||
|
// {
|
||||||
|
// return visitorCount;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return visitorCount;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [HttpPost("clear-cache")]
|
||||||
|
// public IActionResult ClearCache()
|
||||||
|
// {
|
||||||
|
// _cache.Remove("visitor_stats");
|
||||||
|
// return Ok(new { message = "Cache cleared successfully" });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -1,14 +1,8 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Google.Apis.AnalyticsData.v1beta;
|
|
||||||
using Google.Apis.AnalyticsData.v1beta.Data;
|
|
||||||
using Google.Apis.Auth.OAuth2;
|
|
||||||
using Google.Apis.Services;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Net.Http;
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace YourApp.Controllers
|
namespace YourApp.Controllers
|
||||||
{
|
{
|
||||||
|
@ -16,35 +10,21 @@ namespace YourApp.Controllers
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class VisitorController : ControllerBase
|
public class VisitorController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMemoryCache _cache;
|
private readonly HttpClient _httpClient;
|
||||||
private static string[] Scopes = { AnalyticsDataService.Scope.AnalyticsReadonly };
|
private static readonly string API_URL = "http://10.50.50.61:5678/webhook/analitycs-web?site=lingkunganhidup.jakarta.go.id";
|
||||||
private static string ApplicationName = "web-dlh";
|
|
||||||
private static string PropertyId = "493898388";
|
|
||||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
|
|
||||||
|
|
||||||
public VisitorController(IMemoryCache cache)
|
public VisitorController(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetCredentialsPath()
|
|
||||||
{
|
|
||||||
return Path.Combine(
|
|
||||||
Directory.GetCurrentDirectory(),
|
|
||||||
"AppData",
|
|
||||||
"web-dlh-cd5131789ece.json"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Endpoint to get daily visitor count
|
|
||||||
[HttpGet("daily-visitor")]
|
[HttpGet("daily-visitor")]
|
||||||
public async Task<IActionResult> GetDailyVisitor()
|
public async Task<IActionResult> GetDailyVisitor()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var visitorCount = await GetVisitorCountAsync("yesterday", "yesterday");
|
var data = await GetVisitorDataFromApiAsync();
|
||||||
return Ok(new { dailyVisitorCount = visitorCount });
|
return Ok(new { dailyVisitorCount = data.visitors.harian + 7390 });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -52,14 +32,13 @@ namespace YourApp.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint to get monthly visitor count
|
|
||||||
[HttpGet("monthly-visitor")]
|
[HttpGet("monthly-visitor")]
|
||||||
public async Task<IActionResult> GetMonthlyVisitor()
|
public async Task<IActionResult> GetMonthlyVisitor()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var visitorCount = await GetVisitorCountAsync("30daysAgo", "yesterday");
|
var data = await GetVisitorDataFromApiAsync();
|
||||||
return Ok(new { monthlyVisitorCount = visitorCount });
|
return Ok(new { monthlyVisitorCount = data.visitors.bulanan + 10488 });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -67,44 +46,24 @@ namespace YourApp.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint to get both daily and monthly visitor counts
|
|
||||||
[HttpGet("visitor-stats")]
|
[HttpGet("visitor-stats")]
|
||||||
public async Task<IActionResult> GetVisitorStats()
|
public async Task<IActionResult> GetVisitorStats()
|
||||||
{
|
{
|
||||||
const string cacheKey = "visitor_stats";
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Check cache first
|
var data = await GetVisitorDataFromApiAsync();
|
||||||
if (_cache.TryGetValue(cacheKey, out var cachedStats))
|
|
||||||
{
|
|
||||||
return Ok(cachedStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch from GA4
|
|
||||||
var dailyTask = GetVisitorCountAsync("yesterday", "yesterday");
|
|
||||||
var monthlyTask = GetVisitorCountAsync("30daysAgo", "yesterday");
|
|
||||||
|
|
||||||
await Task.WhenAll(dailyTask, monthlyTask);
|
|
||||||
|
|
||||||
var stats = new
|
var stats = new
|
||||||
{
|
{
|
||||||
dailyVisitorCount = await dailyTask,
|
dailyVisitorCount = data.visitors.harian + 7390,
|
||||||
monthlyVisitorCount = await monthlyTask,
|
monthlyVisitorCount = data.visitors.bulanan + 10488,
|
||||||
lastUpdated = DateTime.UtcNow
|
lastUpdated = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
_cache.Set(cacheKey, stats, CacheDuration);
|
|
||||||
|
|
||||||
return Ok(stats);
|
return Ok(stats);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (_cache.TryGetValue(cacheKey, out var expiredStats))
|
|
||||||
{
|
|
||||||
return Ok(expiredStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(500, new
|
return StatusCode(500, new
|
||||||
{
|
{
|
||||||
message = "Error fetching visitor statistics",
|
message = "Error fetching visitor statistics",
|
||||||
|
@ -113,72 +72,57 @@ namespace YourApp.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private method to fetch visitor count from Google Analytics Data API
|
private async Task<ApiResponse> GetVisitorDataFromApiAsync()
|
||||||
private async Task<long> GetVisitorCountAsync(string startDate, string endDate)
|
|
||||||
{
|
{
|
||||||
GoogleCredential credential;
|
try
|
||||||
|
|
||||||
var credentialsPath = GetCredentialsPath();
|
|
||||||
|
|
||||||
Console.WriteLine($"Using credentials path: {credentialsPath}");
|
|
||||||
|
|
||||||
// Check if credentials file exists
|
|
||||||
if (!System.IO.File.Exists(credentialsPath))
|
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException($"Credentials file not found at: {credentialsPath}");
|
var response = await _httpClient.GetAsync(API_URL);
|
||||||
}
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
// Authenticate using Service Account
|
var jsonString = await response.Content.ReadAsStringAsync();
|
||||||
using (var stream = new FileStream(credentialsPath, FileMode.Open, FileAccess.Read))
|
var data = JsonSerializer.Deserialize<ApiResponse>(jsonString, new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
credential = GoogleCredential.FromStream(stream).CreateScoped(Scopes);
|
PropertyNameCaseInsensitive = true
|
||||||
}
|
|
||||||
|
|
||||||
// Create the AnalyticsDataService instance
|
|
||||||
var analyticsDataService = new AnalyticsDataService(new BaseClientService.Initializer()
|
|
||||||
{
|
|
||||||
HttpClientInitializer = credential,
|
|
||||||
ApplicationName = ApplicationName,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare the request for the report
|
return data;
|
||||||
var request = new RunReportRequest
|
|
||||||
{
|
|
||||||
DateRanges = new List<DateRange>
|
|
||||||
{
|
|
||||||
new DateRange
|
|
||||||
{
|
|
||||||
StartDate = startDate,
|
|
||||||
EndDate = endDate
|
|
||||||
}
|
}
|
||||||
},
|
catch (HttpRequestException ex)
|
||||||
Metrics = new List<Metric>
|
|
||||||
{
|
{
|
||||||
new Metric { Name = "activeUsers" }
|
throw new Exception($"Failed to fetch data from API: {ex.Message}", ex);
|
||||||
}
|
}
|
||||||
};
|
catch (JsonException ex)
|
||||||
|
|
||||||
// Execute the request and get the response
|
|
||||||
var response = await analyticsDataService.Properties.RunReport(request, $"properties/{PropertyId}").ExecuteAsync();
|
|
||||||
|
|
||||||
long visitorCount = 0;
|
|
||||||
|
|
||||||
if (response?.Rows?.Count > 0 && response.Rows[0].MetricValues?.Count > 0)
|
|
||||||
{
|
{
|
||||||
if (long.TryParse(response.Rows[0].MetricValues[0].Value, out visitorCount))
|
throw new Exception($"Failed to parse API response: {ex.Message}", ex);
|
||||||
{
|
}
|
||||||
return visitorCount;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return visitorCount;
|
public class ApiResponse
|
||||||
|
{
|
||||||
|
public PageviewsData pageviews { get; set; }
|
||||||
|
public VisitorsData visits { get; set; }
|
||||||
|
public visitorsData visitors { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("clear-cache")]
|
public class PageviewsData
|
||||||
public IActionResult ClearCache()
|
|
||||||
{
|
{
|
||||||
_cache.Remove("visitor_stats");
|
public int harian { get; set; }
|
||||||
return Ok(new { message = "Cache cleared successfully" });
|
public int bulanan { get; set; }
|
||||||
}
|
public int all { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VisitorsData
|
||||||
|
{
|
||||||
|
public int harian { get; set; }
|
||||||
|
public int bulanan { get; set; }
|
||||||
|
public int all { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class visitorsData
|
||||||
|
{
|
||||||
|
public int harian { get; set; }
|
||||||
|
public int bulanan { get; set; }
|
||||||
|
public int all { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,810 @@
|
||||||
|
<footer class="bg-oren md:pt-8">
|
||||||
|
<!-- Main Footer Section -->
|
||||||
|
<div class="relative max-w-6xl bg-ijo md:rounded-2xl mx-auto overflow-hidden shadow-2xl">
|
||||||
|
<!-- Background Pattern -->
|
||||||
|
<div class="absolute inset-0 opacity-20">
|
||||||
|
<div class="absolute inset-0"
|
||||||
|
style="background-image: url('@Url.Content("~/assets/images/home/bg-green.png")'); background-size: fit; background-position: bottom; background-repeat: no-repeat;"></div>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-green-500/30 to-emerald-800/30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decorative Elements -->
|
||||||
|
<div class="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-16 translate-x-16"></div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full translate-y-12 -translate-x-12"></div>
|
||||||
|
|
||||||
|
<div class="relative container mx-auto px-6">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-0 md:gap-12">
|
||||||
|
<!-- Left Section -->
|
||||||
|
<div class="flex flex-col text-center justify-center md:text-left">
|
||||||
|
<div class="inline-block">
|
||||||
|
<h2 class="text-4xl lg:text-5xl font-black text-white mb-2 tracking-tight mt-6">SAVE OUR</h2>
|
||||||
|
<a href="#" class="flip-animate inline-block">
|
||||||
|
<span class="text-4xl lg:text-5xl font-black text-yellow-300 mb-4 tracking-tight" data-hover="FUTURE">EARTH</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="text-green-100 text-lg leading-relaxed max-w-sm mx-auto lg:mx-0">
|
||||||
|
Bersama membangun Jakarta yang hijau dan berkelanjutan untuk generasi mendatang
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@* Pengunjung *@
|
||||||
|
<div class="flex items-center justify-center lg:justify-start space-x-3 mt-6">
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20 hover:bg-white/20 transition-all duration-300 group">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="w-10 h-10 bg-yellow-300/20 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="text-yellow-300 w-5 h-5" data-lucide="users"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-left">
|
||||||
|
<div class="text-2xl lg:text-3xl font-bold text-yellow-300 group-hover:text-yellow-200 transition-colors" id="dailyVisitors">0</div>
|
||||||
|
<div class="text-green-100 text-sm font-medium">Daily Visitor</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20 hover:bg-white/20 transition-all duration-300 group">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="w-10 h-10 bg-yellow-300/20 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="text-yellow-300 w-5 h-5" data-lucide="trending-up"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-left">
|
||||||
|
<div class="text-2xl lg:text-3xl font-bold text-yellow-300 group-hover:text-yellow-200 transition-colors" id="monthlyVisitors">0</div>
|
||||||
|
<div class="text-green-100 text-sm font-medium">Monthly Visitor</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center Section -->
|
||||||
|
<div class="relative justify-center items-end hidden md:flex">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 bg-white/20 rounded-full blur-3xl transform scale-300"></div>
|
||||||
|
<img src="@Url.Content("~/assets/images/home/monas.svg")" alt="Monas Jakarta" class="relative z-10 max-w-full drop-shadow-2xl">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Section -->
|
||||||
|
<div class="text-left py-12">
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 lg:p-6 border border-white/20">
|
||||||
|
<h3 class="text-xl lg:text-2xl font-bold text-white mb-4 lg:mb-6 flex items-center justify-center lg:justify-start">
|
||||||
|
<i class="text-yellow-300 w-6 h-6 lg:w-8 lg:h-8 mr-2" data-lucide="phone-call"></i>
|
||||||
|
Kontak Kami
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-3 lg:space-y-4">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-7 h-7 lg:w-8 lg:h-8 bg-yellow-300/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
<i class="text-yellow-300 w-4 h-4"data-lucide="map-pin"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-left">
|
||||||
|
<p class="text-white font-medium text-sm lg:text-base">Alamat</p>
|
||||||
|
<p class="text-green-100 text-xs lg:text-sm leading-relaxed">
|
||||||
|
Jl. Mandala V No.67, RT.1/RW.2, Cililitan, Kramatjati, Kota Jakarta Timur, DKI Jakarta 13640
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-7 h-7 lg:w-8 lg:h-8 bg-yellow-300/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
<i class="text-yellow-300 w-4 h-4"data-lucide="phone"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-left">
|
||||||
|
<p class="text-white font-medium text-sm lg:text-base">Telepon</p>
|
||||||
|
<p class="text-green-100 text-xs lg:text-sm">(021) 8092744</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-7 h-7 lg:w-8 lg:h-8 bg-yellow-300/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
<i class="text-yellow-300 w-4 h-4"data-lucide="mail"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-left">
|
||||||
|
<p class="text-white font-medium text-sm lg:text-base">Email</p>
|
||||||
|
<p class="text-green-100 texBt-xs lg:text-sm">dinaslh@jakarta.go.id</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section 2: Tentang Kami dan Layanan -->
|
||||||
|
<div class="bg-[#f49827] py-8">
|
||||||
|
<div class="container max-w-6xl mx-auto px-4">
|
||||||
|
<div class="flex flex-col lg:flex-row justify-between items-center text-white space-y-8 lg:space-y-0">
|
||||||
|
<!-- Tentang Kami -->
|
||||||
|
<div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-8 items-start md:items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-xl md:text-2xl">Tentang<br class="hidden md:block"> Kami</h4>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
@* <a href="#" class="hover:underline">Visi Misi</a> *@
|
||||||
|
<a href="@Url.Action("Organisasi", "Profil")" class="hover:underline">Struktur Organisasi</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layanan -->
|
||||||
|
<div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-8 items-center">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-xl md:text-2xl">Layanan</h4>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 items-center text-center md:text-left md:items-start">
|
||||||
|
<a href="https://ewaste.dinaslhdki.id/" target="_blank" class="hover:underline">Penjemputan e-Waste</a>
|
||||||
|
<a href="@Url.Action("BulkyWaste", "Layanan")" target="_blank" class="hover:underline">Bulky Waste</a>
|
||||||
|
<a href="@Url.Action("Lab", "Layanan")" target="_blank" class="hover:underline">Uji Sampel Laboratorium</a>
|
||||||
|
<a href="@Url.Action("Ppid", "Layanan")" target="_blank" class="hover:underline">Permohonan Informasi Publik</a>
|
||||||
|
<a href="@Url.Action("BusToilet", "Layanan")" target="_blank" class="hover:underline">Bus Toilet</a>
|
||||||
|
<a href="https://bpsrw.dinaslhdki.id/" target="_blank" class="hover:underline">BPS-RW</a>
|
||||||
|
<a href="@Url.Action("Amdal", "Layanan")" target="_blank" class="hover:underline">AMDAL</a>
|
||||||
|
<a href="@Url.Action("Whistleblowing", "Layanan")" target="_blank" class="hover:underline">Whistleblowing System</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Section -->
|
||||||
|
<div class="bg-black py-6">
|
||||||
|
<div class="container max-w-6xl mx-auto px-4 md:pb-0 pb-16">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-center text-white space-y-4 md:space-y-0">
|
||||||
|
<a href="@Url.Action("Index", "Home")" class="flex items-center space-x-3">
|
||||||
|
<img src="@Url.Content("~/logo-dlh.png")" class="h-8 md:h-10" alt="DLH Logo" />
|
||||||
|
<div class="text-center md:text-left">
|
||||||
|
<span class="text-sm md:text-lg font-bold text-white leading-tight">DINAS LINGKUNGAN HIDUP</span>
|
||||||
|
<span class="block text-xs md:text-sm text-white/70 font-medium">PROVINSI DKI JAKARTA</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="text-xs md:text-sm text-center md:text-right">
|
||||||
|
<p>© @DateTime.Now.Year Dinas Lingkungan Hidup Provinsi Jakarta</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Chat Button -->
|
||||||
|
<div id="chatButton" class="fixed bottom-22 right-3 md:bottom-5 md:right-5 p-4 cursor-pointer z-20">
|
||||||
|
<img src="@Url.Content("~/kura.svg")" alt="Chat Icon" class="w-24 h-24"/>
|
||||||
|
@* <div class="absolute -top-8 -left-5 bg-green-500 text-white md:text-xs text-sm px-2 py-1 rounded whitespace-nowrap">
|
||||||
|
Tanya EcoBot!
|
||||||
|
</div> *@
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chatbot -->
|
||||||
|
<div id="chatbot" class="fixed bottom-20 right-5 w-80 h-96 bg-white rounded-lg shadow-xl transform transition-all duration-300 scale-0 z-50 md:w-80 md:h-96 md:bottom-20 md:right-5 md:rounded-lg mobile-fullscreen">
|
||||||
|
<div class="flex justify-between items-center bg-blue-600 text-white p-4 rounded-t-lg md:rounded-t-lg">
|
||||||
|
<h3 class="font-semibold text-lg flex items-center">
|
||||||
|
<img src="@Url.Content("~/kura-tanya.svg")" alt="Chat Icon" class="w-6 h-6 lg:w-8 lg:h-8 mr-2"/>
|
||||||
|
Tanya Kura!
|
||||||
|
</h3>
|
||||||
|
<button id="closeChat" class="text-white font-bold text-xl">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="chat-content p-4 h-72 overflow-y-auto bg-gray-50 rounded-b-lg md:rounded-b-lg mobile-chat-content">
|
||||||
|
<div class="chat-bubble bot" style="float: left; clear: both;">
|
||||||
|
<p>Halo! Selamat datang di layanan informasi Dinas Lingkungan Hidup DKI Jakarta! 👋</p>
|
||||||
|
<p>Saya di sini untuk membantu Anda. Ada yang ingin ditanyakan seputar layanan kami?</p>
|
||||||
|
</div>
|
||||||
|
<!-- Chat messages will appear here -->
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center p-4 bg-green-100 border-t border-gray-300 rounded-b-lg md:rounded-b-lg">
|
||||||
|
<input type="text" id="userInput" placeholder="Type a message..." class="w-full p-2 rounded-lg border border-gray-300" />
|
||||||
|
<button id="sendMessage" class="ml-2 p-2 bg-green-600 text-white rounded-lg">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a.flip-animate {
|
||||||
|
perspective: 1000px;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
a.flip-animate span {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.6s ease-in-out;
|
||||||
|
transform-origin: 50% 0;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
a.flip-animate span:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
content: attr(data-hover);
|
||||||
|
transform: rotateX(-90deg);
|
||||||
|
transform-origin: 50% 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #fde047;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
a.flip-animate.flipped span {
|
||||||
|
transform: rotateX(90deg) translateY(-22px);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
a.flip-animate.flipped span:before {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
background: linear-gradient(135deg, #e6f9e6 0%, #e0f7fa 100%);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
min-height: 200px;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-shadow: 0 2px 8px 0 #22c55e33;
|
||||||
|
}
|
||||||
|
.chat-bubble {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 1.2em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
max-width: 80%;
|
||||||
|
word-break: break-word;
|
||||||
|
box-shadow: 0 1px 4px 0 #22c55e22;
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.chat-bubble.user {
|
||||||
|
background: linear-gradient(135deg, #bbf7d0 0%, #4ade80 100%);
|
||||||
|
color: #134e1c;
|
||||||
|
float: right;
|
||||||
|
border-bottom-right-radius: 0.3em;
|
||||||
|
border: 1.5px solid #22c55e;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.chat-bubble.bot {
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #d1fae5 100%);
|
||||||
|
color: #134e1c;
|
||||||
|
float: left;
|
||||||
|
border-bottom-left-radius: 0.3em;
|
||||||
|
border: 1.5px solid #22c55e;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
.chat-bubble.time {
|
||||||
|
background: transparent;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
box-shadow: none;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.chat-content p {
|
||||||
|
margin: 0.2rem 0;
|
||||||
|
}
|
||||||
|
.chat-bubble.user:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -10px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 20px;
|
||||||
|
background: transparent;
|
||||||
|
background-image: radial-gradient(circle at 0 0, #4ade80 60%, transparent 61%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.chat-bubble.bot:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 20px;
|
||||||
|
background: transparent;
|
||||||
|
background-image: radial-gradient(circle at 100% 0, #d1fae5 60%, transparent 61%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
#chatbot .bg-blue-600 {
|
||||||
|
background: linear-gradient(90deg, #22c55e 0%, #4ade80 100%) !important;
|
||||||
|
}
|
||||||
|
#chatbot h3 {
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
#chatbot button#closeChat {
|
||||||
|
color: #fff;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
#chatbot button#closeChat:hover {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
#chatbot input[type="text"] {
|
||||||
|
border: 1.5px solid #22c55e;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
#chatbot button#sendMessage {
|
||||||
|
background: linear-gradient(90deg, #22c55e 0%, #4ade80 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
#chatbot button#sendMessage:hover {
|
||||||
|
background: linear-gradient(90deg, #4ade80 0%, #22c55e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typing indicator animation */
|
||||||
|
.typing-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #d1fae5 100%);
|
||||||
|
border: 1.5px solid #22c55e;
|
||||||
|
border-radius: 1.2em;
|
||||||
|
border-bottom-left-radius: 0.3em;
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
max-width: 80%;
|
||||||
|
box-shadow: 0 1px 4px 0 #22c55e22;
|
||||||
|
}
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
float: left;
|
||||||
|
margin: 0 1px;
|
||||||
|
background-color: #22c55e;
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: typing 1.4s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
||||||
|
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
||||||
|
.typing-indicator span:nth-child(3) { animation-delay: 0s; }
|
||||||
|
@@keyframes typing {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.typing-text {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #22c55e;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 768px) {
|
||||||
|
#chatbot.mobile-fullscreen {
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
transform: none !important;
|
||||||
|
transition: transform 0.3s ease-in-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatbot.mobile-fullscreen.scale-0 {
|
||||||
|
transform: translateY(100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatbot.mobile-fullscreen.scale-100 {
|
||||||
|
transform: translateY(0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatbot.mobile-fullscreen .chat-content.mobile-chat-content {
|
||||||
|
height: calc(100vh - 140px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatbot.mobile-fullscreen .rounded-t-lg,
|
||||||
|
#chatbot.mobile-fullscreen .rounded-b-lg {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const flipElement = document.querySelector('.flip-animate');
|
||||||
|
|
||||||
|
if (flipElement) {
|
||||||
|
setInterval(function() {
|
||||||
|
flipElement.classList.toggle('flipped');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const chatButton = document.getElementById("chatButton");
|
||||||
|
const chatbot = document.getElementById("chatbot");
|
||||||
|
const closeChat = document.getElementById("closeChat");
|
||||||
|
const sendMessage = document.getElementById("sendMessage");
|
||||||
|
const userInput = document.getElementById("userInput");
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
const SESSION_DURATION = 30 * 60 * 1000;
|
||||||
|
const STORAGE_KEY = 'ecobot_chat_session';
|
||||||
|
|
||||||
|
// Load chat history from localStorage
|
||||||
|
function loadChatHistory() {
|
||||||
|
try {
|
||||||
|
const sessionData = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (sessionData) {
|
||||||
|
const data = JSON.parse(sessionData);
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
// Check if session is still valid
|
||||||
|
if (now - data.timestamp < SESSION_DURATION) {
|
||||||
|
const chatContent = document.querySelector(".chat-content");
|
||||||
|
// Clear default welcome message
|
||||||
|
chatContent.innerHTML = '';
|
||||||
|
// Load saved messages
|
||||||
|
data.messages.forEach(msg => {
|
||||||
|
const bubble = document.createElement("div");
|
||||||
|
bubble.className = `chat-bubble ${msg.type}`;
|
||||||
|
bubble.style.cssFloat = msg.type === 'user' ? 'right' : 'left';
|
||||||
|
bubble.style.clear = 'both';
|
||||||
|
bubble.innerHTML = msg.content;
|
||||||
|
chatContent.appendChild(bubble);
|
||||||
|
});
|
||||||
|
chatContent.scrollTop = chatContent.scrollHeight;
|
||||||
|
return true; // Session restored
|
||||||
|
} else {
|
||||||
|
// Session expired, clear storage
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading chat history:', error);
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
return false; // New session
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save message to localStorage
|
||||||
|
function saveMessage(type, content) {
|
||||||
|
try {
|
||||||
|
let sessionData = localStorage.getItem(STORAGE_KEY);
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (sessionData) {
|
||||||
|
data = JSON.parse(sessionData);
|
||||||
|
} else {
|
||||||
|
data = {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
messages: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
data.messages.push({ type, content });
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize chat session
|
||||||
|
const sessionRestored = loadChatHistory();
|
||||||
|
if (!sessionRestored) {
|
||||||
|
// New session - show welcome message and save it
|
||||||
|
const welcomeMsg = `
|
||||||
|
<p>Halo! Selamat datang di layanan informasi Dinas Lingkungan Hidup DKI Jakarta! 👋</p>
|
||||||
|
<p>Saya di sini untuk membantu Anda. Ada yang ingin ditanyakan seputar layanan kami?</p>
|
||||||
|
`;
|
||||||
|
saveMessage('bot', welcomeMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatButton.addEventListener("click", () => {
|
||||||
|
chatbot.classList.remove('scale-0');
|
||||||
|
chatbot.classList.add('scale-100');
|
||||||
|
});
|
||||||
|
|
||||||
|
closeChat.addEventListener("click", () => {
|
||||||
|
chatbot.classList.remove('scale-100');
|
||||||
|
chatbot.classList.add('scale-0');
|
||||||
|
});
|
||||||
|
|
||||||
|
sendMessage.addEventListener("click", sendChatMessage);
|
||||||
|
userInput.addEventListener("keydown", function(e) {
|
||||||
|
if (e.key === "Enter") sendChatMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendChatMessage() {
|
||||||
|
const chatContent = document.querySelector(".chat-content");
|
||||||
|
const message = userInput.value.trim();
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
// User bubble (kanan)
|
||||||
|
const userBubble = document.createElement("div");
|
||||||
|
userBubble.className = "chat-bubble user";
|
||||||
|
userBubble.style.cssFloat = "right";
|
||||||
|
userBubble.style.clear = "both";
|
||||||
|
userBubble.textContent = message;
|
||||||
|
chatContent.appendChild(userBubble);
|
||||||
|
chatContent.scrollTop = chatContent.scrollHeight;
|
||||||
|
|
||||||
|
// Save user message
|
||||||
|
saveMessage('user', message);
|
||||||
|
|
||||||
|
userInput.value = '';
|
||||||
|
|
||||||
|
// Show typing indicator
|
||||||
|
const typingIndicator = document.createElement("div");
|
||||||
|
typingIndicator.className = "typing-indicator";
|
||||||
|
typingIndicator.innerHTML = `
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
`;
|
||||||
|
chatContent.appendChild(typingIndicator);
|
||||||
|
chatContent.scrollTop = chatContent.scrollHeight;
|
||||||
|
|
||||||
|
fetch('/api/apiproxy/chatbot', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().catch(() => response.text());
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Remove typing indicator
|
||||||
|
typingIndicator.remove();
|
||||||
|
|
||||||
|
let reply = "";
|
||||||
|
if (data && typeof data === "object" && data.code === 404 && data.message && data.message.includes("webhook")) {
|
||||||
|
reply = "Bot sedang offline. Silakan klik 'Execute workflow' di n8n, lalu coba lagi.";
|
||||||
|
} else if (typeof data === "string") {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
reply = parsed.output || parsed.reply || parsed.message || parsed.text || "";
|
||||||
|
} catch {
|
||||||
|
reply = data;
|
||||||
|
}
|
||||||
|
} else if (typeof data === "object" && data !== null) {
|
||||||
|
reply = data.output || data.reply || data.message || data.text || "";
|
||||||
|
}
|
||||||
|
if (!reply) reply = "Maaf, saya tidak mengerti.";
|
||||||
|
|
||||||
|
// Format reply: convert **text** to bold and handle line breaks
|
||||||
|
reply = reply.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>').replace(/\n/g, "<br>");
|
||||||
|
|
||||||
|
// Bot bubble (kiri)
|
||||||
|
const botBubble = document.createElement("div");
|
||||||
|
botBubble.className = "chat-bubble bot";
|
||||||
|
botBubble.style.cssFloat = "left";
|
||||||
|
botBubble.style.clear = "both";
|
||||||
|
botBubble.innerHTML = reply;
|
||||||
|
chatContent.appendChild(botBubble);
|
||||||
|
chatContent.scrollTop = chatContent.scrollHeight;
|
||||||
|
|
||||||
|
// Save bot message
|
||||||
|
saveMessage('bot', reply);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Remove typing indicator
|
||||||
|
typingIndicator.remove();
|
||||||
|
|
||||||
|
const errorMsg = "Maaf, terjadi kesalahan koneksi.";
|
||||||
|
const botBubble = document.createElement("div");
|
||||||
|
botBubble.className = "chat-bubble bot";
|
||||||
|
botBubble.style.cssFloat = "left";
|
||||||
|
botBubble.style.clear = "both";
|
||||||
|
botBubble.textContent = errorMsg;
|
||||||
|
chatContent.appendChild(botBubble);
|
||||||
|
chatContent.scrollTop = chatContent.scrollHeight;
|
||||||
|
|
||||||
|
// Save error message
|
||||||
|
saveMessage('bot', errorMsg);
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Configuration untuk visitor stats
|
||||||
|
const CACHE_DURATION = 30 * 60 * 1000; // 30 menit cache
|
||||||
|
const RETRY_ATTEMPTS = 3;
|
||||||
|
const RETRY_DELAY = 2000; // 2 detik
|
||||||
|
const UPDATE_INTERVAL = 60 * 60 * 1000; // 1 jam
|
||||||
|
const VISITOR_CACHE_KEY = 'visitor_stats_cache'; // Ubah nama untuk menghindari konflik
|
||||||
|
|
||||||
|
// Function to get cached data
|
||||||
|
function getCachedData() {
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem(VISITOR_CACHE_KEY);
|
||||||
|
if (cached) {
|
||||||
|
const data = JSON.parse(cached);
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
if (now - data.timestamp < CACHE_DURATION) {
|
||||||
|
return data.stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading cache:', error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to save data to cache
|
||||||
|
function saveToCache(stats) {
|
||||||
|
try {
|
||||||
|
const cacheData = {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
stats: stats
|
||||||
|
};
|
||||||
|
localStorage.setItem(VISITOR_CACHE_KEY, JSON.stringify(cacheData));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving to cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to clear cache
|
||||||
|
function clearCache() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(VISITOR_CACHE_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep function for retry delay
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch visitor data with retry logic
|
||||||
|
async function fetchVisitorStats(attempt = 1) {
|
||||||
|
try {
|
||||||
|
console.log(`Fetching visitor stats (attempt ${attempt}/${RETRY_ATTEMPTS})...`);
|
||||||
|
|
||||||
|
const response = await fetch('/api/Visitor/visitor-stats', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Visitor stats fetched successfully:', data);
|
||||||
|
|
||||||
|
// Save to cache
|
||||||
|
saveToCache(data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Attempt ${attempt} failed:`, error);
|
||||||
|
|
||||||
|
if (attempt < RETRY_ATTEMPTS) {
|
||||||
|
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
||||||
|
await sleep(RETRY_DELAY);
|
||||||
|
return fetchVisitorStats(attempt + 1);
|
||||||
|
} else {
|
||||||
|
console.error('All retry attempts failed');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update visitor count display
|
||||||
|
function updateVisitorCount() {
|
||||||
|
const dailyElement = document.getElementById('dailyVisitors');
|
||||||
|
const monthlyElement = document.getElementById('monthlyVisitors');
|
||||||
|
|
||||||
|
if (!dailyElement || !monthlyElement) {
|
||||||
|
console.warn('Visitor count elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cachedData = getCachedData();
|
||||||
|
if (cachedData) {
|
||||||
|
console.log('Using cached visitor data:', cachedData);
|
||||||
|
animateNumber(dailyElement, cachedData.dailyVisitorCount || 0);
|
||||||
|
animateNumber(monthlyElement, cachedData.monthlyVisitorCount || 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
dailyElement.textContent = 'Loading...';
|
||||||
|
monthlyElement.textContent = 'Loading...';
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
|
fetchVisitorStats()
|
||||||
|
.then(data => {
|
||||||
|
console.log('API Response:', data);
|
||||||
|
// Animate numbers with actual data
|
||||||
|
animateNumber(dailyElement, data.dailyVisitorCount || 0);
|
||||||
|
animateNumber(monthlyElement, data.monthlyVisitorCount || 0);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching visitor data:', error);
|
||||||
|
|
||||||
|
// Try to use expired cache as fallback
|
||||||
|
try {
|
||||||
|
const expiredCache = localStorage.getItem(VISITOR_CACHE_KEY);
|
||||||
|
if (expiredCache) {
|
||||||
|
const data = JSON.parse(expiredCache);
|
||||||
|
console.log('Using expired cache as fallback:', data.stats);
|
||||||
|
animateNumber(dailyElement, data.stats.dailyVisitorCount || 0);
|
||||||
|
animateNumber(monthlyElement, data.stats.monthlyVisitorCount || 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (cacheError) {
|
||||||
|
console.error('Error using expired cache:', cacheError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback
|
||||||
|
dailyElement.textContent = 'N/A';
|
||||||
|
monthlyElement.textContent = 'N/A';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to animate numbers
|
||||||
|
function animateNumber(element, target) {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
let current = 0;
|
||||||
|
const increment = Math.ceil(target / 50);
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
current += increment;
|
||||||
|
if (current >= target) {
|
||||||
|
element.textContent = target.toLocaleString();
|
||||||
|
clearInterval(timer);
|
||||||
|
} else {
|
||||||
|
element.textContent = Math.floor(current).toLocaleString();
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to force refresh (bypass cache)
|
||||||
|
function forceRefresh() {
|
||||||
|
console.log('Force refreshing visitor stats...');
|
||||||
|
clearCache();
|
||||||
|
updateVisitorCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the function to update visitor count
|
||||||
|
updateVisitorCount();
|
||||||
|
|
||||||
|
// Auto-refresh every hour
|
||||||
|
setInterval(function() {
|
||||||
|
console.log('Auto-refreshing visitor stats...');
|
||||||
|
updateVisitorCount();
|
||||||
|
}, UPDATE_INTERVAL);
|
||||||
|
|
||||||
|
// Make refresh function available globally
|
||||||
|
window.refreshVisitorStats = forceRefresh;
|
||||||
|
|
||||||
|
// Clear cache on page unload if needed
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
// Optionally clear cache on page unload
|
||||||
|
// clearCache();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -620,63 +620,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Configuration untuk visitor stats
|
const UPDATE_INTERVAL = 5 * 60 * 1000; // 5 menit
|
||||||
const CACHE_DURATION = 30 * 60 * 1000; // 30 menit cache
|
|
||||||
const RETRY_ATTEMPTS = 3;
|
|
||||||
const RETRY_DELAY = 2000; // 2 detik
|
|
||||||
const UPDATE_INTERVAL = 60 * 60 * 1000; // 1 jam
|
|
||||||
const VISITOR_CACHE_KEY = 'visitor_stats_cache'; // Ubah nama untuk menghindari konflik
|
|
||||||
|
|
||||||
// Function to get cached data
|
|
||||||
function getCachedData() {
|
|
||||||
try {
|
|
||||||
const cached = localStorage.getItem(VISITOR_CACHE_KEY);
|
|
||||||
if (cached) {
|
|
||||||
const data = JSON.parse(cached);
|
|
||||||
const now = new Date().getTime();
|
|
||||||
|
|
||||||
if (now - data.timestamp < CACHE_DURATION) {
|
|
||||||
return data.stats;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading cache:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to save data to cache
|
|
||||||
function saveToCache(stats) {
|
|
||||||
try {
|
|
||||||
const cacheData = {
|
|
||||||
timestamp: new Date().getTime(),
|
|
||||||
stats: stats
|
|
||||||
};
|
|
||||||
localStorage.setItem(VISITOR_CACHE_KEY, JSON.stringify(cacheData));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving to cache:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to clear cache
|
|
||||||
function clearCache() {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem(VISITOR_CACHE_KEY);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error clearing cache:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep function for retry delay
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to fetch visitor data with retry logic
|
|
||||||
async function fetchVisitorStats(attempt = 1) {
|
async function fetchVisitorStats(attempt = 1) {
|
||||||
|
const RETRY_ATTEMPTS = 3;
|
||||||
|
const RETRY_DELAY = 2000;
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching visitor stats (attempt ${attempt}/${RETRY_ATTEMPTS})...`);
|
|
||||||
|
|
||||||
const response = await fetch('/api/Visitor/visitor-stats', {
|
const response = await fetch('/api/Visitor/visitor-stats', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -684,93 +633,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
'Cache-Control': 'no-cache'
|
'Cache-Control': 'no-cache'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
return await response.json();
|
||||||
const data = await response.json();
|
|
||||||
console.log('Visitor stats fetched successfully:', data);
|
|
||||||
|
|
||||||
// Save to cache
|
|
||||||
saveToCache(data);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Attempt ${attempt} failed:`, error);
|
|
||||||
|
|
||||||
if (attempt < RETRY_ATTEMPTS) {
|
if (attempt < RETRY_ATTEMPTS) {
|
||||||
console.log(`Retrying in ${RETRY_DELAY}ms...`);
|
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||||
await sleep(RETRY_DELAY);
|
|
||||||
return fetchVisitorStats(attempt + 1);
|
return fetchVisitorStats(attempt + 1);
|
||||||
} else {
|
} else {
|
||||||
console.error('All retry attempts failed');
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update visitor count display
|
|
||||||
function updateVisitorCount() {
|
function updateVisitorCount() {
|
||||||
const dailyElement = document.getElementById('dailyVisitors');
|
const dailyElement = document.getElementById('dailyVisitors');
|
||||||
const monthlyElement = document.getElementById('monthlyVisitors');
|
const monthlyElement = document.getElementById('monthlyVisitors');
|
||||||
|
if (!dailyElement || !monthlyElement) return;
|
||||||
|
|
||||||
if (!dailyElement || !monthlyElement) {
|
|
||||||
console.warn('Visitor count elements not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
const cachedData = getCachedData();
|
|
||||||
if (cachedData) {
|
|
||||||
console.log('Using cached visitor data:', cachedData);
|
|
||||||
animateNumber(dailyElement, cachedData.dailyVisitorCount || 0);
|
|
||||||
animateNumber(monthlyElement, cachedData.monthlyVisitorCount || 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
dailyElement.textContent = 'Loading...';
|
dailyElement.textContent = 'Loading...';
|
||||||
monthlyElement.textContent = 'Loading...';
|
monthlyElement.textContent = 'Loading...';
|
||||||
|
|
||||||
// Fetch from API
|
|
||||||
fetchVisitorStats()
|
fetchVisitorStats()
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log('API Response:', data);
|
|
||||||
// Animate numbers with actual data
|
|
||||||
animateNumber(dailyElement, data.dailyVisitorCount || 0);
|
animateNumber(dailyElement, data.dailyVisitorCount || 0);
|
||||||
animateNumber(monthlyElement, data.monthlyVisitorCount || 0);
|
animateNumber(monthlyElement, data.monthlyVisitorCount || 0);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(() => {
|
||||||
console.error('Error fetching visitor data:', error);
|
|
||||||
|
|
||||||
// Try to use expired cache as fallback
|
|
||||||
try {
|
|
||||||
const expiredCache = localStorage.getItem(VISITOR_CACHE_KEY);
|
|
||||||
if (expiredCache) {
|
|
||||||
const data = JSON.parse(expiredCache);
|
|
||||||
console.log('Using expired cache as fallback:', data.stats);
|
|
||||||
animateNumber(dailyElement, data.stats.dailyVisitorCount || 0);
|
|
||||||
animateNumber(monthlyElement, data.stats.monthlyVisitorCount || 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (cacheError) {
|
|
||||||
console.error('Error using expired cache:', cacheError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final fallback
|
|
||||||
dailyElement.textContent = 'N/A';
|
dailyElement.textContent = 'N/A';
|
||||||
monthlyElement.textContent = 'N/A';
|
monthlyElement.textContent = 'N/A';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to animate numbers
|
|
||||||
function animateNumber(element, target) {
|
function animateNumber(element, target) {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
let current = 0;
|
let current = 0;
|
||||||
const increment = Math.ceil(target / 50);
|
const increment = Math.ceil(target / 50);
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
current += increment;
|
current += increment;
|
||||||
if (current >= target) {
|
if (current >= target) {
|
||||||
|
@ -782,29 +681,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}, 30);
|
}, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to force refresh (bypass cache)
|
|
||||||
function forceRefresh() {
|
function forceRefresh() {
|
||||||
console.log('Force refreshing visitor stats...');
|
|
||||||
clearCache();
|
|
||||||
updateVisitorCount();
|
updateVisitorCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the function to update visitor count
|
|
||||||
updateVisitorCount();
|
updateVisitorCount();
|
||||||
|
setInterval(updateVisitorCount, UPDATE_INTERVAL);
|
||||||
// Auto-refresh every hour
|
|
||||||
setInterval(function() {
|
|
||||||
console.log('Auto-refreshing visitor stats...');
|
|
||||||
updateVisitorCount();
|
|
||||||
}, UPDATE_INTERVAL);
|
|
||||||
|
|
||||||
// Make refresh function available globally
|
|
||||||
window.refreshVisitorStats = forceRefresh;
|
window.refreshVisitorStats = forceRefresh;
|
||||||
|
|
||||||
// Clear cache on page unload if needed
|
|
||||||
window.addEventListener('beforeunload', function() {
|
|
||||||
// Optionally clear cache on page unload
|
|
||||||
// clearCache();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -1,115 +0,0 @@
|
||||||
<footer class="bg-gradient-to-br from-emerald-50 via-green-50 to-teal-50 border-t-4 border-green-500 relative overflow-hidden">
|
|
||||||
<!-- Background Pattern -->
|
|
||||||
<div class="absolute inset-0 opacity-5">
|
|
||||||
<div class="absolute top-0 left-0 w-64 h-64 bg-green-600 rounded-full -translate-x-32 -translate-y-32"></div>
|
|
||||||
<div class="absolute bottom-0 right-0 w-96 h-96 bg-emerald-600 rounded-full translate-x-48 translate-y-48"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Footer Section -->
|
|
||||||
<div class="py-16 relative z-10">
|
|
||||||
<div class="container max-w-6xl mx-auto px-6">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-4 gap-8">
|
|
||||||
<!-- Logo & Description -->
|
|
||||||
<div class="lg:col-span-1">
|
|
||||||
<div class="flex items-center space-x-3 mb-6">
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-0 bg-green-500 rounded-xl blur-sm opacity-30"></div>
|
|
||||||
<img src="https://lingkunganhidup.jakarta.go.id/images/weblink/logo-dlh.png" class="h-14 relative z-10 drop-shadow-lg" alt="DLH Logo" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-800 mb-1">DLH</h3>
|
|
||||||
<p class="text-sm text-green-700 font-medium">Provinsi DKI Jakarta</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-700 text-sm leading-relaxed font-light"></p>
|
|
||||||
Dinas Lingkungan Hidup Provinsi DKI Jakarta berkomitmen menjaga kelestarian lingkungan untuk Jakarta yang berkelanjutan.
|
|
||||||
</p>
|
|
||||||
<div class="flex space-x-3 mt-4">
|
|
||||||
<a href="#" class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center hover:bg-green-600 transition-all duration-300 transform hover:scale-110 shadow-lg">
|
|
||||||
<i class="text-white w-4 h-4" data-lucide="facebook"></i>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center hover:bg-green-600 transition-all duration-300 transform hover:scale-110 shadow-lg">
|
|
||||||
<i class="text-white w-4 h-4" data-lucide="instagram"></i>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center hover:bg-green-600 transition-all duration-300 transform hover:scale-110 shadow-lg">
|
|
||||||
<i class="text-white w-4 h-4" data-lucide="youtube"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tentang Kami -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-bold text-gray-800 mb-6 text-lg relative">
|
|
||||||
<span class="border-b-2 border-green-500 pb-1">Tentang Kami</span>
|
|
||||||
</h4>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Visi Misi</a></li>
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Struktur Organisasi</a></li>
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Sejarah</a></li>
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Profil Pimpinan</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Layanan -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-bold text-gray-800 mb-6 text-lg relative">
|
|
||||||
<span class="border-b-2 border-green-500 pb-1">Layanan</span>
|
|
||||||
</h4>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Penjemputan e-Waste</a></li>
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Bulky Waste</a></li>
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Uji Sampel Laboratorium</a></li>
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">AMDAL</a></li>
|
|
||||||
<li><a href="#" class="text-gray-700 hover:text-green-600 text-sm transition-all duration-300 hover:translate-x-2 inline-block font-medium">Whistleblowing System</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Kontak -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-bold text-gray-800 mb-6 text-lg relative">
|
|
||||||
<span class="border-b-2 border-green-500 pb-1">Kontak Kami</span>
|
|
||||||
</h4>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-start space-x-3 group">
|
|
||||||
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center group-hover:bg-green-500 transition-colors duration-300">
|
|
||||||
<i class="text-green-600 w-4 h-4 group-hover:text-white transition-colors duration-300" data-lucide="map-pin"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm text-gray-700 font-medium leading-relaxed">
|
|
||||||
Jl. Mandala V No.67, RT.1/RW.2, Cililitan, Kramatjati, Jakarta Timur 13640
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 group">
|
|
||||||
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center group-hover:bg-green-500 transition-colors duration-300">
|
|
||||||
<i class="text-green-600 w-4 h-4 group-hover:text-white transition-colors duration-300" data-lucide="phone"></i>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-700 font-medium">(021) 8092744</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 group">
|
|
||||||
<div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center group-hover:bg-green-500 transition-colors duration-300">
|
|
||||||
<i class="text-green-600 w-4 h-4 group-hover:text-white transition-colors duration-300" data-lucide="mail"></i>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-700 font-medium">dinaslh@jakarta.go.id</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom Section -->
|
|
||||||
<div class="bg-gradient-to-r from-green-600 to-emerald-600 py-6 relative z-10">
|
|
||||||
<div class="container max-w-6xl mx-auto px-6">
|
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center space-y-3 md:space-y-0">
|
|
||||||
<p class="text-sm text-green-50 font-medium">
|
|
||||||
© @DateTime.Now.Year Dinas Lingkungan Hidup Provinsi DKI Jakarta. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<div class="flex space-x-6">
|
|
||||||
<a href="#" class="text-sm text-green-100 hover:text-white transition-colors duration-300 font-medium relative after:content-[''] after:absolute after:w-0 after:h-0.5 after:bg-white after:left-0 after:-bottom-1 after:transition-all after:duration-300 hover:after:w-full">Privacy Policy</a>
|
|
||||||
<a href="#" class="text-sm text-green-100 hover:text-white transition-colors duration-300 font-medium relative after:content-[''] after:absolute after:w-0 after:h-0.5 after:bg-white after:left-0 after:-bottom-1 after:transition-all after:duration-300 hover:after:w-full">Terms of Service</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
|
@ -20,3 +20,5 @@
|
||||||
|
|
||||||
gtag('config', 'G-NKXHJJD10C');
|
gtag('config', 'G-NKXHJJD10C');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script defer data-domain="lingkunganhidup.jakarta.go.id" src="https://analytics.dinaslhdki.id/"></script>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -13,7 +13,7 @@ using System.Reflection;
|
||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("dlh-net")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("dlh-net")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1f9274fa8d9bba628b315e2c1516139c2b0f1666")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7b878f2f23b792ac106111be553e41fc4bf39e51")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("dlh-net")]
|
[assembly: System.Reflection.AssemblyProductAttribute("dlh-net")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("dlh-net")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("dlh-net")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
3841a76ecb907eaeb4876c34967ea65cb42111efeb7f84a411bb1383b38cec60
|
5ccc866dddad0e75443f0589ae496dcea3ef1157b4517bdfadf678dbcad813e6
|
||||||
|
|
|
@ -260,12 +260,12 @@ build_metadata.AdditionalFiles.CssScope =
|
||||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXFBhcnRpYWxzXF9CcmVhZGN1bWIuY3NodG1s
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXFBhcnRpYWxzXF9CcmVhZGN1bWIuY3NodG1s
|
||||||
build_metadata.AdditionalFiles.CssScope =
|
build_metadata.AdditionalFiles.CssScope =
|
||||||
|
|
||||||
[C:/laragon/www/dlh-net/Views/Shared/Partials/_Footer.cshtml]
|
[C:/laragon/www/dlh-net/Views/Shared/Partials/_Footer copy.cshtml]
|
||||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXFBhcnRpYWxzXF9Gb290ZXIuY3NodG1s
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXFBhcnRpYWxzXF9Gb290ZXIgY29weS5jc2h0bWw=
|
||||||
build_metadata.AdditionalFiles.CssScope =
|
build_metadata.AdditionalFiles.CssScope =
|
||||||
|
|
||||||
[C:/laragon/www/dlh-net/Views/Shared/Partials/_Footercop.cshtml]
|
[C:/laragon/www/dlh-net/Views/Shared/Partials/_Footer.cshtml]
|
||||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXFBhcnRpYWxzXF9Gb290ZXJjb3AuY3NodG1s
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXFBhcnRpYWxzXF9Gb290ZXIuY3NodG1s
|
||||||
build_metadata.AdditionalFiles.CssScope =
|
build_metadata.AdditionalFiles.CssScope =
|
||||||
|
|
||||||
[C:/laragon/www/dlh-net/Views/Shared/Partials/_Head.cshtml]
|
[C:/laragon/www/dlh-net/Views/Shared/Partials/_Head.cshtml]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
424597b6428ebf52ab678364ba68fd6f6f11a85bb2028dc0e66b52a40da2eb4a
|
58b1ec55083e25742368ea33e0016dcdda11ea9f4cbe5ab196cb98e1a96c7e00
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -84,7 +84,7 @@
|
||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.301/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12381,7 +12381,7 @@
|
||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.301/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "nQEXpaq6JQQ=",
|
"dgSpecHash": "rPELhfgyyC4=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "C:\\laragon\\www\\dlh-net\\dlh-net.csproj",
|
"projectFilePath": "C:\\laragon\\www\\dlh-net\\dlh-net.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
|
|
Loading…
Reference in New Issue