Merge branch 'stg' into nico/26-feb-26/fix-seed-2

This commit is contained in:
2026-03-12 15:21:05 +08:00
committed by GitHub
31 changed files with 3614 additions and 3755 deletions

19
.env.example Normal file
View File

@@ -0,0 +1,19 @@
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dashboard_desa?schema=public"
# Authentication
BETTER_AUTH_SECRET="your-secret-key-here-min-32-characters"
ADMIN_EMAIL="admin@example.com"
ADMIN_PASSWORD="admin123"
# GitHub OAuth (Optional)
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Application
PORT=3000
NODE_ENV=development
LOG_LEVEL=info
# Public URL
VITE_PUBLIC_URL="http://localhost:3000"

106
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: Publish Docker to GHCR
on:
workflow_dispatch:
inputs:
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- prod
- stg
tag:
description: "Image tag (e.g. 1.0.0)"
required: true
default: "1.0.0"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h
- name: Checkout branch ${{ github.event.inputs.stack_env }}
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.stack_env }}
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
path: .ci
sparse-checkout: .github/workflows/script
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
type=raw,value=${{ github.event.inputs.stack_env }}-latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
no-cache: true
- name: Notify success
if: success()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
- name: Notify failure
if: failure()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"

60
.github/workflows/re-pull.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Re-Pull Docker
on:
workflow_dispatch:
inputs:
stack_name:
description: "stack name"
required: true
type: string
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- stg
- prod
jobs:
publish:
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
sparse-checkout: .github/workflows/script
- name: Deploy ke Portainer
run: bash ./.github/workflows/script/re-pull.sh
env:
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
- name: Notify success
if: success()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
- name: Notify failure
if: failure()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"

26
.github/workflows/script/notify.sh vendored Normal file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
if [ "$NOTIFY_STATUS" = "success" ]; then
ICON="✅"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
else
ICON="❌"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
fi
if [ -n "$NOTIFY_DETAIL" ]; then
TEXT="${TEXT}
${NOTIFY_DETAIL}"
fi
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg chat_id "$TELEGRAM_CHAT_ID" \
--arg text "$TEXT" \
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"

93
.github/workflows/script/re-pull.sh vendored Normal file
View File

@@ -0,0 +1,93 @@
#!/bin/bash
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
: "${STACK_NAME:?STACK_NAME tidak di-set}"
echo "🔐 Autentikasi ke Portainer..."
TOKEN=$(curl -s -X POST https://${PORTAINER_URL}/api/auth \
-H "Content-Type: application/json" \
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
| jq -r .jwt)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
exit 1
fi
echo "🔍 Mencari stack: $STACK_NAME..."
STACK=$(curl -s -X GET https://${PORTAINER_URL}/api/stacks \
-H "Authorization: Bearer ${TOKEN}" \
| jq ".[] | select(.Name == \"$STACK_NAME\")")
if [ -z "$STACK" ]; then
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
echo " Pastikan nama stack sudah benar."
exit 1
fi
STACK_ID=$(echo "$STACK" | jq -r .Id)
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
ENV=$(echo "$STACK" | jq '.Env // []')
echo "📄 Mengambil compose file..."
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
-H "Authorization: Bearer ${TOKEN}" \
| jq -r .StackFileContent)
PAYLOAD=$(jq -n \
--arg content "$STACK_FILE" \
--argjson env "$ENV" \
'{stackFileContent: $content, env: $env, pullImage: true}')
echo "🚀 Redeploying $STACK_NAME (pull latest image)..."
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
if [ "$HTTP_STATUS" != "200" ]; then
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
cat /tmp/portainer_response.json | jq .
exit 1
fi
echo "⏳ Menunggu container running..."
MAX_RETRY=15
COUNT=0
while [ $COUNT -lt $MAX_RETRY ]; do
sleep 5
COUNT=$((COUNT + 1))
CONTAINERS=$(curl -s -X GET \
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
-H "Authorization: Bearer ${TOKEN}")
TOTAL=$(echo "$CONTAINERS" | jq 'length')
RUNNING=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "running")] | length')
FAILED=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not))] | length')
echo "🔄 [${COUNT}/${MAX_RETRY}] Running: ${RUNNING} | Failed: ${FAILED} | Total: ${TOTAL}"
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status)"'
if [ "$FAILED" -gt "0" ]; then
echo ""
echo "❌ Ada container yang crash!"
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not)) | " → \(.Names[0]) | \(.Status)"'
exit 1
fi
if [ "$RUNNING" -gt "0" ]; then
echo ""
echo "✅ Stack $STACK_NAME berhasil di-redeploy dan running!"
exit 0
fi
done
echo ""
echo "❌ Timeout! Stack tidak kunjung running setelah $((MAX_RETRY * 5)) detik."
exit 1

4
.gitignore vendored
View File

@@ -16,6 +16,7 @@ _.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
# Only .env.example is allowed to be committed
.env
.env.development.local
.env.test.local
@@ -33,6 +34,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
# Dashboard-MD
Dashboard-MD
# Playwright artifacts
test-results/
playwright-report/

62
Dockerfile Normal file
View File

@@ -0,0 +1,62 @@
# Stage 1: Build
FROM oven/bun:1.3 AS build
# Install build dependencies for native modules
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /app
# Copy package files
COPY package.json bun.lock* ./
# Install dependencies
RUN bun install --frozen-lockfile
# Copy the rest of the application code
COPY . .
# Use .env.example as default env for build
RUN cp .env.example .env
# Generate Prisma client
RUN bun x prisma generate
# Generate API types
RUN bun run gen:api
# Build the application frontend using our custom build script
RUN bun run build
# Stage 2: Runtime
FROM oven/bun:1.3-slim AS runtime
# Set environment variables
ENV NODE_ENV=production
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /app
# Copy necessary files from build stage
COPY --from=build /app/package.json ./
COPY --from=build /app/tsconfig.json ./
COPY --from=build /app/dist ./dist
COPY --from=build /app/generated ./generated
COPY --from=build /app/src ./src
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/prisma ./prisma
# Expose the port
EXPOSE 3000
# Start the application
CMD ["bun", "start"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
public/SDGS-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/SDGS-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
public/SDGS-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
public/SDGS-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

58
scripts/build.ts Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bun
/**
* Build script for production
* 1. Build CSS with PostCSS/Tailwind
* 2. Bundle JS with Bun (without CSS)
* 3. Replace CSS reference in HTML
*/
import { $ } from "bun";
import fs from "node:fs";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import autoprefixer from "autoprefixer";
console.log("🔨 Starting production build...");
// Ensure dist directory exists
if (!fs.existsSync("./dist")) {
fs.mkdirSync("./dist", { recursive: true });
}
// Step 1: Build CSS with PostCSS
console.log("🎨 Building CSS...");
const cssInput = fs.readFileSync("./src/index.css", "utf-8");
const cssResult = await postcss([tailwindcss(), autoprefixer()]).process(
cssInput,
{
from: "./src/index.css",
to: "./dist/index.css",
},
);
fs.writeFileSync("./dist/index.css", cssResult.css);
console.log("✅ CSS built successfully!");
// Step 2: Build JS with Bun (build HTML too, we'll fix CSS link later)
console.log("📦 Bundling JavaScript...");
await $`bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='"production"' --env='VITE_*'`;
// Step 3: Copy public assets
console.log("📁 Copying public assets...");
if (fs.existsSync("./public")) {
await $`cp -r public/* dist/ 2>/dev/null || true`;
}
// Step 4: Ensure HTML references the correct CSS
// Bun build might have renamed the CSS, we want to use our own index.css
console.log("🔧 Fixing HTML CSS reference...");
const htmlPath = "./dist/index.html";
if (fs.existsSync(htmlPath)) {
let html = fs.readFileSync(htmlPath, "utf-8");
// Replace any bundled CSS reference with our index.css
html = html.replace(/href="[^"]*\.css"/g, 'href="/index.css"');
fs.writeFileSync(htmlPath, html);
}
console.log("✅ Build completed successfully!");

View File

@@ -1,23 +1,13 @@
import {
Badge,
Button,
Card,
Grid,
GridCol,
Group,
Select,
Stack,
Table,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconBuildingStore,
IconCategory,
IconCurrency,
IconUsers,
IconTrendingUp,
IconTrendingDown,
IconChevronDown,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import { useState } from "react";
const BumdesPage = () => {
@@ -25,69 +15,98 @@ const BumdesPage = () => {
const dark = colorScheme === "dark";
const [timeFilter, setTimeFilter] = useState<string>("bulan");
const [categoryFilter, setCategoryFilter] = useState<string>("semua");
// Sample data for KPI cards
// KPI Data
const kpiData = [
{
title: "UMKM Aktif",
value: 45,
icon: <IconUsers size={24} />,
color: "darmasaba-blue",
value: "45",
subtitle: "Beroperasi",
icon: IconUsers,
},
{
title: "UMKM Terdaftar",
value: 68,
icon: <IconBuildingStore size={24} />,
color: "darmasaba-success",
value: "68",
subtitle: "Total terdaftar",
icon: IconBuildingStore,
},
{
title: "Omzet",
value: "Rp 48.000.000",
icon: <IconCurrency size={24} />,
color: "darmasaba-warning",
value: "48 JT",
subtitle: "Bulan ini",
icon: IconCurrency,
},
{
title: "Kategori UMKM",
value: 34,
icon: <IconCategory size={24} />,
color: "darmasaba-danger",
value: "34",
subtitle: "Jenis produk",
icon: IconCategory,
},
];
// Sample data for top products
// Mini stats data
const miniStats = [
{
title: "Total Penjualan",
value: "Rp 30.900.000",
subtitle: "+18% vs bulan lalu",
isPositive: true,
},
{
title: "Produk Aktif",
value: "7",
subtitle: "Kategori produk",
},
{
title: "Total Transaksi",
value: "500",
subtitle: "Transaksi bulan ini",
},
];
// Top 3 products data
const topProducts = [
{
rank: 1,
name: "Beras Premium Organik",
umkmOwner: "Warung Pak Joko",
growth: "+12%",
umkmOwner: "Kelompok Tani Subak",
sales: "Rp 8.500.000",
volume: "650 Kg Terjual",
growth: "+15%",
},
{
rank: 2,
name: "Keripik Singkong",
umkmOwner: "Ibu Sari Snack",
sales: "Rp 4.200.000",
volume: "320 Kg Terjual",
growth: "+8%",
},
{
rank: 3,
name: "Madu Alami",
umkmOwner: "Peternakan Lebah",
sales: "Rp 3.750.000",
volume: "150 Liter Terjual",
growth: "+5%",
},
];
// Sample data for product sales
// Product sales data
const productSales = [
{
produk: "Beras Premium Organik",
umkm: "Kelompok Tani Subak",
penjualanBulanIni: "Rp 8.500.000",
bulanLalu: "Rp 8.500.000",
trend: 10,
bulanLalu: "Rp 7.400.000",
trend: 15,
volume: "650 Kg",
stok: "850 Kg",
},
{
produk: "Keripik Singkong",
umkm: "Ibu Sari Snack",
penjualanBulanIni: "Rp 4.200.000",
bulanLalu: "Rp 3.800.000",
trend: 10,
@@ -96,6 +115,7 @@ const BumdesPage = () => {
},
{
produk: "Madu Alami",
umkm: "Peternakan Lebah",
penjualanBulanIni: "Rp 3.750.000",
bulanLalu: "Rp 4.100.000",
trend: -8,
@@ -104,6 +124,7 @@ const BumdesPage = () => {
},
{
produk: "Kecap Tradisional",
umkm: "Bu Darmi",
penjualanBulanIni: "Rp 2.800.000",
bulanLalu: "Rp 2.500.000",
trend: 12,
@@ -112,277 +133,456 @@ const BumdesPage = () => {
},
];
const cardStyle = {
backgroundColor: dark ? "#1E293B" : "white",
border: `1px solid ${dark ? "#1E293B" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<Stack gap="lg">
{/* KPI Cards */}
<Grid gutter="md">
{kpiData.map((kpi, index) => (
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{typeof kpi.value === "number"
? kpi.value.toLocaleString()
: kpi.value}
</Text>
</Stack>
<Badge variant="light" color={kpi.color} p={8} radius="md">
{kpi.icon}
</Badge>
</Group>
</Card>
</GridCol>
))}
</Grid>
{/* Update Penjualan Produk Header */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
<div
className="min-h-screen"
style={{
backgroundColor: dark ? "#0F172A" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
<div
className="max-w-7xl mx-auto"
style={{
maxWidth: "80rem",
marginLeft: "auto",
marginRight: "auto",
}}
>
<Group justify="space-between" align="center" px="md" py="xs">
<Title order={3} c={dark ? "dark.0" : "black"}>
Update Penjualan Produk
</Title>
<Group>
<Button
variant={timeFilter === "minggu" ? "filled" : "light"}
onClick={() => setTimeFilter("minggu")}
color="darmasaba-blue"
{/* Row 1: Top 4 Metrics Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{kpiData.map((kpi, index) => (
<div
key={index}
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
Minggu ini
</Button>
<Button
variant={timeFilter === "bulan" ? "filled" : "light"}
onClick={() => setTimeFilter("bulan")}
color="darmasaba-blue"
>
Bulan ini
</Button>
</Group>
</Group>
</Card>
<Grid gutter="md">
{/* Produk Unggulan (Left Column) */}
<GridCol span={{ base: 12, lg: 4 }}>
<Stack gap="md">
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Stack gap="md">
<Group justify="space-between">
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Total Penjualan
</Text>
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
Rp 28.500.000
</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Produk Aktif
</Text>
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
124 Produk
</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Total Transaksi
</Text>
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
1.240 Transaksi
</Text>
</Group>
</Stack>
</Card>
{/* Top 3 Produk Terlaris */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
Top 3 Produk Terlaris
</Title>
<Stack gap="sm">
{topProducts.map((product) => (
<Group
key={product.rank}
justify="space-between"
align="center"
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
<Group gap="sm">
<Badge
variant="filled"
color={
product.rank === 1
? "gold"
: product.rank === 2
? "gray"
: "bronze"
}
radius="xl"
size="lg"
>
{product.rank}
</Badge>
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>
{product.name}
</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{product.umkmOwner}
</Text>
</Stack>
</Group>
<Badge
variant="light"
color={product.growth.startsWith("+") ? "green" : "red"}
>
{product.growth}
</Badge>
</Group>
))}
</Stack>
</Card>
</Stack>
</GridCol>
{kpi.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{kpi.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1F3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))}
</div>
{/* Detail Penjualan Produk (Right Column) */}
<GridCol span={{ base: 12, lg: 8 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
{/* Row 2: Sales Update Header */}
<div
className="rounded-xl shadow-sm mb-6 overflow-hidden"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
marginBottom: "1.5rem",
}}
>
<div
className="px-6 py-4 flex items-center justify-between"
style={{ backgroundColor: "#1F3A5F" }}
>
<Group justify="space-between" mb="md">
<Title order={4} c={dark ? "dark.0" : "black"}>
Detail Penjualan Produk
</Title>
<Select
placeholder="Filter kategori"
data={[
{ value: "semua", label: "Semua Kategori" },
{ value: "makanan", label: "Makanan" },
{ value: "minuman", label: "Minuman" },
{ value: "kerajinan", label: "Kerajinan" },
]}
defaultValue="semua"
w={200}
/>
</Group>
<h3 className="text-lg font-semibold text-white">
Update Penjualan Produk
</h3>
<div className="flex items-center gap-2">
<button
onClick={() => setTimeFilter("minggu")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
timeFilter === "minggu"
? "bg-white text-[#1F3A5F]"
: "bg-white/20 text-white hover:bg-white/30"
}`}
>
Minggu ini
</button>
<button
onClick={() => setTimeFilter("bulan")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
timeFilter === "bulan"
? "bg-white text-[#1F3A5F]"
: "bg-white/20 text-white hover:bg-white/30"
}`}
>
Bulan ini
</button>
</div>
</div>
</div>
<Table striped highlightOnHover withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>
<Text c={dark ? "white" : "dimmed"}>Produk</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "white" : "dimmed"}>
Penjualan Bulan Ini
</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "white" : "dimmed"}>Bulan Lalu</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "white" : "dimmed"}>Trend</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "white" : "dimmed"}>Volume</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "white" : "dimmed"}>Stok</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "white" : "dimmed"}>Aksi</Text>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{productSales.map((product, index) => (
<Table.Tr key={index}>
<Table.Td>
<Text fw={500} c={dark ? "dark.0" : "black"}>
{product.produk}
</Text>
</Table.Td>
<Table.Td>
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
{product.penjualanBulanIni}
</Text>
</Table.Td>
<Table.Td>
<Text fz={"sm"} c={dark ? "white" : "dimmed"}>
{product.bulanLalu}
</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
<Text c={product.trend >= 0 ? "green" : "red"}>
{product.trend >= 0 ? "↑" : "↓"}{" "}
{Math.abs(product.trend)}%
</Text>
</Group>
</Table.Td>
<Table.Td>
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
{product.volume}
</Text>
</Table.Td>
<Table.Td>
<Badge
variant="light"
color={
parseInt(product.stok) > 200 ? "green" : "yellow"
{/* Row 3: Main Content Grid */}
<div
className="grid grid-cols-1 lg:grid-cols-10 gap-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(10, 1fr)",
gap: "1.5rem",
}}
>
{/* Left Column (30%) */}
<div className="lg:col-span-3 space-y-6">
{/* Produk Unggulan Section */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Produk Unggulan
</h3>
{/* Mini Stats Cards */}
<div className="space-y-4 mb-6">
{miniStats.map((stat, index) => (
<div
key={index}
className="p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<p
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{stat.title}
</p>
<p
className="text-xl font-bold"
style={textStyle}
>
{stat.value}
</p>
{stat.subtitle && (
<p
className="text-xs mt-1"
style={
stat.isPositive
? { color: "#22C55E" }
: subtitleStyle
}
>
{product.stok}
</Badge>
</Table.Td>
<Table.Td>
<Button
variant="subtle"
size="compact-sm"
color="darmasaba-blue"
>
Detail
</Button>
</Table.Td>
</Table.Tr>
{stat.subtitle}
</p>
)}
</div>
))}
</Table.Tbody>
</Table>
</Card>
</GridCol>
</Grid>
</Stack>
</div>
{/* Top 3 Products */}
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
Top 3 Produk Terlaris
</h4>
<div className="space-y-4">
{topProducts.map((product) => (
<div
key={product.rank}
className="flex items-start gap-3 p-3 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-white font-bold text-sm flex-shrink-0"
style={{
backgroundColor:
product.rank === 1
? "#FFD700"
: product.rank === 2
? "#C0C0C0"
: "#CD7F32",
}}
>
#{product.rank}
</div>
<div className="flex-1">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.name}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{product.umkmOwner}
</p>
<div className="flex items-center justify-between mt-2">
<p className="text-xs font-medium" style={{ color: "#22C55E" }}>
{product.sales}
</p>
<p
className="text-xs"
style={{ color: "#22C55E" }}
>
{product.growth}
</p>
</div>
<p className="text-xs mt-1" style={subtitleStyle}>
{product.volume}
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Column (70%) */}
<div className="lg:col-span-7">
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<div className="flex items-center justify-between mb-6">
<h3
className="text-lg font-semibold"
style={textStyle}
>
Detail Penjualan Produk
</h3>
<div className="relative">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="appearance-none px-4 py-2 pr-8 rounded-lg text-sm font-medium border-0 focus:ring-2 focus:ring-[#1F3A5F] cursor-pointer"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
color: dark ? "white" : "#1F2937",
}}
>
<option value="semua">Semua Kategori</option>
<option value="makanan">Makanan</option>
<option value="minuman">Minuman</option>
<option value="kerajinan">Kerajinan</option>
</select>
<IconChevronDown
size={16}
className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
/>
</div>
</div>
{/* Data Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr
style={{
borderBottom: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Produk
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Penjualan Bulan Ini
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Bulan Lalu
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Trend
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Volume
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Stok
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Aksi
</th>
</tr>
</thead>
<tbody>
{productSales.map((product, index) => (
<tr
key={index}
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<td className="py-4 px-4">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.produk}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{product.umkm}
</p>
</td>
<td className="py-4 px-4">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.penjualanBulanIni}
</p>
</td>
<td className="py-4 px-4">
<p
className="text-sm"
style={subtitleStyle}
>
{product.bulanLalu}
</p>
</td>
<td className="py-4 px-4">
<div
className="flex items-center gap-1 text-sm font-medium"
style={{
color: product.trend >= 0 ? "#22C55E" : "#EF4444",
}}
>
{product.trend >= 0 ? (
<IconTrendingUp size={16} />
) : (
<IconTrendingDown size={16} />
)}
{product.trend >= 0 ? "+" : ""}
{product.trend}%
</div>
</td>
<td className="py-4 px-4">
<p
className="text-sm"
style={textStyle}
>
{product.volume}
</p>
</td>
<td className="py-4 px-4">
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"
style={{
backgroundColor: parseInt(product.stok) > 200 ? "#DCFCE7" : "#FEE2E2",
color: parseInt(product.stok) > 200 ? "#166534" : "#991B1B",
}}
>
{product.stok}
</span>
</td>
<td className="py-4 px-4">
<button
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
style={{
backgroundColor: "#1F3A5F",
color: "white",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#2d4a6f")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "#1F3A5F")
}
>
Detail
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,4 +1,5 @@
import {
Briefcase,
Calendar,
CheckCircle,
FileText,
@@ -27,7 +28,9 @@ import {
Card, // Added for icon containers
Grid,
Group,
Image,
Progress,
SimpleGrid,
Stack,
Text,
ThemeIcon,
@@ -67,9 +70,16 @@ const eventData = [
];
const apbdesData = [
{ name: "Belanja", value: 70, color: "blue" },
{ name: "Pendapatan", value: 90, color: "green" },
{ name: "Pembangunan", value: 50, color: "orange" },
{ name: "Belanja", value: 390, label: "390M" },
{ name: "Pendapatan", value: 470, label: "470M" },
{ name: "Pembiayaan", value: 290, label: "290M" },
];
const sdgsData = [
{ label: "Desa Berenergi Bersih Dan Terbarukan", value: 99.64, image: "/SDGS-7.png" },
{ label: "Desa Damai Berkeadilan", value: 78.65, image: "/SDGS-16.png" },
{ label: "Desa Sehat Dan Sejahtera", value: 77.37, image: "/SDGS-3.png" },
{ label: "Desa Tanpa Kemiskinan", value: 52.62, image: "/SDGS-1.png" }
];
export function DashboardContent() {
@@ -109,7 +119,7 @@ export function DashboardContent() {
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-blue"}
color={dark ? "gray" : "darmasaba-navy"}
>
<FileText style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
@@ -143,7 +153,7 @@ export function DashboardContent() {
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-blue"}
color={dark ? "gray" : "darmasaba-navy"}
>
<MessageCircle style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
@@ -180,7 +190,7 @@ export function DashboardContent() {
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-blue"}
color={dark ? "gray" : "darmasaba-navy"}
>
<CheckCircle style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
@@ -214,7 +224,7 @@ export function DashboardContent() {
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-blue"}
color={dark ? "gray" : "darmasaba-navy"}
>
<Users style={{ width: "70%", height: "70%" }} />
</ThemeIcon>
@@ -282,7 +292,7 @@ export function DashboardContent() {
<Tooltip />
<Bar
dataKey="value"
fill="var(--mantine-color-blue-filled)"
fill="#1E3A5F"
radius={[4, 4, 0, 0]}
/>
</BarChart>
@@ -375,46 +385,7 @@ export function DashboardContent() {
<Group gap="xs" mb="lg">
<Box>
{/* Original SVG icon */}
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="3"
y="3"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
<rect
x="3"
y="14"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
<rect
x="14"
y="3"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
<rect
x="14"
y="14"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
</svg>
<Briefcase color="#1E3A5F" />
</Box>
<Title order={4}>Divisi Teraktif</Title>
</Group>
@@ -433,7 +404,7 @@ export function DashboardContent() {
value={(divisi.value / 37) * 100}
size="sm"
radius="xl"
color="blue"
color="#1E3A5F"
/>
</Box>
))}
@@ -459,7 +430,7 @@ export function DashboardContent() {
<Box
key={index}
style={{
borderLeft: "4px solid var(--mantine-color-blue-filled)",
borderLeft: "4px solid #1E3A5F",
paddingLeft: 12,
}}
>
@@ -493,18 +464,53 @@ export function DashboardContent() {
{data.name}
</Text>
</Grid.Col>
<Grid.Col span={9}>
<Grid.Col span={7}>
<Progress
value={data.value}
value={(data.value / 470) * 100}
size="lg"
radius="xl"
color={data.color}
color="#1E3A5F"
/>
</Grid.Col>
<Grid.Col span={2}>
<Text size="sm" fw={600} ta="right">
{data.label}
</Text>
</Grid.Col>
</Grid>
))}
</Stack>
</Card>
{/* SDGS Desa */}
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Title order={4} mb="lg">
SDGS Desa
</Title>
<SimpleGrid cols={{ base: 2, md: 5 }}>
{sdgsData.map((data, index) => (
<Card key={index} withBorder bg={dark ? "#141D34" : "white"} p="md">
<Group gap="sm" align="center">
<Image src={data.image} width={40} height={40} />
<Box>
<Text size="sm" ta={"center"} fw={500} lineClamp={2}>
{data.label}
</Text>
<Text size="sm" ta={"center"} fw={600} c="darmasaba-blue">
{data.value}
</Text>
</Box>
</Group>
</Card>
))}
</SimpleGrid>
</Card>
</Stack>
);
}

View File

@@ -1,410 +1,543 @@
import { BarChart, PieChart } from "@mantine/charts";
import {
Box,
Card,
Grid,
Group,
Stack,
Table,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconArrowDown,
IconArrowUp,
IconBabyCarriage,
IconSkull,
IconUsers,
IconHome,
IconExclamationCircle,
} from "@tabler/icons-react";
import React from "react";
// Sample Data
const kpiData = [
{
id: 1,
title: "Total Penduduk",
value: "5.634",
sub: "Aktif terdaftar",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon penduduk"
>
<title>Total Penduduk</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>
),
},
{
id: 2,
title: "Kepala Keluarga",
value: "1.354",
sub: "Total KK",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kepala keluarga"
>
<title>Kepala Keluarga</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
),
},
{
id: 3,
title: "Kelahiran",
value: "23",
sub: "Tahun ini",
icon: (
<IconBabyCarriage
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kelahiran"
/>
),
},
{
id: 4,
title: "Kemiskinan",
value: "324",
delta: "-10% dari tahun lalu",
deltaType: "positive",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kemiskinan"
>
<title>Kemiskinan</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
),
},
];
const ageDistributionData = [
{ ageRange: "17-25", total: 850 },
{ ageRange: "26-35", total: 1200 },
{ ageRange: "36-45", total: 1100 },
{ ageRange: "46-55", total: 950 },
{ ageRange: "56-65", total: 750 },
{ ageRange: "65+", total: 484 },
];
const jobDistributionData = [
{ job: "Sipil", total: 1200 },
{ job: "Guru", total: 850 },
{ job: "Petani", total: 950 },
{ job: "Pedagang", total: 750 },
{ job: "Wiraswasta", total: 984 },
];
const religionData = [
{ religion: "Hindu", total: 4234, color: "red" },
{ religion: "Islam", total: 856, color: "blue" },
{ religion: "Kristen", total: 412, color: "green" },
{ religion: "Buddha", total: 202, color: "yellow" },
];
const banjarData = [
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
];
const dynamicStats = [
{
title: "Kelahiran",
value: "23",
icon: <IconBabyCarriage size={16} />,
color: "green",
},
{
title: "Kematian",
value: "12",
icon: <IconSkull size={16} />,
color: "red",
},
{
title: "Pindah Masuk",
value: "45",
icon: <IconArrowDown size={16} />,
color: "blue",
},
{
title: "Pindah Keluar",
value: "32",
icon: <IconArrowUp size={16} />,
color: "orange",
},
];
import { useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const DemografiPekerjaan = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Total Penduduk",
value: "5.634",
subtitle: "Aktif terdaftar",
icon: IconUsers,
},
{
id: 2,
title: "Kepala Keluarga",
value: "1.354",
subtitle: "Total KK",
icon: IconHome,
},
{
id: 3,
title: "Kelahiran",
value: "23",
subtitle: "Tahun ini",
icon: IconBabyCarriage,
},
{
id: 4,
title: "Kemiskinan",
value: "324",
subtitle: "-10% dari tahun lalu",
icon: IconExclamationCircle,
},
];
// Age distribution data
const ageDistributionData = [
{ ageRange: "17-25", total: 850 },
{ ageRange: "26-35", total: 1200 },
{ ageRange: "36-45", total: 1100 },
{ ageRange: "46-55", total: 950 },
{ ageRange: "56-65", total: 750 },
{ ageRange: "65+", total: 484 },
];
// Job distribution data
const jobDistributionData = [
{ job: "Sipil", total: 1200 },
{ job: "Guru", total: 850 },
{ job: "Petani", total: 950 },
{ job: "Pedagang", total: 750 },
{ job: "Wiraswasta", total: 984 },
];
// Religion data
const religionData = [
{ religion: "Hindu", total: 4234, color: "#EF4444" },
{ religion: "Islam", total: 856, color: "#3B82F6" },
{ religion: "Kristen", total: 412, color: "#10B981" },
{ religion: "Buddha", total: 202, color: "#F59E0B" },
];
// Banjar data
const banjarData = [
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
];
// Dynamic stats
const dynamicStats = [
{
title: "Kelahiran",
value: "23",
icon: IconBabyCarriage,
color: "#10B981",
},
{
title: "Kematian",
value: "12",
icon: IconSkull,
color: "#EF4444",
},
{
title: "Pindah Masuk",
value: "45",
icon: IconArrowDown,
color: "#3B82F6",
},
{
title: "Pindah Keluar",
value: "32",
icon: IconArrowUp,
color: "#F59E0B",
},
];
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD", "#DBEAFE"];
const cardStyle = {
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<Box className="space-y-6">
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
{kpi.title}
</Text>
{React.cloneElement(kpi.icon, {
className: "h-6 w-6",
color: dark
? "var(--mantine-color-dark-3)"
: "var(--mantine-color-dimmed)",
})}
</Group>
<Title order={3} fw={700} c={dark ? "dark.0" : "black"} mt="xs">
{kpi.value}
</Title>
{kpi.delta && (
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: dark
? "dark.3"
: "dimmed"
}
mt={4}
>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c={dark ? "dark.3" : "dimmed"} mt={2}>
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</Grid>
{/* Charts Section */}
<Grid gutter="lg">
{/* Grafik Pengelompokan Umur */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Grafik Pengelompokan Umur
</Title>
<BarChart
h={300}
data={ageDistributionData}
dataKey="ageRange"
series={[{ name: "total", color: "darmasaba-navy" }]}
withLegend
/>
</Card>
</Grid.Col>
{/* Demografi Pekerjaan */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Demografi Pekerjaan
</Title>
<BarChart
h={300}
data={jobDistributionData}
dataKey="job"
series={[{ name: "total", color: "darmasaba-navy" }]}
withLegend
/>
</Card>
</Grid.Col>
</Grid>
{/* Agama & Data per Banjar */}
<Grid gutter="lg">
{/* Distribusi Agama */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Distribusi Agama
</Title>
<PieChart
h={300}
data={religionData.map((item) => ({
name: item.religion,
value: item.total,
color: item.color,
}))}
withLabels
withLabelsLine
labelsPosition="outside"
labelsType="percent"
/>
</Card>
</Grid.Col>
{/* Data per Banjar */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
Data per Banjar
</Title>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>Banjar</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>Penduduk</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>KK</Text>
</Table.Th>
<Table.Th>
<Text c={dark ? "dark.0" : "black"}>Miskin</Text>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{banjarData.map((item, index) => (
<Table.Tr key={`${item.banjar}-${index}`}>
<Table.Td>
<Text c={dark ? "dark.0" : "black"}>{item.banjar}</Text>
</Table.Td>
<Table.Td>
<Text c={dark ? "dark.0" : "black"}>
{item.population.toLocaleString()}
</Text>
</Table.Td>
<Table.Td>
<Text c={dark ? "dark.0" : "black"}>
{item.kk.toLocaleString()}
</Text>
</Table.Td>
<Table.Td>
<Text c={dark ? "red.4" : "red"}>
{item.poor.toLocaleString()}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Card>
</Grid.Col>
</Grid>
{/* Statistik Dinamika Penduduk */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
<div
className="min-h-screen"
style={{
backgroundColor: dark ? "#10192D" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
<div
className="max-w-7xl mx-auto"
style={{
maxWidth: "80rem",
marginLeft: "auto",
marginRight: "auto",
}}
>
{/* Row 1: 4 Statistic Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
Statistik Dinamika Penduduk
</Title>
<Grid gutter="md">
{dynamicStats.map((stat, index) => (
<Grid.Col
key={`${stat.title}-${index}`}
span={{ base: 12, md: 3 }}
>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
{kpiData.map((kpi) => (
<div
key={kpi.id}
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{kpi.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{kpi.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1E3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))}
</div>
{/* Row 2: 2 Chart Cards */}
<div
className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{/* Age Distribution Bar Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Grafik Pengelompokan Umur
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={ageDistributionData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="ageRange"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="total" radius={[4, 4, 0, 0]}>
{ageDistributionData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Job Distribution Bar Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Demografi Pekerjaan
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={jobDistributionData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="job"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="total" radius={[4, 4, 0, 0]}>
{jobDistributionData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Row 3: 3 Insight Cards */}
<div
className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{/* Religion Distribution Pie Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Distribusi Agama
</h3>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={religionData}
cx="50%"
cy="50%"
outerRadius={80}
dataKey="total"
nameKey="religion"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : 0}%`
}
>
<Group justify="space-between" align="center">
<Box>
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
{stat.title}
</Text>
<Title order={4} fw={700} c={stat.color}>
{stat.value}
</Title>
</Box>
<Box c={stat.color}>{stat.icon}</Box>
</Group>
</Card>
</Grid.Col>
{religionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
{/* Population per Banjar Table */}
<div
className="rounded-xl shadow-sm p-6 lg:col-span-2"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Data per Banjar
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
}}
>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Banjar
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Penduduk
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
KK
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Miskin
</th>
</tr>
</thead>
<tbody>
{banjarData.map((item, index) => (
<tr
key={index}
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#F3F4F6"}`,
}}
>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.banjar}
</td>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.population.toLocaleString()}
</td>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.kk.toLocaleString()}
</td>
<td
className="py-3 px-4 text-sm"
style={{ color: "#EF4444" }}
>
{item.poor.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Population Dynamics Stats */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Statistik Dinamika Penduduk
</h3>
<div
className="grid grid-cols-1 md:grid-cols-4 gap-4"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1rem",
}}
>
{dynamicStats.map((stat, index) => (
<div
key={index}
className="p-4 rounded-lg"
style={{
backgroundColor: dark ? "#1F2937" : "#F9FAFB",
borderRadius: "8px",
padding: "1rem",
}}
>
<div className="flex items-center justify-between">
<div>
<p
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{stat.title}
</p>
<p
className="text-2xl font-bold"
style={{ color: stat.color }}
>
{stat.value}
</p>
</div>
<div
className="w-10 h-10 rounded-full flex items-center justify-center"
style={{
backgroundColor: `${stat.color}20`,
color: stat.color,
}}
>
<stat.icon size={20} />
</div>
</div>
</div>
))}
</Grid>
</Card>
</Stack>
</Box>
</div>
</div>
</div>
</div>
);
};

View File

@@ -142,7 +142,7 @@ const HelpPage = () => {
};
return (
<Container size="lg" py="xl">
<Container size="lg" py="lg">
{/* Statistics Section */}
<SimpleGrid cols={3} spacing="lg" mb="xl">
{stats.map((stat, index) => (

View File

@@ -1,283 +1,328 @@
import { BarChart } from "@mantine/charts";
import {
Badge,
Box,
Button,
Card,
Grid,
Group,
Progress,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import React from "react";
// Sample Data
const kpiData = [
{
id: 1,
title: "Interaksi Hari Ini",
value: "61",
delta: "+15% dari kemarin",
deltaType: "positive",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H16.5m-13.5 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"
/>
</svg>
),
},
{
id: 2,
title: "Jawaban Otomatis",
value: "87%",
sub: "53 dari 61 interaksi",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
/>
</svg>
),
},
{
id: 3,
title: "Belum Ditindak",
value: "8",
sub: "Perlu respon manual",
deltaType: "negative",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
),
},
{
id: 4,
title: "Waktu Respon",
value: "2.3 sec",
sub: "Rata-rata",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
),
},
];
const chartData = [
{ day: "Sen", total: 100 },
{ day: "Sel", total: 120 },
{ day: "Rab", total: 90 },
{ day: "Kam", total: 150 },
{ day: "Jum", total: 110 },
{ day: "Sab", total: 80 },
{ day: "Min", total: 130 },
];
const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 },
{ topic: "Jadwal Posyandu", count: 64 },
{ topic: "Pengaduan jalan rusak", count: 52 },
{ topic: "Info program bansos", count: 48 },
];
const busyHours = [
{ period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 },
{ period: "Sore (1620)", percentage: 20 },
{ period: "Malam (2008)", percentage: 10 },
];
IconAlertTriangle,
IconClock,
IconMessageChatbot,
IconSparkles,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const JennaAnalytic = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Interaksi Hari Ini",
value: "61",
subtitle: "+15% dari kemarin",
icon: IconMessageChatbot,
},
{
id: 2,
title: "Jawaban Otomatis",
value: "87%",
subtitle: "53 dari 61 interaksi",
icon: IconSparkles,
},
{
id: 3,
title: "Belum Ditindak",
value: "8",
subtitle: "Perlu respon manual",
icon: IconAlertTriangle,
},
{
id: 4,
title: "Waktu Respon",
value: "2.3s",
subtitle: "Rata-rata",
icon: IconClock,
},
];
// Weekly chatbot interaction data
const weeklyData = [
{ day: "Sen", interactions: 100 },
{ day: "Sel", interactions: 120 },
{ day: "Rab", interactions: 90 },
{ day: "Kam", interactions: 150 },
{ day: "Jum", interactions: 110 },
{ day: "Sab", interactions: 80 },
{ day: "Min", interactions: 130 },
];
// Top topics data
const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 },
{ topic: "Jadwal Posyandu", count: 64 },
{ topic: "Pengaduan jalan rusak", count: 52 },
{ topic: "Info program bansos", count: 48 },
];
// Busy hour distribution
const busyHours = [
{ period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 },
{ period: "Sore (1620)", percentage: 20 },
{ period: "Malam (2008)", percentage: 10 },
];
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD"];
const cardStyle = {
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<Box className="space-y-6">
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
{kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c="dimmed">
{kpi.title}
</Text>
{React.cloneElement(kpi.icon, {
className: "h-6 w-6", // Keeping classes for now, can be replaced by Mantine Icon component if available or styled with sx prop
color: "var(--mantine-color-dimmed)", // Set color via prop
})}
</Group>
<Title order={3} fw={700} mt="xs">
{kpi.value}
</Title>
{kpi.delta && (
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: "dimmed"
}
mt={4}
>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c="dimmed" mt={2}>
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</Grid>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
<div
className="min-h-screen"
style={{
backgroundColor: dark ? "#10192D" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
<div
className="max-w-7xl mx-auto"
style={{
maxWidth: "80rem",
marginLeft: "auto",
marginRight: "auto",
}}
>
{/* Row 1: 4 Statistic Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
<Title order={3} fw={500} mb="md">
Interaksi Chatbot
</Title>
<BarChart
h={300}
data={chartData}
dataKey="day"
series={[{ name: "total", color: "blue" }]}
withLegend
/>
</Card>
{/* Charts and Lists Section */}
<Grid gutter="lg">
{/* Grafik Interaksi Chatbot (now Bar Chart) */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
{kpiData.map((kpi) => (
<div
key={kpi.id}
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<Title order={3} fw={500} mb="md">
Jam Tersibuk
</Title>
<Stack gap="sm">
{busyHours.map((item, index) => (
<Box key={index}>
<Text size="sm">{item.period}</Text>
<Group align="center">
<Progress value={item.percentage} flex={1} />
<Text size="sm" fw={500}>
{item.percentage}%
</Text>
</Group>
</Box>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{kpi.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{kpi.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1E3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))}
</div>
{/* Row 2: Full Width Weekly Bar Chart */}
<div
className="rounded-xl shadow-sm p-6 mb-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
marginBottom: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Interaksi Chatbot Mingguan
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={weeklyData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="day"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="interactions" radius={[4, 4, 0, 0]}>
{weeklyData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Stack>
</Card>
</Grid.Col>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Topik Pertanyaan Terbanyak & Jam Tersibuk */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Stack gap="lg">
{/* Topik Pertanyaan Terbanyak */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Title order={3} fw={500} mb="md">
Topik Pertanyaan Terbanyak
</Title>
<Stack gap="xs">
{topTopics.map((item, index) => (
<Group
key={index}
justify="space-between"
align="center"
p="xs"
{/* Row 3: Two Insight Cards */}
<div
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
}}
>
{/* Left: Frequently Asked Topics */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Topik Pertanyaan Terbanyak
</h3>
<div className="space-y-3">
{topTopics.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-3"
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
}}
>
<span
className="text-sm font-medium"
style={textStyle}
>
{item.topic}
</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-darmasaba-blue-100 text-darmasaba-blue-800">
{item.count}x
</span>
</div>
))}
</div>
</div>
{/* Right: Busy Hour Distribution */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Distribusi Jam Tersibuk
</h3>
<div className="space-y-4">
{busyHours.map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-1">
<span
className="text-sm font-medium"
style={textStyle}
>
<Text size="sm" fw={500}>
{item.topic}
</Text>
<Badge variant="light" color="gray">
{item.count}x
</Badge>
</Group>
))}
</Stack>
</Card>
{/* Jam Tersibuk */}
</Stack>
</Grid.Col>
</Grid>
</Stack>
</Box>
{item.period}
</span>
<span
className="text-sm font-semibold"
style={textStyle}
>
{item.percentage}%
</span>
</div>
<div
className="w-full rounded-full h-2"
style={{ backgroundColor: dark ? "#2d3748" : "#E5E7EB" }}
>
<div
className="h-2 rounded-full transition-all"
style={{
width: `${item.percentage}%`,
backgroundColor: COLORS[index % COLORS.length],
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default JennaAnalytic;

View File

@@ -118,13 +118,6 @@ const KeamananPage = () => {
return (
<Stack gap="lg">
{/* Page Header */}
<Group justify="space-between" align="center">
<Title order={2} c={dark ? "dark.0" : "black"}>
Keamanan Lingkungan Desa
</Title>
</Group>
{/* KPI Cards */}
<Grid gutter="md">
{kpiData.map((kpi, index) => (

View File

@@ -1,356 +1,567 @@
import { BarChart } from "@mantine/charts";
import {
Badge,
Box,
Button,
Card,
Grid,
Group,
Progress,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconCurrency,
IconTrendingDown,
IconTrendingUp,
IconCheck,
IconClock,
} from "@tabler/icons-react";
import React from "react";
// Sample Data
const kpiData = [
{
id: 1,
title: "Total APBDes",
value: "Rp 5.2M",
sub: "Tahun 2025",
icon: <IconCurrency className="h-6 w-6 text-muted-foreground" />,
},
{
id: 2,
title: "Realisasi",
value: "68%",
sub: "Rp 3.5M dari 5.2M",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
/>
</svg>
),
},
{
id: 3,
title: "Pemasukan",
value: "Rp 580jt",
sub: "Bulan ini",
delta: "+8%",
deltaType: "positive",
icon: <IconTrendingUp className="h-6 w-6 text-muted-foreground" />,
},
{
id: 4,
title: "Pengeluaran",
value: "Rp 520jt",
sub: "Bulan ini",
icon: <IconTrendingDown className="h-6 w-6 text-muted-foreground" />,
},
];
const incomeExpenseData = [
{ month: "Apr", income: 450, expense: 380 },
{ month: "Mei", income: 520, expense: 420 },
{ month: "Jun", income: 480, expense: 500 },
{ month: "Jul", income: 580, expense: 450 },
{ month: "Agu", income: 550, expense: 520 },
{ month: "Sep", income: 600, expense: 480 },
{ month: "Okt", income: 580, expense: 520 },
];
const allocationData = [
{ sector: "Pembangunan", amount: 1200 },
{ sector: "Kesehatan", amount: 800 },
{ sector: "Pendidikan", amount: 650 },
{ sector: "Sosial", amount: 550 },
{ sector: "Kebudayaan", amount: 400 },
{ sector: "Teknologi", amount: 300 },
];
const assistanceFundData = [
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
];
const apbdReport = {
income: [
{ category: "Dana Desa", amount: 1800 },
{ category: "Alokasi Dana Desa", amount: 480 },
{ category: "Bagi Hasil Pajak & Retribusi", amount: 300 },
{ category: "Pendapatan Asli Desa", amount: 200 },
{ category: "Hibah Bantuan", amount: 300 },
],
expenses: [
{ category: "Penyelenggaraan Pemerintah", amount: 425 },
{ category: "Pembangunan Desa", amount: 850 },
{ category: "Pembinaan Kemasyarakatan", amount: 320 },
{ category: "Pemberdayaan Masyarakat", amount: 380 },
{ category: "Penanggulangan Bencana", amount: 180 },
],
totalIncome: 3080,
totalExpenses: 2155,
};
import { useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const KeuanganAnggaran = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Total APBDes",
value: "Rp 5.2M",
subtitle: "Tahun 2025",
icon: IconCurrency,
},
{
id: 2,
title: "Realisasi",
value: "68%",
subtitle: "Rp 3.5M dari 5.2M",
icon: IconCheck,
},
{
id: 3,
title: "Pemasukan",
value: "Rp 580jt",
subtitle: "Bulan ini",
delta: "+8%",
icon: IconTrendingUp,
},
{
id: 4,
title: "Pengeluaran",
value: "Rp 520jt",
subtitle: "Bulan ini",
icon: IconTrendingDown,
},
];
// Income vs Expense data
const incomeExpenseData = [
{ month: "Apr", income: 450, expense: 380 },
{ month: "Mei", income: 520, expense: 420 },
{ month: "Jun", income: 480, expense: 500 },
{ month: "Jul", income: 580, expense: 450 },
{ month: "Agu", income: 550, expense: 520 },
{ month: "Sep", income: 600, expense: 480 },
{ month: "Okt", income: 580, expense: 520 },
];
// Allocation data
const allocationData = [
{ sector: "Pembangunan", amount: 1200 },
{ sector: "Kesehatan", amount: 800 },
{ sector: "Pendidikan", amount: 650 },
{ sector: "Sosial", amount: 550 },
{ sector: "Kebudayaan", amount: 400 },
{ sector: "Teknologi", amount: 300 },
];
// Assistance fund data
const assistanceFundData = [
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
];
// APBDes Report data
const apbdReport = {
income: [
{ category: "Dana Desa", amount: 1800 },
{ category: "Alokasi Dana Desa", amount: 480 },
{ category: "Bagi Hasil Pajak & Retribusi", amount: 300 },
{ category: "Pendapatan Asli Desa", amount: 200 },
{ category: "Hibah Bantuan", amount: 300 },
],
expenses: [
{ category: "Penyelenggaraan Pemerintah", amount: 425 },
{ category: "Pembangunan Desa", amount: 850 },
{ category: "Pembinaan Kemasyarakatan", amount: 320 },
{ category: "Pemberdayaan Masyarakat", amount: 380 },
{ category: "Penanggulangan Bencana", amount: 180 },
],
totalIncome: 3080,
totalExpenses: 2155,
};
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD", "#DBEAFE"];
const cardStyle = {
backgroundColor: dark ? "#1E293B" : "white",
border: `1px solid ${dark ? "#1E293B" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return (
<Box>
<Stack gap="xl">
{/* KPI Cards */}
<Grid gutter="lg">
<div
className="min-h-screen"
style={{
backgroundColor: dark ? "#0F172A" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
<div
className="max-w-7xl mx-auto"
style={{
maxWidth: "80rem",
marginLeft: "auto",
marginRight: "auto",
}}
>
{/* Row 1: 4 Summary Metrics Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c="dimmed">
{kpi.title}
</Text>
{React.cloneElement(kpi.icon, {
className: "h-6 w-6",
color: "var(--mantine-color-dimmed)",
})}
</Group>
<Title order={3} fw={700} mt="xs">
{kpi.value}
</Title>
{kpi.delta && (
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: "dimmed"
}
mt={4}
<div
key={kpi.id}
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{kpi.delta}
</Text>
)}
{kpi.sub && (
<Text size="xs" c="dimmed" mt="auto">
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
{kpi.title}
</h3>
<p
className="text-3xl font-bold mb-1"
style={textStyle}
>
{kpi.value}
</p>
<p
className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
{kpi.delta && (
<p className="text-xs mt-1" style={{ color: "#22C55E" }}>
{kpi.delta}
</p>
)}
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1F3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))}
</Grid>
</div>
{/* Charts Section */}
<Grid gutter="lg">
{/* Grafik Pemasukan vs Pengeluaran */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Pemasukan vs Pengeluaran
</Title>
<BarChart
h={300}
data={incomeExpenseData}
{/* Row 2: Line Chart Section */}
<div
className="rounded-xl shadow-sm p-6 mb-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
marginBottom: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Pemasukan vs Pengeluaran
</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={incomeExpenseData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#E5E7EB"}
/>
<XAxis
dataKey="month"
series={[
{ name: "income", color: "green", label: "Pemasukan" },
{ name: "expense", color: "red", label: "Pengeluaran" },
]}
withLegend
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
</Card>
</Grid.Col>
{/* Alokasi Anggaran Per Sektor */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Alokasi Anggaran Per Sektor
</Title>
<BarChart
h={300}
data={allocationData}
dataKey="sector"
series={[
{ name: "amount", color: "darmasaba-navy", label: "Jumlah" },
]}
withLegend
orientation="horizontal"
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
tickFormatter={(value) => `${value}jt`}
/>
</Card>
</Grid.Col>
</Grid>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Line
type="monotone"
dataKey="income"
stroke="#22C55E"
strokeWidth={3}
dot={{ fill: "#22C55E", strokeWidth: 2, r: 5 }}
activeDot={{ r: 7 }}
name="Pemasukan"
/>
<Line
type="monotone"
dataKey="expense"
stroke="#EF4444"
strokeWidth={3}
dot={{ fill: "#EF4444", strokeWidth: 2, r: 5 }}
activeDot={{ r: 7 }}
name="Pengeluaran"
/>
</LineChart>
</ResponsiveContainer>
<Grid gutter="lg">
{/* Dana Bantuan & Hibah */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
{/* Legend */}
<div className="flex items-center gap-6 mt-4">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: "#22C55E" }}
/>
<span className="text-sm" style={subtitleStyle}>
Pemasukan
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: "#EF4444" }}
/>
<span className="text-sm" style={subtitleStyle}>
Pengeluaran
</span>
</div>
</div>
</div>
{/* Row 3: Analytics Section */}
<div
className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{/* Left: Horizontal Bar Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
<Title order={3} fw={500} mb="md">
Dana Bantuan & Hibah
</Title>
<Stack gap="sm">
{assistanceFundData.map((fund, index) => (
<Group
key={index}
justify="space-between"
align="center"
p="sm"
Alokasi Anggaran Per Sektor
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={allocationData} layout="vertical">
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
stroke={dark ? "#334155" : "#E5E7EB"}
/>
<XAxis
type="number"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
tickFormatter={(value) => `${value}jt`}
/>
<YAxis
dataKey="sector"
type="category"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#374151" }}
width={120}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="amount" radius={[0, 4, 4, 0]}>
{allocationData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Right: Assistance Funds List */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Dana Bantuan dan Hibah
</h3>
<div className="space-y-3">
{assistanceFundData.map((fund, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<div>
<p
className="text-sm font-medium"
style={textStyle}
>
{fund.source}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
Rp {fund.amount.toLocaleString()}jt
</p>
</div>
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"
style={{
border: "1px solid var(--mantine-color-gray-3)",
borderRadius: "var(--mantine-radius-sm)",
backgroundColor: fund.status === "cair" ? "#DCFCE7" : "#FEF3C7",
color: fund.status === "cair" ? "#166534" : "#92400E",
}}
>
<Box>
<Text size="sm" fw={500}>
{fund.source}
</Text>
<Text size="sm" c="dimmed">
Rp {fund.amount.toLocaleString()}jt
</Text>
</Box>
<Badge
variant="light"
color={fund.status === "cair" ? "green" : "yellow"}
>
{fund.status}
</Badge>
</Group>
))}
</Stack>
</Card>
</Grid.Col>
{fund.status === "cair" ? (
<IconCheck size={14} className="mr-1" />
) : (
<IconClock size={14} className="mr-1" />
)}
{fund.status}
</span>
</div>
))}
</div>
</div>
{/* Laporan APBDes */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} fw={500} mb="md">
Laporan APBDes
</Title>
<Box mb="md">
<Title order={4} mb="sm">
Pendapatan
</Title>
<Stack gap="xs">
{apbdReport.income.map((item, index) => (
<Group key={index} justify="space-between">
<Text size="sm">{item.category}</Text>
<Text size="sm" c="green">
Rp {item.amount.toLocaleString()}jt
</Text>
</Group>
))}
<Group justify="space-between" mt="sm">
<Text fw={700}>Total Pendapatan:</Text>
<Text fw={700} c="green">
Rp {apbdReport.totalIncome.toLocaleString()}jt
</Text>
</Group>
</Stack>
</Box>
<Box>
<Title order={4} mb="sm">
Belanja
</Title>
<Stack gap="xs">
{apbdReport.expenses.map((item, index) => (
<Group key={index} justify="space-between">
<Text size="sm">{item.category}</Text>
<Text size="sm" c="red">
Rp {item.amount.toLocaleString()}jt
</Text>
</Group>
))}
<Group justify="space-between" mt="sm">
<Text fw={700}>Total Belanja:</Text>
<Text fw={700} c="red">
Rp {apbdReport.totalExpenses.toLocaleString()}jt
</Text>
</Group>
</Stack>
</Box>
<Box
mt="md"
pt="md"
style={{ borderTop: "1px solid var(--mantine-color-gray-3)" }}
</div>
{/* Row 4: Report Section */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-6"
style={textStyle}
>
Laporan APBDes
</h3>
<div
className="grid grid-cols-1 md:grid-cols-2 gap-8"
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "2rem",
}}
>
{/* Left: Pendapatan */}
<div>
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
<Group justify="space-between">
<Text fw={700}>Saldo:</Text>
<Text
fw={700}
c={
apbdReport.totalIncome > apbdReport.totalExpenses
? "green"
: "red"
}
Pendapatan
</h4>
<div className="space-y-3">
{apbdReport.income.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
Rp{" "}
{(
apbdReport.totalIncome - apbdReport.totalExpenses
).toLocaleString()}
jt
</Text>
</Group>
</Box>
</Card>
</Grid.Col>
</Grid>
</Stack>
</Box>
<span className="text-sm" style={subtitleStyle}>
{item.category}
</span>
<span
className="text-sm font-medium"
style={{ color: "#22C55E" }}
>
Rp {item.amount.toLocaleString()}jt
</span>
</div>
))}
<div
className="flex items-center justify-between py-3 mt-4 pt-4"
style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-base font-bold" style={textStyle}>
Total Pendapatan
</span>
<span
className="text-base font-bold"
style={{ color: "#22C55E" }}
>
Rp {apbdReport.totalIncome.toLocaleString()}jt
</span>
</div>
</div>
</div>
{/* Right: Belanja */}
<div>
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
Belanja
</h4>
<div className="space-y-3">
{apbdReport.expenses.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<span className="text-sm" style={subtitleStyle}>
{item.category}
</span>
<span
className="text-sm font-medium"
style={{ color: "#EF4444" }}
>
Rp {item.amount.toLocaleString()}jt
</span>
</div>
))}
<div
className="flex items-center justify-between py-3 mt-4 pt-4"
style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-base font-bold" style={textStyle}>
Total Belanja
</span>
<span
className="text-base font-bold"
style={{ color: "#EF4444" }}
>
Rp {apbdReport.totalExpenses.toLocaleString()}jt
</span>
</div>
</div>
</div>
</div>
{/* Footer: Balance */}
<div
className="flex items-center justify-between py-4 mt-6 pt-6"
style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-lg font-bold" style={textStyle}>
Saldo
</span>
<span
className="text-lg font-bold"
style={{
color:
apbdReport.totalIncome > apbdReport.totalExpenses
? "#22C55E"
: "#EF4444",
}}
>
Rp{" "}
{(
apbdReport.totalIncome - apbdReport.totalExpenses
).toLocaleString()}
jt
</span>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,21 +1,3 @@
import {
ActionIcon,
Box,
Card,
Divider,
Grid,
GridCol,
Group,
List,
Badge as MantineBadge,
Progress as MantineProgress,
Skeleton,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
Bar,
BarChart,
@@ -28,511 +10,380 @@ import {
XAxis,
YAxis,
} from "recharts";
import { Button } from "@/components/ui/button";
import { useMantineColorScheme } from "@mantine/core";
import { IconMessage } from "@tabler/icons-react";
const KinerjaDivisi = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// Data for division progress chart
const divisionProgressData = [
{ name: "Sekretariat", selesai: 12, berjalan: 5, tertunda: 2 },
{ name: "Keuangan", selesai: 8, berjalan: 7, tertunda: 1 },
{ name: "Sosial", selesai: 10, berjalan: 3, tertunda: 4 },
{ name: "Humas", selesai: 6, berjalan: 9, tertunda: 3 },
];
// Division task summaries
const divisionTasks = [
// Top row - 4 activity cards
const activities = [
{
name: "Sekretariat",
tasks: [
{ title: "Laporan Bulanan", status: "selesai" },
{ title: "Arsip Dokumen", status: "berjalan" },
{ title: "Undangan Rapat", status: "tertunda" },
],
},
{
name: "Keuangan",
tasks: [
{ title: "Laporan APBDes", status: "selesai" },
{ title: "Verifikasi Dana", status: "tertunda" },
{ title: "Pengeluaran Harian", status: "berjalan" },
],
},
{
name: "Sosial",
tasks: [
{ title: "Program Bantuan", status: "selesai" },
{ title: "Kegiatan Posyandu", status: "berjalan" },
{ title: "Monitoring Stunting", status: "tertunda" },
],
},
{
name: "Humas",
tasks: [
{ title: "Publikasi Kegiatan", status: "selesai" },
{ title: "Koordinasi Media", status: "berjalan" },
{ title: "Laporan Kegiatan", status: "tertunda" },
],
},
];
// Archive items
const archiveItems = [
{ name: "Surat Keputusan", count: 12 },
{ name: "Laporan Keuangan", count: 8 },
{ name: "Dokumentasi", count: 24 },
{ name: "Notulensi Rapat", count: 15 },
];
// Activity progress
const activityProgress = [
{
name: "Pembangunan Jalan",
progress: 75,
date: "15 Feb 2026",
status: "berjalan",
},
{
name: "Posyandu Bulanan",
title: "Rakor 2025",
progress: 100,
date: "10 Feb 2026",
status: "selesai",
date: "15 Jan 2025",
},
{
name: "Vaksinasi Massal",
progress: 45,
date: "20 Feb 2026",
status: "berjalan",
title: "Pemutakhiran Indeks Desa",
progress: 100,
date: "20 Feb 2025",
},
{
name: "Festival Budaya",
progress: 20,
date: "5 Mar 2026",
status: "berjalan",
title: "Mengurus akta cerai warga",
progress: 100,
date: "5 Mar 2025",
},
{
title: "Pasek 7 desa adat",
progress: 100,
date: "10 Mar 2025",
},
];
// Document statistics
const documentStats = [
{ name: "Gambar", value: 42 },
{ name: "Dokumen", value: 87 },
{ name: "Gambar", value: 300, color: "#FAC858" },
{ name: "Dokumen", value: 310, color: "#92CC76" },
];
// Activity progress statistics
const activityProgressStats = [
{ name: "Selesai", value: 12, fill: "#10B981" },
{ name: "Dikerjakan", value: 8, fill: "#F59E0B" },
{ name: "Segera Dikerjakan", value: 5, fill: "#EF4444" },
{ name: "Dibatalkan", value: 2, fill: "#6B7280" },
{ name: "Selesai", value: 83.33, fill: "#92CC76" },
{ name: "Dikerjakan", value: 16.67, fill: "#FAC858" },
{ name: "Segera Dikerjakan", value: 0, fill: "#5470C6" },
{ name: "Dibatalkan", value: 0, fill: "#EE6767" },
];
const COLORS = ["#10B981", "#F59E0B", "#EF4444", "#6B7280"];
const STATUS_COLORS: Record<string, string> = {
selesai: "green",
berjalan: "blue",
tertunda: "red",
proses: "yellow",
};
// Discussion data
const discussions = [
{
title: "Pembahasan APBDes 2026",
sender: "Kepala Desa",
timestamp: "2 jam yang lalu",
date: "10 Mar 2025",
},
{
title: "Kegiatan Posyandu",
sender: "Divisi Sosial",
timestamp: "5 jam yang lalu",
date: "9 Mar 2025",
},
{
title: "Festival Budaya",
sender: "Divisi Humas",
timestamp: "1 hari yang lalu",
date: "8 Mar 2025",
},
];
// Today's agenda
const todayAgenda = [
{ time: "09:00", event: "Rapat Evaluasi Bulanan" },
{ time: "14:00", event: "Koordinasi Program Bantuan" },
];
return (
<Stack gap="lg">
{/* Grafik Progres Tugas per Divisi */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
<div
className="min-h-screen"
style={{
backgroundColor: dark ? "#10192D" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
{/* Top Row - 4 Activity Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
Grafik Progres Tugas per Divisi
</Title>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={divisionProgressData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#141D34" : "white"}
/>
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{
fill: dark
? "var(--mantine-color-text)"
: "var(--mantine-color-text)",
}}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{
fill: dark
? "var(--mantine-color-text)"
: "var(--mantine-color-text)",
}}
/>
<Tooltip
contentStyle={
dark
? {
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
: {}
}
/>
<Bar
dataKey="selesai"
stackId="a"
fill="#10B981"
name="Selesai"
radius={[4, 4, 0, 0]}
/>
<Bar
dataKey="berjalan"
stackId="a"
fill="#3B82F6"
name="Berjalan"
radius={[4, 4, 0, 0]}
/>
<Bar
dataKey="tertunda"
stackId="a"
fill="#EF4444"
name="Tertunda"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Card>
{/* Ringkasan Tugas per Divisi */}
<Grid gutter="md">
{divisionTasks.map((division, index) => (
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
{activities.map((activity, index) => (
<div
key={index}
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
{/* Dark blue title bar */}
<div
className="text-white px-3 py-2 rounded-t-lg -mx-5 -mt-5 mb-4"
style={{ backgroundColor: "#1E3A5F" }}
>
<Title order={4} mb="sm" c={dark ? "white" : "darmasaba-navy"}>
{division.name}
</Title>
<Stack gap="sm">
{division.tasks.map((task, taskIndex) => (
<Box key={taskIndex}>
<Group justify="space-between">
<Text size="sm" c={dark ? "white" : "darmasaba-navy"}>
{task.title}
</Text>
<MantineBadge
color={STATUS_COLORS[task.status] || "gray"}
variant="light"
>
{task.status}
</MantineBadge>
</Group>
</Box>
))}
</Stack>
</Card>
</GridCol>
))}
</Grid>
<h3 className="text-sm font-semibold">{activity.title}</h3>
</div>
{/* Arsip Digital Perangkat Desa */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
Arsip Digital Perangkat Desa
</Title>
<Grid gutter="md">
{archiveItems.map((item, index) => (
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
{/* Orange progress bar */}
<div
className="w-full rounded-full h-2 mb-3"
style={{ backgroundColor: dark ? "#2d3748" : "#E5E7EB" }}
>
<div
className="bg-orange-500 h-2 rounded-full"
style={{ width: `${activity.progress}%` }}
/>
</div>
{/* Date and badge */}
<div className="flex justify-between items-center">
<span
className="text-xs"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
<Group justify="space-between">
<Text c={dark ? "white" : "darmasaba-navy"} fw={500}>
{item.name}
</Text>
<Text c={dark ? "white" : "darmasaba-navy"} fw={700}>
{item.count}
</Text>
</Group>
</Card>
</GridCol>
))}
</Grid>
</Card>
{/* Kartu Progres Kegiatan */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
Progres Kegiatan / Program
</Title>
<Stack gap="md">
{activityProgress.map((activity, index) => (
<Card
key={index}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
>
<Group justify="space-between" mb="sm">
<Text c={dark ? "white" : "darmasaba-navy"} fw={500}>
{activity.name}
</Text>
<MantineBadge
color={STATUS_COLORS[activity.status] || "gray"}
variant="light"
>
{activity.status}
</MantineBadge>
</Group>
<Group justify="space-between">
<MantineProgress
value={activity.progress}
size="sm"
radius="xl"
color={activity.progress === 100 ? "green" : "blue"}
w="calc(100% - 80px)"
/>
<Text size="sm" c={dark ? "white" : "darmasaba-navy"}>
{activity.progress}%
</Text>
</Group>
<Text size="sm" c="dimmed" mt="sm">
{activity.date}
</Text>
</Card>
))}
</Stack>
</Card>
</span>
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full font-medium">
Selesai
</span>
</div>
</div>
))}
</div>
{/* Statistik Dokumen & Progres Kegiatan */}
<Grid gutter="md">
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
{/* Second Row - Charts */}
<div
className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{/* Left Card - Jumlah Dokumen (Bar Chart) */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
Jumlah Dokumen
</Title>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={documentStats}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#141D34" : "white"}
/>
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{
fill: dark
? "var(--mantine-color-text)"
: "var(--mantine-color-text)",
}}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{
fill: dark
? "var(--mantine-color-text)"
: "var(--mantine-color-text)",
}}
/>
<Tooltip
contentStyle={
dark
? {
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
: {}
}
/>
<Bar
dataKey="value"
fill={
dark
? "var(--mantine-color-blue-6)"
: "var(--mantine-color-blue-filled)"
}
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Card>
</GridCol>
Jumlah Dokumen
</h3>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={documentStats}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{documentStats.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
{/* Right Card - Progres Kegiatan (Pie Chart) */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
Progres Kegiatan
</Title>
<ResponsiveContainer width="100%" height={200}>
<PieChart
margin={{ top: 20, right: 80, bottom: 20, left: 80 }}
Progres Kegiatan
</h3>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={activityProgressStats.filter(item => item.value > 0)}
cx="50%"
cy="50%"
outerRadius={80}
dataKey="value"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : 0}%`
}
>
<Pie
data={activityProgressStats}
cx="50%"
cy="50%"
labelLine
outerRadius={65}
dataKey="value"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : "0"}%`
}
/>
<Tooltip
contentStyle={
dark
? {
backgroundColor: "var(--mantine-color-dark-7)",
borderColor: "var(--mantine-color-dark-6)",
}
: {}
}
/>
</PieChart>
</ResponsiveContainer>
</Card>
</GridCol>
</Grid>
{activityProgressStats
.filter(item => item.value > 0)
.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
</PieChart>
</ResponsiveContainer>
{/* Diskusi Internal */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
Diskusi Internal
</Title>
<Stack gap="sm">
{discussions.map((discussion, index) => (
<Card
key={index}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
>
<Group justify="space-between">
<Text c={dark ? "white" : "darmasaba-navy"} fw={500}>
{discussion.title}
</Text>
<Text size="sm" c="dimmed">
{discussion.timestamp}
</Text>
</Group>
<Text size="sm" c="dimmed">
{discussion.sender}
</Text>
</Card>
))}
</Stack>
</Card>
{/* Legend */}
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Segera Dikerjakan
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Dikerjakan
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Selesai
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Dibatalkan
</span>
</div>
</div>
</div>
</div>
{/* Agenda / Acara Hari Ini */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
{/* Bottom Row */}
<div
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
}}
>
<Title order={4} mb="md" c={dark ? "white" : "darmasaba-navy"}>
Agenda / Acara Hari Ini
</Title>
{todayAgenda.length > 0 ? (
<Stack gap="sm">
{todayAgenda.map((agenda, index) => (
<Group key={index} align="flex-start">
<Box w={60}>
<Text c="dimmed">{agenda.time}</Text>
</Box>
<Divider orientation="vertical" mx="sm" />
<Text c={dark ? "white" : "darmasaba-navy"}>
{agenda.event}
</Text>
</Group>
{/* Left Card - Diskusi */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Diskusi
</h3>
<div className="space-y-3">
{discussions.map((discussion, index) => (
<div
key={index}
className="flex items-start gap-3 p-3 rounded-lg transition-colors"
style={{
backgroundColor: dark ? "#1F2937" : "#F9FAFB",
}}
>
<div className="flex-shrink-0">
<div
className="w-8 h-8 rounded-full flex items-center justify-center"
style={{ backgroundColor: "#DBEAFE" }}
>
<IconMessage
className="w-4 h-4"
style={{ color: "#1E3A5F" }}
stroke={2}
/>
</div>
</div>
<div className="flex-1">
<h4
className="text-sm font-medium"
style={{ color: dark ? "white" : "#1F2937" }}
>
{discussion.title}
</h4>
<p
className="text-xs mt-1"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
{discussion.sender} {discussion.date}
</p>
</div>
</div>
))}
</Stack>
) : (
<Text c="dimmed" ta="center" py="md">
Tidak ada acara hari ini
</Text>
)}
</Card>
</Stack>
</div>
</div>
{/* Right Card - Acara Hari Ini */}
<div
className="rounded-xl shadow-sm p-5"
style={{
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Acara Hari Ini
</h3>
<div className="flex items-center justify-center h-32">
<p
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
Tidak ada acara hari ini
</p>
</div>
</div>
</div>
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,8 @@ export function Sidebar({ className }: SidebarProps) {
(item) => location.pathname === item.path,
);
const headerBgColor = colorScheme === "dark" ? "#ebedf0ff" : "#19355E";
return (
<Box className={className}>
{/* Logo */}
@@ -92,7 +94,7 @@ export function Sidebar({ className }: SidebarProps) {
label={item.name}
active={isActive}
variant="subtle"
color="blue"
color={headerBgColor}
style={{
background: isActive ? isActiveBg : "transparent",
fontWeight: isActive ? "bold" : "normal",
@@ -184,5 +186,6 @@ export function Sidebar({ className }: SidebarProps) {
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,464 +1,477 @@
import {
Badge,
Card,
Grid,
GridCol,
Group,
List,
Progress,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconAward,
IconBabyCarriage,
IconBook,
IconCalendarEvent,
IconHeartbeat,
IconMedicalCross,
IconSchool,
IconStethoscope,
IconUsers,
} from "@tabler/icons-react";
import { useState } from "react";
import { useMantineColorScheme, Title } from "@mantine/core";
const SosialPage = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// Sample data for health statistics
const healthStats = {
ibuHamil: 87,
balita: 342,
alertStunting: 12,
posyanduAktif: 8,
};
// Sample data for health progress
const healthProgress = [
{ label: "Imunisasi Lengkap", value: 92, color: "green" },
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" },
{ label: "Gizi Baik", value: 86, color: "teal" },
{ label: "Target Stunting", value: 14, color: "red" },
// Health statistics data
const healthStats = [
{
title: "Ibu Hamil Aktif",
value: "87",
subtitle: "Aktif",
icon: IconHeartbeat,
},
{
title: "Balita Terdaftar",
value: "342",
subtitle: "Terdaftar",
icon: IconBabyCarriage,
},
{
title: "Alert Stunting",
value: "12",
subtitle: "Perlu perhatian",
icon: IconStethoscope,
alert: true,
},
{
title: "Posyandu Aktif",
value: "8",
subtitle: "Beroperasi",
icon: IconMedicalCross,
},
];
// Sample data for posyandu schedule
// Health progress data
const healthProgress = [
{ label: "Imunisasi Lengkap", value: 92 },
{ label: "Pemeriksaan Rutin", value: 88 },
{ label: "Gizi Baik", value: 86 },
{ label: "Target Stunting", value: 14, isAlert: true },
];
// Posyandu schedule data
const posyanduSchedule = [
{
nama: "Posyandu Mawar",
tanggal: "Senin, 15 Feb 2026",
nama: "Posyandu Barat",
tanggal: "5 Oktober 2025",
jam: "08:00 - 11:00",
},
{
nama: "Posyandu Melati",
tanggal: "Selasa, 16 Feb 2026",
nama: "Posyandu Timur",
tanggal: "6 Oktober 2025",
jam: "08:00 - 11:00",
},
{
nama: "Posyandu Dahlia",
tanggal: "Rabu, 17 Feb 2026",
nama: "Posyandu Utara",
tanggal: "7 Oktober 2025",
jam: "08:00 - 11:00",
},
{
nama: "Posyandu Anggrek",
tanggal: "Kamis, 18 Feb 2026",
nama: "Posyandu Selatan",
tanggal: "8 Oktober 2025",
jam: "08:00 - 11:00",
},
];
// Sample data for education stats
const educationStats = {
siswa: {
tk: 125,
sd: 480,
smp: 210,
sma: 150,
},
sekolah: {
jumlah: 8,
guru: 42,
},
};
// Education stats data
const educationStats = [
{ level: "TK / PAUD", value: "500" },
{ level: "Siswa SD", value: "458" },
{ level: "Siswa SMP", value: "234" },
{ level: "Siswa SMA", value: "189" },
];
// Sample data for scholarships
// School info data
const schoolInfo = [
{ label: "Lembaga Pendidikan", value: "10" },
{ label: "Tenaga Pengajar", value: "3" },
];
// Scholarship data
const scholarshipData = {
penerima: 45,
dana: "Rp 1.200.000.000",
penerima: "250+",
dana: "1.5M",
tahunAjaran: "2025/2026",
};
// Sample data for cultural events
// Cultural events data
const culturalEvents = [
{
nama: "Hari Kesaktian Pancasila",
tanggal: "1 Oktober 2025",
lokasi: "Balai Desa",
},
{
nama: "Festival Budaya Desa",
tanggal: "20 Mei 2026",
nama: "Lomba Baris Berbaris",
tanggal: "1 Desember 2025",
lokasi: "Lapangan Desa",
},
{
nama: "Perayaan HUT Desa",
tanggal: "17 Agustus 2026",
lokasi: "Balai Desa",
nama: "Lomba Tari Tradisional",
tanggal: "10 Desember 2025",
lokasi: "Banjar Desa",
},
{
nama: "Davoz",
tanggal: "20 Desember 2025",
lokasi: "Kantor Desa",
},
];
return (
<Stack gap="lg">
{/* Health Statistics Cards */}
<Grid gutter="md">
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Ibu Hamil Aktif
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.ibuHamil}
</Text>
</Stack>
<ThemeIcon
variant="light"
color="darmasaba-blue"
size="xl"
radius="xl"
>
<IconHeartbeat size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Balita Terdaftar
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.balita}
</Text>
</Stack>
<ThemeIcon
variant="light"
color="darmasaba-success"
size="xl"
radius="xl"
>
<IconBabyCarriage size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Alert Stunting
</Text>
<Text size="xl" fw={700} c="red">
{healthStats.alertStunting}
</Text>
</Stack>
<ThemeIcon variant="light" color="red" size="xl" radius="xl">
<IconStethoscope size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Posyandu Aktif
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.posyanduAktif}
</Text>
</Stack>
<ThemeIcon
variant="light"
color="darmasaba-warning"
size="xl"
radius="xl"
>
<IconMedicalCross size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
</Grid>
{/* Health Progress Bars */}
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Statistik Kesehatan
</Title>
<Stack gap="md">
{healthProgress.map((item, index) => (
<div key={index}>
<Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "dark.0" : "black"}>
{item.label}
</Text>
<Text size="sm" fw={600} c={dark ? "dark.0" : "black"}>
{item.value}%
</Text>
</Group>
<Progress
value={item.value}
size="lg"
radius="xl"
color={item.color}
/>
<div
className={`min-h-screen py-6 px-4 sm:px-6 lg:px-8 ${
dark ? "bg-slate-900" : "bg-gray-100"
}`}
>
<div className="max-w-7xl mx-auto w-full">
{/* Row 1: Top 4 Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
{healthStats.map((stat, index) => (
<div
key={index}
className={`rounded-xl shadow-sm p-6 ${
dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3
className={`text-sm font-medium mb-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{stat.title}
</h3>
<p
className={`text-3xl font-bold mb-1 ${
stat.alert
? "text-red-500"
: dark
? "text-white"
: "text-gray-800"
}`}
>
{stat.value}
</p>
<p
className={`text-xs ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{stat.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center text-white ${
stat.alert ? "bg-red-500" : "bg-blue-900"
}`}
>
<stat.icon size={24} />
</div>
</div>
</div>
</div>
))}
</Stack>
</Card>
</div>
<Grid gutter="md">
{/* Jadwal Posyandu */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
{/* Row 2: Statistik Kesehatan */}
<div
className={`rounded-xl shadow-sm p-6 mb-6 ${
dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
}`}
>
<h3
className={`text-lg font-semibold mb-6 ${
dark ? "text-white" : "text-gray-800"
}`}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Jadwal Posyandu
</Title>
<Stack gap="sm">
{posyanduSchedule.map((item, index) => (
<Card
key={index}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
h="100%"
Statistik Kesehatan
</h3>
<div className="space-y-4">
{healthProgress.map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-2">
<span
className={`text-sm font-medium ${
dark ? "text-white" : "text-gray-800"
}`}
>
{item.label}
</span>
<span
className={`text-sm font-semibold ${
item.isAlert
? "text-red-500"
: dark
? "text-white"
: "text-gray-800"
}`}
>
{item.value}%
</span>
</div>
<div
className={`w-full rounded-full h-2 ${
dark ? "bg-slate-700" : "bg-gray-200"
}`}
>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>
<div
className={`h-2 rounded-full transition-all ${
item.isAlert ? "bg-red-500" : "bg-blue-900"
}`}
style={{ width: `${item.value}%` }}
/>
</div>
</div>
))}
</div>
</div>
{/* Row 3: Jadwal Posyandu & Pendidikan */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Jadwal Posyandu */}
<div
className={`rounded-xl shadow-sm p-6 ${
dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
}`}
>
<h3
className={`text-lg font-semibold mb-4 ${
dark ? "text-white" : "text-gray-800"
}`}
>
Jadwal Posyandu
</h3>
<div className="space-y-3">
{posyanduSchedule.map((item, index) => (
<div
key={index}
className={`p-4 rounded-lg ${
dark ? "bg-slate-700" : "bg-gray-50"
}`}
>
<div className="flex items-center justify-between">
<div>
<p
className={`text-sm font-medium ${
dark ? "text-white" : "text-gray-800"
}`}
>
{item.nama}
</Text>
<Text size="sm" c={dark ? "dark.0" : "black"}>
</p>
<p
className={`text-xs mt-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{item.tanggal}
</Text>
</Stack>
<Badge variant="light" color="darmasaba-blue">
</p>
</div>
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-900"
>
{item.jam}
</Badge>
</Group>
</Card>
</span>
</div>
</div>
))}
</Stack>
</Card>
</GridCol>
</div>
</div>
{/* Pendidikan */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
{/* Pendidikan Section */}
<div
className={`rounded-xl shadow-sm p-6 ${
dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
}`}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
<h3
className={`text-lg font-semibold mb-4 ${
dark ? "text-white" : "text-gray-800"
}`}
>
Pendidikan
</Title>
<Stack gap="md">
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
TK / PAUD
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.siswa.tk}
</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
SD
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.siswa.sd}
</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
SMP
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.siswa.smp}
</Text>
</Group>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
SMA
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.siswa.sma}
</Text>
</Group>
</h3>
<div className="space-y-3 mb-6">
{educationStats.map((item, index) => (
<div
key={index}
className={`flex items-center justify-between py-2 ${
dark ? "border-b border-slate-700" : "border-b border-gray-100"
}`}
>
<span className="text-sm" style={{ color: dark ? "#9CA3AF" : "#6B7280" }}>
{item.level}
</span>
<span
className={`text-sm font-semibold ${
dark ? "text-white" : "text-gray-800"
}`}
>
{item.value}
</span>
</div>
))}
</div>
<Card
withBorder
radius="md"
p="md"
mt="md"
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
Jumlah Lembaga Pendidikan
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.sekolah.jumlah}
</Text>
</Group>
<Group justify="space-between" mt="sm">
<Text fw={500} c={dark ? "dark.0" : "black"}>
Jumlah Tenaga Pengajar
</Text>
<Text fw={700} c={dark ? "dark.0" : "black"}>
{educationStats.sekolah.guru}
</Text>
</Group>
</Card>
</Stack>
</Card>
</GridCol>
</Grid>
{/* Info Sekolah */}
<h4
className={`text-base font-semibold mb-4 ${
dark ? "text-white" : "text-gray-800"
}`}
>
Info Sekolah
</h4>
<div className="space-y-3">
{schoolInfo.map((item, index) => (
<div
key={index}
className={`flex items-center justify-between py-3 px-4 rounded-lg ${
dark ? "bg-slate-700" : "bg-gray-50"
}`}
>
<span className="text-sm" style={{ color: dark ? "#9CA3AF" : "#6B7280" }}>
{item.label}
</span>
<span
className={`text-lg font-bold ${
dark ? "text-white" : "text-gray-800"
}`}
>
{item.value}
</span>
</div>
))}
</div>
</div>
</div>
<Grid gutter="md">
{/* Beasiswa Desa */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
{/* Row 4: Beasiswa Desa & Kalender Event Budaya */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Beasiswa Desa */}
<div
className={`rounded-xl shadow-sm p-6 ${
dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
}`}
>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Beasiswa Desa
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
Penerima: {scholarshipData.penerima}
</Text>
</Stack>
<ThemeIcon
variant="light"
color="darmasaba-success"
size="xl"
radius="xl"
<div className="flex items-center justify-between mb-6">
<h3
className={`text-lg font-semibold ${
dark ? "text-white" : "text-gray-800"
}`}
>
Beasiswa Desa
</h3>
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white bg-green-500"
>
<IconAward size={24} />
</ThemeIcon>
</Group>
<Text mt="md" c={dark ? "dark.0" : "black"}>
Dana Tersalurkan:{" "}
<Text span fw={700}>
{scholarshipData.dana}
</Text>
</Text>
<Text mt="sm" c={dark ? "dark.3" : "dimmed"}>
Tahun Ajaran: {scholarshipData.tahunAjaran}
</Text>
</Card>
</GridCol>
</div>
</div>
{/* Kalender Event Budaya */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Kalender Event Budaya
</Title>
<List spacing="sm">
{culturalEvents.map((event, index) => (
<List.Item
key={index}
icon={
<ThemeIcon color="darmasaba-blue" size={24} radius="xl">
<IconCalendarEvent size={12} />
</ThemeIcon>
}
{/* Two centered metrics */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div
className={`p-4 rounded-lg text-center ${
dark ? "bg-slate-700" : "bg-gray-50"
}`}
>
<p
className={`text-3xl font-bold mb-1 ${
dark ? "text-white" : "text-gray-800"
}`}
>
<Group justify="space-between">
<Text fw={500} c={dark ? "dark.0" : "black"}>
{scholarshipData.penerima}
</p>
<p
className={`text-xs ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
Penerima Beasiswa
</p>
</div>
<div
className={`p-4 rounded-lg text-center ${
dark ? "bg-slate-700" : "bg-gray-50"
}`}
>
<p
className={`text-3xl font-bold mb-1 text-green-500`}
>
{scholarshipData.dana}
</p>
<p
className={`text-xs ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
Dana Tersalurkan
</p>
</div>
</div>
{/* Footer text */}
<p
className={`text-center text-sm ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
Tahun Ajaran {scholarshipData.tahunAjaran}
</p>
</div>
{/* Kalender Event Budaya */}
<div
className={`rounded-xl shadow-sm p-6 ${
dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
}`}
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Kalender Event Budaya
</Title>
<div className="space-y-4">
{culturalEvents.map((event, index) => (
<div
key={index}
className={`flex items-start gap-3 p-4 rounded-lg ${
dark ? "bg-slate-700" : "bg-gray-50"
}`}
>
<div
className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 bg-blue-100 text-blue-900"
>
<IconCalendarEvent size={20} />
</div>
<div className="flex-1">
<p
className={`text-sm font-medium ${
dark ? "text-white" : "text-gray-800"
}`}
>
{event.nama}
</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{event.lokasi}
</Text>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{event.tanggal}
</Text>
</List.Item>
</p>
<p
className={`text-xs mt-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{event.tanggal}
</p>
<p
className={`text-xs mt-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
Location: {event.lokasi}
</p>
</div>
</div>
))}
</List>
</Card>
</GridCol>
</Grid>
</Stack>
</div>
</div>
</div>
</div>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1 +1,100 @@
@import "tailwindcss";
/* Custom CSS variables for Tailwind */
:root {
/* Darmasaba Navy Colors */
--darmasaba-navy-50: #E1E4F2;
--darmasaba-navy-100: #B9C2DD;
--darmasaba-navy-200: #91A0C9;
--darmasaba-navy-300: #697EBA;
--darmasaba-navy-400: #4C6CAE;
--darmasaba-navy-500: #3B5B97;
--darmasaba-navy-600: #2C497F;
--darmasaba-navy-700: #1E3766;
--darmasaba-navy-800: #12264D;
--darmasaba-navy-900: #071833;
--darmasaba-navy: #1E3A5F;
/* Darmasaba Blue Colors */
--darmasaba-blue-50: #E3F0FF;
--darmasaba-blue-100: #B6D9FF;
--darmasaba-blue-200: #89C2FF;
--darmasaba-blue-300: #5CA9FF;
--darmasaba-blue-400: #3B8FFF;
--darmasaba-blue-500: #237AE0;
--darmasaba-blue-600: #1C6BBF;
--darmasaba-blue-700: #155BA0;
--darmasaba-blue-800: #0E4980;
--darmasaba-blue-900: #073260;
--darmasaba-blue: #3B82F6;
/* Darmasaba Success Colors */
--darmasaba-success-50: #E3F9E7;
--darmasaba-success-100: #BFEEC7;
--darmasaba-success-200: #9BD8A7;
--darmasaba-success-300: #77C387;
--darmasaba-success-400: #5DB572;
--darmasaba-success-500: #499A5D;
--darmasaba-success-600: #3C7F4A;
--darmasaba-success-700: #2F6438;
--darmasaba-success-800: #234926;
--darmasaba-success-900: #17301B;
--darmasaba-success: #22C55E;
/* Darmasaba Warning Colors */
--darmasaba-warning-50: #FFF8E1;
--darmasaba-warning-100: #FEE7B3;
--darmasaba-warning-200: #FDD785;
--darmasaba-warning-300: #FDC757;
--darmasaba-warning-400: #FBBF3B;
--darmasaba-warning-500: #E1AC23;
--darmasaba-warning-600: #C2981D;
--darmasaba-warning-700: #A38418;
--darmasaba-warning-800: #856F12;
--darmasaba-warning-900: #675A0D;
--darmasaba-warning: #FACC15;
/* Darmasaba Danger Colors */
--darmasaba-danger-50: #FFE3E3;
--darmasaba-danger-100: #FFBABA;
--darmasaba-danger-200: #FF9191;
--darmasaba-danger-300: #FF6868;
--darmasaba-danger-400: #FA4B4B;
--darmasaba-danger-500: #E03333;
--darmasaba-danger-600: #C22A2A;
--darmasaba-danger-700: #A32020;
--darmasaba-danger-800: #851616;
--darmasaba-danger-900: #670C0C;
--darmasaba-danger: #EF4444;
/* Darmasaba Background */
--darmasaba-background: #F5F8FB;
/* Standard colors for dark mode */
--slate-900: #0F172A;
--slate-800: #1E293B;
--slate-700: #334155;
--slate-600: #475569;
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-600: #4B5563;
--gray-700: #1F2937;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-800: #1F2937;
--blue-50: #EFF6FF;
--blue-100: #DBEAFE;
--blue-900: #1E3A5F;
--red-500: #EF4444;
--green-500: #22C55E;
}
/* Dark mode support */
[data-mantine-color-scheme="dark"] {
color-scheme: dark;
}
[data-mantine-color-scheme="light"] {
color-scheme: light;
}

View File

@@ -66,10 +66,10 @@ const routeRules: RouteRule[] = [
redirectTo: "/signin",
},
{
match: (p) => p === "/dashboard" || p.startsWith("/dashboard/"),
match: (p) => p === "/admin" || p.startsWith("/admin/"),
requireAuth: true,
requiredRole: "admin",
redirectTo: "/profile",
redirectTo: "/signin",
},
];

View File

@@ -1,5 +1,4 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: <explanation */
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { authStore } from "@/store/auth";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
@@ -7,10 +6,22 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: RootComponent,
beforeLoad: protectedRouteMiddleware,
onEnter({ context }) {
authStore.user = context?.user as any;
authStore.session = context?.session as any;
beforeLoad: async () => {
// Fetch session but don't block navigation
try {
const res = await fetch("/api/session", {
method: "GET",
credentials: "include",
});
if (res.ok) {
const { data } = await res.json();
authStore.user = data?.user;
authStore.session = data;
}
} catch {
// Ignore errors, allow public access
}
return {};
},
});

View File

@@ -1,788 +1,7 @@
import {
ActionIcon,
Avatar,
Box,
Button,
Card,
Container,
Grid,
Group,
Image,
Paper,
rem,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
Transition,
useMantineColorScheme,
} from "@mantine/core";
import {
IconApi,
IconBolt,
IconBrandGithub,
IconBrandLinkedin,
IconBrandTwitter,
IconChevronRight,
IconLock,
IconMoon,
IconRocket,
IconShield,
IconStack2,
IconSun,
} from "@tabler/icons-react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: HomePage,
beforeLoad: () => {
throw redirect({ to: "/dashboard" });
},
});
// Navigation items
const NAV_ITEMS = [
{ label: "Home", link: "/" },
{ label: "Features", link: "#features" },
{ label: "Testimonials", link: "#testimonials" },
{ label: "Pricing", link: "/pricing" },
{ label: "Contact", link: "/contact" },
];
// Features data
const FEATURES = [
{
icon: IconBolt,
title: "Lightning Fast",
description: "Built on Bun runtime for exceptional performance and speed.",
},
{
icon: IconShield,
title: "Secure by Design",
description:
"Enterprise-grade authentication with Better Auth integration.",
},
{
icon: IconApi,
title: "RESTful API",
description:
"Full-featured API with Elysia.js for seamless backend operations.",
},
{
icon: IconStack2,
title: "Modern Stack",
description: "React 19, TanStack Router, and Mantine UI for the best DX.",
},
{
icon: IconLock,
title: "API Key Auth",
description: "Secure API key management for external integrations.",
},
{
icon: IconRocket,
title: "Production Ready",
description: "Type-safe, tested, and optimized for production deployment.",
},
];
// Testimonials data
const TESTIMONIALS = [
{
id: "testimonial-1",
name: "Alex Johnson",
role: "Lead Developer",
content:
"This template saved us weeks of setup time. The architecture is clean and well-thought-out.",
avatar:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
{
id: "testimonial-2",
name: "Sarah Williams",
role: "CTO",
content:
"The performance improvements we saw after switching to this stack were remarkable. Highly recommended!",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
{
id: "testimonial-3",
name: "Michael Chen",
role: "Product Manager",
content:
"The developer experience is top-notch. Everything is well-documented and easy to extend.",
avatar:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
];
function NavigationBar() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<Box
h={70}
px="md"
style={{
borderBottom: "1px solid var(--mantine-color-gray-2)",
transition: "all 0.3s ease",
boxShadow: scrolled ? "0 2px 10px rgba(0,0,0,0.1)" : "none",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Group h="100%" justify="space-between">
<Group>
<Link to="/" style={{ textDecoration: "none" }}>
<Title order={3} c="blue">
BunStack
</Title>
</Link>
<Group ml={50} visibleFrom="sm" gap="lg">
{NAV_ITEMS.map((item) => {
const isActive = window.location.pathname === item.link;
return (
<Box
key={item.label}
component={Link}
to={item.link}
style={{
textDecoration: "none",
fontSize: rem(16),
padding: `${rem(8)} ${rem(12)}`,
borderRadius: rem(6),
transition: "all 0.2s ease",
color: isActive
? "var(--mantine-color-blue-6)"
: "var(--mantine-color-dimmed)",
fontWeight: 500,
cursor: "pointer",
display: "block",
}}
className="nav-item"
>
{item.label}
</Box>
);
})}
</Group>
</Group>
<Group>
<ActionIcon
variant="default"
onClick={() => toggleColorScheme()}
size="lg"
>
{colorScheme === "dark" ? (
<IconSun size={18} />
) : (
<IconMoon size={18} />
)}
</ActionIcon>
<Button component={Link} to="/signin" variant="light" size="sm">
Sign In
</Button>
<Button component={Link} to="/signup" size="sm">
Get Started
</Button>
</Group>
</Group>
</Box>
);
}
function HeroSection() {
const [loaded, setLoaded] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
// Simulate delay for image transition
useEffect(() => {
const timer = setTimeout(() => {
setImageLoaded(true);
}, 200);
return () => clearTimeout(timer);
}, []);
return (
<Box
pt={rem(140)} // Adjusted padding for simpler header
pb={rem(60)}
>
<Container size="lg">
<Grid gutter={{ base: rem(40), md: rem(80) }} align="center">
<Grid.Col span={{ base: 12, md: 6 }}>
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Stack gap="xl" style={styles}>
<Title
order={1}
style={{
fontSize: rem(48),
fontWeight: 900,
lineHeight: 1.2,
}}
>
Build Faster with{" "}
<Text span c="blue" inherit>
Bun Stack
</Text>
</Title>
<Text size="xl" c="dimmed">
A modern, full-stack React template powered by Bun,
Elysia.js, and TanStack Router. Ship your ideas faster than
ever.
</Text>
<Group gap="md">
<Button
component={Link}
to="/admin"
size="lg"
variant="filled"
rightSection={<IconRocket size="1.25rem" />}
>
Get Started
</Button>
<Button
component={Link}
to="/docs"
size="lg"
variant="outline"
>
Learn More
</Button>
</Group>
</Stack>
)}
</Transition>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<Transition
mounted={imageLoaded}
transition="slide-left"
duration={800}
timingFunction="ease"
>
{(styles) => (
<Paper shadow="xl" radius="lg" p="md" withBorder style={styles}>
<Image
src="https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
alt="Code editor showing Bun Stack code"
radius="md"
/>
</Paper>
)}
</Transition>
</Grid.Col>
</Grid>
</Container>
</Box>
);
}
function AnimatedFeatureCard({
feature,
index,
isVisible,
}: {
feature: (typeof FEATURES)[number];
index: number;
isVisible: boolean;
}) {
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
useEffect(() => {
if (isVisible) {
const timer = setTimeout(() => {
setIsDelayedVisible(true);
}, index * 100);
return () => clearTimeout(timer);
}
}, [isVisible, index]);
return (
<Transition
mounted={isDelayedVisible}
transition="slide-up"
duration={500}
timingFunction="ease"
>
{(styles) => (
<Card
className="feature-card"
padding="lg"
radius="md"
withBorder
shadow="sm"
style={styles}
>
<ThemeIcon variant="light" color="blue" size={60} radius="md">
<feature.icon size="1.75rem" />
</ThemeIcon>
<Stack gap={8} mt="md">
<Title order={4}>{feature.title}</Title>
<Text size="sm" c="dimmed" lh={1.5}>
{feature.description}
</Text>
</Stack>
</Card>
)}
</Transition>
);
}
function FeaturesSection() {
const [visibleFeatures, setVisibleFeatures] = useState(
Array(FEATURES.length).fill(false),
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setVisibleFeatures((prev) => {
const newVisible = [...prev];
newVisible[index] = true;
return newVisible;
});
}
});
},
{ threshold: 0.1 },
);
const elements = document.querySelectorAll(".feature-card");
elements.forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return (
<Container size="lg" py={rem(80)}>
<Stack gap="xl" align="center" mb={rem(50)}>
<Transition
mounted={true}
transition="fade"
duration={600}
timingFunction="ease"
>
{(styles) => (
<div style={styles}>
<Title order={2} ta="center">
Everything You Need
</Title>
<Text c="dimmed" size="lg" ta="center" maw={600}>
A complete toolkit for building modern web applications with
best practices built-in.
</Text>
</div>
)}
</Transition>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
{FEATURES.map((feature, index) => (
<AnimatedFeatureCard
key={feature.title}
feature={feature}
index={index}
isVisible={visibleFeatures[index]}
/>
))}
</SimpleGrid>
</Container>
);
}
function AnimatedTestimonialCard({
testimonial,
index,
isVisible,
}: {
testimonial: (typeof TESTIMONIALS)[number];
index: number;
isVisible: boolean;
}) {
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
useEffect(() => {
if (isVisible) {
const timer = setTimeout(() => {
setIsDelayedVisible(true);
}, index * 150);
return () => clearTimeout(timer);
}
}, [isVisible, index]);
return (
<Transition
mounted={isDelayedVisible}
transition="slide-up"
duration={500}
timingFunction="ease"
>
{(styles) => (
<Card
padding="lg"
radius="md"
withBorder
shadow="sm"
className="testimonial-card"
style={styles}
>
<Text c="dimmed" mb="md">
"{testimonial.content}"
</Text>
<Group>
<Avatar src={testimonial.avatar} size="md" radius="xl" />
<Stack gap={0}>
<Text fw={600}>{testimonial.name}</Text>
<Text size="sm" c="dimmed">
{testimonial.role}
</Text>
</Stack>
</Group>
</Card>
)}
</Transition>
);
}
function TestimonialsSection() {
const [visibleTestimonials, setVisibleTestimonials] = useState(
Array(TESTIMONIALS.length).fill(false),
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setVisibleTestimonials((prev) => {
const newVisible = [...prev];
newVisible[index] = true;
return newVisible;
});
}
});
},
{ threshold: 0.1 },
);
const elements = document.querySelectorAll(".testimonial-card");
elements.forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return (
<Box py={rem(80)}>
<Container size="lg">
<Stack gap="xl" align="center" mb={rem(50)}>
<Transition
mounted={true}
transition="fade"
duration={600}
timingFunction="ease"
>
{(styles) => (
<div style={styles}>
<Title order={2} ta="center">
Loved by Developers
</Title>
<Text c="dimmed" size="lg" ta="center" maw={600}>
Join thousands of satisfied developers who have accelerated
their projects with Bun Stack.
</Text>
</div>
)}
</Transition>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
{TESTIMONIALS.map((testimonial, index) => (
<AnimatedTestimonialCard
key={testimonial.id}
testimonial={testimonial}
index={index}
isVisible={visibleTestimonials[index]}
/>
))}
</SimpleGrid>
</Container>
</Box>
);
}
function CtaSection() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
return (
<Container size="lg" py={rem(80)}>
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Paper
radius="lg"
p={rem(60)}
bg="blue"
style={{
...styles,
background:
"linear-gradient(135deg, var(--mantine-color-blue-6), var(--mantine-color-indigo-6))",
}}
>
<Stack align="center" gap="xl" ta="center">
<Title c="white" order={2}>
Ready to get started?
</Title>
<Text c="white" size="lg" maw={600}>
Join thousands of developers who are building faster and more
reliable applications with Bun Stack.
</Text>
<Group>
<Button
component={Link}
to="/signup"
size="lg"
variant="white"
color="dark"
rightSection={<IconChevronRight size="1.125rem" />}
>
Create Account
</Button>
<Button
component={Link}
to="/docs"
size="lg"
variant="outline"
color="white"
>
View Documentation
</Button>
</Group>
</Stack>
</Paper>
)}
</Transition>
</Container>
);
}
function Footer() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setLoaded(true);
}, 300);
return () => clearTimeout(timer);
}, []);
return (
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Box
py={rem(40)}
style={{
...styles,
borderTop: "1px solid var(--mantine-color-gray-2)",
}}
>
<Container size="lg">
<Grid gutter={{ base: rem(40), md: rem(80) }}>
<Grid.Col span={{ base: 12, md: 4 }}>
<Stack gap="md">
<Title order={3}>BunStack</Title>
<Text size="sm" c="dimmed">
The ultimate full-stack solution for modern web
applications.
</Text>
<Group>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandGithub size="1.25rem" />
</ActionIcon>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandTwitter size="1.25rem" />
</ActionIcon>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandLinkedin size="1.25rem" />
</ActionIcon>
</Group>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Stack gap="xs">
<Title order={4}>Product</Title>
<Text
size="sm"
c="dimmed"
component={Link}
to="/features"
td="none"
>
Features
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/pricing"
td="none"
>
Pricing
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/docs"
td="none"
>
Documentation
</Text>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Stack gap="xs">
<Title order={4}>Company</Title>
<Text
size="sm"
c="dimmed"
component={Link}
to="/about"
td="none"
>
About
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/blog"
td="none"
>
Blog
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/careers"
td="none"
>
Careers
</Text>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Stack gap="xs">
<Title order={4}>Subscribe to our newsletter</Title>
<Text size="sm" c="dimmed">
Get the latest news and updates
</Text>
<Group>
<input
type="email"
placeholder="Your email"
style={{
padding: "8px 12px",
borderRadius: "4px",
border: "1px solid var(--mantine-color-gray-3)",
flex: 1,
}}
/>
<Button>Subscribe</Button>
</Group>
</Stack>
</Grid.Col>
</Grid>
<Box
pt={rem(40)}
style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}
>
<Group justify="space-between" align="center">
<Text size="sm" c="dimmed">
© 2024 Bun Stack. Built with Bun, Elysia, and React.
</Text>
<Group gap="lg">
<Text
component={Link}
to="/privacy"
size="sm"
c="dimmed"
style={{ textDecoration: "none" }}
>
Privacy Policy
</Text>
<Text
component={Link}
to="/terms"
size="sm"
c="dimmed"
style={{ textDecoration: "none" }}
>
Terms of Service
</Text>
</Group>
</Group>
</Box>
</Container>
</Box>
)}
</Transition>
);
}
function HomePage() {
return (
<Box>
<NavigationBar />
<HeroSection />
<FeaturesSection />
<TestimonialsSection />
<CtaSection />
<Footer />
</Box>
);
}

View File

@@ -21,6 +21,7 @@ export const auth = betterAuth({
clientId: process.env.GITHUB_CLIENT_ID || "CLIENT_ID_MISSING",
clientSecret: process.env.GITHUB_CLIENT_SECRET || "CLIENT_SECRET_MISSING",
enabled: true,
redirectURI: `${baseUrl}/api/auth/callback/github`,
},
},
user: {

View File

@@ -1,11 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
content: [
"./src/index.html",
"./public/**/*.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
"darmasaba-navy": {
DEFAULT: "#1E3A5F", // Primary navy color
DEFAULT: "#1E3A5F",
50: "#E1E4F2",
100: "#B9C2DD",
200: "#91A0C9",
@@ -18,7 +22,7 @@ module.exports = {
900: "#071833",
},
"darmasaba-blue": {
DEFAULT: "#3B82F6", // Primary blue color
DEFAULT: "#3B82F6",
50: "#E3F0FF",
100: "#B6D9FF",
200: "#89C2FF",