Compare commits
13 Commits
nico/13-ma
...
stg
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e6734d1a5 | |||
|
|
1b9ddf0f4b | ||
| a0f440f6b3 | |||
| 1f56dd7660 | |||
| 1a2a213d0a | |||
| 1ec10fe623 | |||
| 226b0880e6 | |||
| 5d9be8c479 | |||
| e83bea2bc2 | |||
| 95c08681a7 | |||
| 9b015ec84d | |||
| 38b22dd2dd | |||
| 5801eb4596 |
19
.env.example
Normal file
19
.env.example
Normal 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
106
.github/workflows/publish.yml
vendored
Normal 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
60
.github/workflows/re-pull.yml
vendored
Normal 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
26
.github/workflows/script/notify.sh
vendored
Normal 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
93
.github/workflows/script/re-pull.sh
vendored
Normal 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
4
.gitignore
vendored
@@ -16,6 +16,7 @@ _.log
|
|||||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
# dotenv environment variable files
|
# dotenv environment variable files
|
||||||
|
# Only .env.example is allowed to be committed
|
||||||
.env
|
.env
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@@ -33,6 +34,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Dashboard-MD
|
||||||
|
Dashboard-MD
|
||||||
|
|
||||||
# Playwright artifacts
|
# Playwright artifacts
|
||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|||||||
62
Dockerfile
Normal file
62
Dockerfile
Normal 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
|
||||||
|
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"]
|
||||||
12
QWEN.md
12
QWEN.md
@@ -51,11 +51,13 @@ bun install
|
|||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Fill in your DATABASE_URL and BETTER_AUTH_SECRET
|
# Fill in your DATABASE_URL and BETTER_AUTH_SECRET
|
||||||
|
# Optional: Set ADMIN_EMAIL and ADMIN_PASSWORD for admin user
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Initialization
|
### Database Initialization
|
||||||
```bash
|
```bash
|
||||||
bun x prisma migrate dev
|
bun x prisma migrate dev
|
||||||
|
bun run seed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start Development
|
### Start Development
|
||||||
@@ -109,4 +111,12 @@ bun run dev
|
|||||||
- `test:e2e`: Runs end-to-end tests
|
- `test:e2e`: Runs end-to-end tests
|
||||||
- `build`: Builds the application for production
|
- `build`: Builds the application for production
|
||||||
- `start`: Starts the production server
|
- `start`: Starts the production server
|
||||||
- `seed`: Seeds the database with initial data
|
- `seed`: Seeds the database with admin and demo users
|
||||||
|
|
||||||
|
## Default Users (after running `bun run seed`)
|
||||||
|
|
||||||
|
- **Admin**: `ADMIN_EMAIL` (from env) / `ADMIN_PASSWORD` (default: `admin123`)
|
||||||
|
- **Demo Users**:
|
||||||
|
- `demo1@example.com` / `demo123` (role: user)
|
||||||
|
- `demo2@example.com` / `demo123` (role: user)
|
||||||
|
- `moderator@example.com` / `demo123` (role: moderator)
|
||||||
6
bun.lock
6
bun.lock
@@ -47,6 +47,7 @@
|
|||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-router": "^1.158.1",
|
"@tanstack/react-router": "^1.158.1",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"better-auth": "^1.4.18",
|
"better-auth": "^1.4.18",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"cmdk": "^1.0.1",
|
"cmdk": "^1.0.1",
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
"@tanstack/react-router-devtools": "^1.158.1",
|
"@tanstack/react-router-devtools": "^1.158.1",
|
||||||
"@tanstack/router-cli": "1.158.1",
|
"@tanstack/router-cli": "1.158.1",
|
||||||
"@tanstack/router-vite-plugin": "^1.158.1",
|
"@tanstack/router-vite-plugin": "^1.158.1",
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -628,6 +630,8 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
|
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||||
|
|
||||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
@@ -776,6 +780,8 @@
|
|||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
|
||||||
|
|
||||||
|
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
||||||
|
|
||||||
"better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="],
|
"better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="],
|
||||||
|
|
||||||
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts",
|
"dev": "lsof -ti:3000 | xargs kill -9 2>/dev/null || true; bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"check": "biome check --write .",
|
"check": "biome check --write .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"test": "bun test __tests__/api",
|
"test": "bun test __tests__/api",
|
||||||
"test:ui": "bun test --ui __tests__/api",
|
"test:ui": "bun test --ui __tests__/api",
|
||||||
"test:e2e": "bun run build && playwright test",
|
"test:e2e": "bun run build && playwright test",
|
||||||
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*'",
|
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*' && cp -r public/* dist/ 2>/dev/null || true",
|
||||||
"start": "NODE_ENV=production bun src/index.ts",
|
"start": "NODE_ENV=production bun src/index.ts",
|
||||||
"seed": "bun prisma/seed.ts"
|
"seed": "bun prisma/seed.ts"
|
||||||
},
|
},
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-router": "^1.158.1",
|
"@tanstack/react-router": "^1.158.1",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"better-auth": "^1.4.18",
|
"better-auth": "^1.4.18",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"cmdk": "^1.0.1",
|
"cmdk": "^1.0.1",
|
||||||
@@ -92,6 +93,7 @@
|
|||||||
"@tanstack/react-router-devtools": "^1.158.1",
|
"@tanstack/react-router-devtools": "^1.158.1",
|
||||||
"@tanstack/router-cli": "1.158.1",
|
"@tanstack/router-cli": "1.158.1",
|
||||||
"@tanstack/router-vite-plugin": "^1.158.1",
|
"@tanstack/router-vite-plugin": "^1.158.1",
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
112
prisma/seed.ts
112
prisma/seed.ts
@@ -1,13 +1,16 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
import { hash } from "bcryptjs";
|
||||||
|
import { generateId } from "better-auth";
|
||||||
import { prisma } from "@/utils/db";
|
import { prisma } from "@/utils/db";
|
||||||
|
|
||||||
async function seedAdminUser() {
|
async function seedAdminUser() {
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
const adminEmail = process.env.ADMIN_EMAIL;
|
const adminEmail = process.env.ADMIN_EMAIL;
|
||||||
|
const adminPassword = process.env.ADMIN_PASSWORD || "admin123";
|
||||||
|
|
||||||
if (!adminEmail) {
|
if (!adminEmail) {
|
||||||
console.log(
|
console.log(
|
||||||
"No ADMIN_EMAIL environment variable found. Skipping admin role assignment.",
|
"No ADMIN_EMAIL environment variable found. Skipping admin user creation.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -30,9 +33,35 @@ async function seedAdminUser() {
|
|||||||
console.log(`User with email ${adminEmail} already has admin role.`);
|
console.log(`User with email ${adminEmail} already has admin role.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
// Create new admin user
|
||||||
`No user found with email ${adminEmail}. Skipping admin role assignment.`,
|
const hashedPassword = await hash(adminPassword, 12);
|
||||||
);
|
const userId = generateId();
|
||||||
|
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id: userId,
|
||||||
|
email: adminEmail,
|
||||||
|
name: "Admin User",
|
||||||
|
role: "admin",
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.account.create({
|
||||||
|
data: {
|
||||||
|
id: generateId(),
|
||||||
|
userId,
|
||||||
|
accountId: userId,
|
||||||
|
providerId: "credential",
|
||||||
|
password: hashedPassword,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Admin user created with email: ${adminEmail}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error seeding admin user:", error);
|
console.error("Error seeding admin user:", error);
|
||||||
@@ -40,15 +69,82 @@ async function seedAdminUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function seedDemoUsers() {
|
||||||
|
const demoUsers = [
|
||||||
|
{ email: "demo1@example.com", name: "Demo User 1", role: "user" },
|
||||||
|
{ email: "demo2@example.com", name: "Demo User 2", role: "user" },
|
||||||
|
{
|
||||||
|
email: "moderator@example.com",
|
||||||
|
name: "Moderator User",
|
||||||
|
role: "moderator",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const userData of demoUsers) {
|
||||||
|
try {
|
||||||
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
where: { email: userData.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
const userId = generateId();
|
||||||
|
const hashedPassword = await hash("demo123", 12);
|
||||||
|
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id: userId,
|
||||||
|
email: userData.email,
|
||||||
|
name: userData.name,
|
||||||
|
role: userData.role,
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.account.create({
|
||||||
|
data: {
|
||||||
|
id: generateId(),
|
||||||
|
userId,
|
||||||
|
accountId: userId,
|
||||||
|
providerId: "credential",
|
||||||
|
password: hashedPassword,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Demo user created: ${userData.email}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Demo user already exists: ${userData.email}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error seeding user ${userData.email}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log("Seeding database...");
|
console.log("Seeding database...");
|
||||||
|
|
||||||
await seedAdminUser();
|
await seedAdminUser();
|
||||||
|
await seedDemoUsers();
|
||||||
|
|
||||||
console.log("Database seeding completed.");
|
console.log("Database seeding completed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
// Only auto-execute when run directly (not when imported)
|
||||||
console.error("Error during seeding:", error);
|
const isMainModule =
|
||||||
process.exit(1);
|
typeof require !== "undefined"
|
||||||
});
|
? require.main === module
|
||||||
|
: import.meta.path.endsWith("seed.ts");
|
||||||
|
|
||||||
|
if (isMainModule) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Error during seeding:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for programmatic use
|
||||||
|
export { seedAdminUser, seedDemoUsers, main as runSeed };
|
||||||
|
|||||||
BIN
public/logo-desa-plus.png
Normal file
BIN
public/logo-desa-plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -66,6 +66,12 @@ const eventData = [
|
|||||||
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
|
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const apbdesData = [
|
||||||
|
{ name: "Belanja", value: 70, color: "blue" },
|
||||||
|
{ name: "Pendapatan", value: 90, color: "green" },
|
||||||
|
{ name: "Pembangunan", value: 50, color: "orange" },
|
||||||
|
];
|
||||||
|
|
||||||
export function DashboardContent() {
|
export function DashboardContent() {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
@@ -480,42 +486,23 @@ export function DashboardContent() {
|
|||||||
Grafik APBDes
|
Grafik APBDes
|
||||||
</Title>
|
</Title>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group align="center" gap="md">
|
{apbdesData.map((data, index) => (
|
||||||
<Text size="sm" fw={500} w={60}>
|
<Grid key={index} align="center">
|
||||||
Belanja
|
<Grid.Col span={3}>
|
||||||
</Text>
|
<Text size="sm" fw={500}>
|
||||||
<Progress
|
{data.name}
|
||||||
value={70}
|
</Text>
|
||||||
size="lg"
|
</Grid.Col>
|
||||||
radius="xl"
|
<Grid.Col span={9}>
|
||||||
color="blue"
|
<Progress
|
||||||
style={{ flex: 1 }}
|
value={data.value}
|
||||||
/>
|
size="lg"
|
||||||
</Group>
|
radius="xl"
|
||||||
<Group align="center" gap="md">
|
color={data.color}
|
||||||
<Text size="sm" fw={500} w={60}>
|
/>
|
||||||
Pendapatan
|
</Grid.Col>
|
||||||
</Text>
|
</Grid>
|
||||||
<Progress
|
))}
|
||||||
value={90}
|
|
||||||
size="lg"
|
|
||||||
radius="xl"
|
|
||||||
color="green"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<Group align="center" gap="md">
|
|
||||||
<Text size="sm" fw={500} w={60}>
|
|
||||||
Pembangunan
|
|
||||||
</Text>
|
|
||||||
<Progress
|
|
||||||
value={50}
|
|
||||||
size="lg"
|
|
||||||
radius="xl"
|
|
||||||
color="orange"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,121 +1,72 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Avatar,
|
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Divider,
|
|
||||||
Group,
|
Group,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconUserShield } from "@tabler/icons-react";
|
import { useLocation } from "@tanstack/react-router";
|
||||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
import { Bell, Moon, Sun } from "lucide-react";
|
||||||
import { Bell, Moon, Sun, User as UserIcon } from "lucide-react"; // Renamed User to UserIcon to avoid conflict with Mantine's User component if it exists
|
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Define page titles based on route
|
const title =
|
||||||
const getPageTitle = () => {
|
location.pathname === "/"
|
||||||
switch (location.pathname) {
|
? "Desa Darmasaba"
|
||||||
case "/":
|
: "Desa Darmasaba";
|
||||||
return "Beranda";
|
|
||||||
case "/kinerja-divisi":
|
|
||||||
return "Kinerja Divisi";
|
|
||||||
case "/pengaduan-layanan-publik":
|
|
||||||
return "Pengaduan & Layanan Publik";
|
|
||||||
case "/jenna-analytic":
|
|
||||||
return "Jenna Analytic";
|
|
||||||
case "/demografi-pekerjaan":
|
|
||||||
return "Demografi & Kependudukan";
|
|
||||||
case "/keuangan-anggaran":
|
|
||||||
return "Keuangan & Anggaran";
|
|
||||||
case "/bumdes":
|
|
||||||
return "Bumdes & UMKM Desa";
|
|
||||||
case "/sosial":
|
|
||||||
return "Sosial";
|
|
||||||
case "/keamanan":
|
|
||||||
return "Keamanan";
|
|
||||||
case "/bantuan":
|
|
||||||
return "Bantuan";
|
|
||||||
case "/pengaturan":
|
|
||||||
case "/pengaturan/umum":
|
|
||||||
case "/pengaturan/notifikasi":
|
|
||||||
case "/pengaturan/keamanan":
|
|
||||||
case "/pengaturan/akses-dan-tim":
|
|
||||||
return "Pengaturan";
|
|
||||||
default:
|
|
||||||
return "Desa Darmasaba";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group justify="space-between" w="100%">
|
<Box
|
||||||
{/* Title */}
|
style={{
|
||||||
<Title order={3} c={"white"}>
|
display: "grid",
|
||||||
{getPageTitle()}
|
gridTemplateColumns: "1fr auto 1fr",
|
||||||
</Title>
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* LEFT SPACER (burger sudah di luar) */}
|
||||||
|
<Box />
|
||||||
|
|
||||||
{/* Right Section */}
|
{/* CENTER TITLE */}
|
||||||
<Group gap="md">
|
<Text
|
||||||
{/* User Info */}
|
c="white"
|
||||||
<Group gap="sm">
|
fw={600}
|
||||||
<Box ta="right">
|
size="md"
|
||||||
<Text c={"white"} size="sm" fw={500}>
|
style={{
|
||||||
I. B. Surya Prabhawa M...
|
textAlign: "center",
|
||||||
</Text>
|
whiteSpace: "nowrap",
|
||||||
<Text c={"white"} size="xs">
|
overflow: "hidden",
|
||||||
Kepala Desa
|
textOverflow: "ellipsis",
|
||||||
</Text>
|
}}
|
||||||
</Box>
|
>
|
||||||
<Avatar color="blue" radius="xl">
|
{title}
|
||||||
<UserIcon color="white" style={{ width: "70%", height: "70%" }} />
|
</Text>
|
||||||
</Avatar>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Divider */}
|
{/* RIGHT ICONS */}
|
||||||
<Divider orientation="vertical" h={30} />
|
<Group gap="xs" justify="flex-end">
|
||||||
|
<ActionIcon
|
||||||
|
onClick={toggleColorScheme}
|
||||||
|
variant="subtle"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
{dark ? <Sun size={18} /> : <Moon size={18} />}
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
{/* Icons */}
|
<ActionIcon variant="subtle" radius="xl" pos="relative">
|
||||||
<Group gap="sm">
|
<Bell size={18} />
|
||||||
<ActionIcon
|
<Badge
|
||||||
onClick={() => toggleColorScheme()}
|
size="xs"
|
||||||
variant="subtle"
|
color="red"
|
||||||
size="lg"
|
style={{ position: "absolute", top: -4, right: -4 }}
|
||||||
radius="xl"
|
|
||||||
aria-label="Toggle color scheme"
|
|
||||||
>
|
>
|
||||||
{dark ? (
|
10
|
||||||
<Sun color="white" style={{ width: "70%", height: "70%" }} />
|
</Badge>
|
||||||
) : (
|
</ActionIcon>
|
||||||
<Moon color="white" style={{ width: "70%", height: "70%" }} />
|
|
||||||
)}
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon variant="subtle" size="lg" radius="xl" pos="relative">
|
|
||||||
<Bell color="white" style={{ width: "70%", height: "70%" }} />
|
|
||||||
<Badge
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
variant="filled"
|
|
||||||
style={{ position: "absolute", top: 0, right: 0 }}
|
|
||||||
radius={"xl"}
|
|
||||||
>
|
|
||||||
10
|
|
||||||
</Badge>
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon variant="subtle" size="lg" radius="xl">
|
|
||||||
<IconUserShield
|
|
||||||
color="white"
|
|
||||||
style={{ width: "70%", height: "70%" }}
|
|
||||||
onClick={() => navigate({ to: "/signin" })}
|
|
||||||
/>
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,13 +143,6 @@ const HelpPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="lg" py="xl">
|
<Container size="lg" py="xl">
|
||||||
<Title order={1} mb="xl" ta="center">
|
|
||||||
Pusat Bantuan
|
|
||||||
</Title>
|
|
||||||
<Text size="lg" color="dimmed" ta="center" mb="xl">
|
|
||||||
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Statistics Section */}
|
{/* Statistics Section */}
|
||||||
<SimpleGrid cols={3} spacing="lg" mb="xl">
|
<SimpleGrid cols={3} spacing="lg" mb="xl">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
|
|||||||
@@ -122,10 +122,10 @@ const KinerjaDivisi = () => {
|
|||||||
|
|
||||||
// Activity progress statistics
|
// Activity progress statistics
|
||||||
const activityProgressStats = [
|
const activityProgressStats = [
|
||||||
{ name: "Selesai", value: 12 },
|
{ name: "Selesai", value: 12, fill: "#10B981" },
|
||||||
{ name: "Dikerjakan", value: 8 },
|
{ name: "Dikerjakan", value: 8, fill: "#F59E0B" },
|
||||||
{ name: "Segera Dikerjakan", value: 5 },
|
{ name: "Segera Dikerjakan", value: 5, fill: "#EF4444" },
|
||||||
{ name: "Dibatalkan", value: 2 },
|
{ name: "Dibatalkan", value: 2, fill: "#6B7280" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const COLORS = ["#10B981", "#F59E0B", "#EF4444", "#6B7280"];
|
const COLORS = ["#10B981", "#F59E0B", "#EF4444", "#6B7280"];
|
||||||
@@ -204,9 +204,9 @@ const KinerjaDivisi = () => {
|
|||||||
contentStyle={
|
contentStyle={
|
||||||
dark
|
dark
|
||||||
? {
|
? {
|
||||||
backgroundColor: "var(--mantine-color-dark-7)",
|
backgroundColor: "var(--mantine-color-dark-7)",
|
||||||
borderColor: "var(--mantine-color-dark-6)",
|
borderColor: "var(--mantine-color-dark-6)",
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -402,9 +402,9 @@ const KinerjaDivisi = () => {
|
|||||||
contentStyle={
|
contentStyle={
|
||||||
dark
|
dark
|
||||||
? {
|
? {
|
||||||
backgroundColor: "var(--mantine-color-dark-7)",
|
backgroundColor: "var(--mantine-color-dark-7)",
|
||||||
borderColor: "var(--mantine-color-dark-6)",
|
borderColor: "var(--mantine-color-dark-6)",
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -434,33 +434,27 @@ const KinerjaDivisi = () => {
|
|||||||
Progres Kegiatan
|
Progres Kegiatan
|
||||||
</Title>
|
</Title>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
<PieChart>
|
<PieChart
|
||||||
|
margin={{ top: 20, right: 80, bottom: 20, left: 80 }}
|
||||||
|
>
|
||||||
<Pie
|
<Pie
|
||||||
data={activityProgressStats}
|
data={activityProgressStats}
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
labelLine={false}
|
labelLine
|
||||||
outerRadius={80}
|
outerRadius={65}
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
label={({ name, percent }) =>
|
label={({ name, percent }) =>
|
||||||
`${name}: ${percent ? (percent * 100).toFixed(0) : "0"}%`
|
`${name}: ${percent ? (percent * 100).toFixed(0) : "0"}%`
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
{activityProgressStats.map((entry, index) => (
|
|
||||||
<Cell
|
|
||||||
key={`cell-${index}`}
|
|
||||||
fill={COLORS[index % COLORS.length]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={
|
contentStyle={
|
||||||
dark
|
dark
|
||||||
? {
|
? {
|
||||||
backgroundColor: "var(--mantine-color-dark-7)",
|
backgroundColor: "var(--mantine-color-dark-7)",
|
||||||
borderColor: "var(--mantine-color-dark-6)",
|
borderColor: "var(--mantine-color-dark-6)",
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Box,
|
Box,
|
||||||
Collapse,
|
Collapse,
|
||||||
Group,
|
Image,
|
||||||
Input,
|
Input,
|
||||||
NavLink as MantineNavLink,
|
NavLink as MantineNavLink,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
useMantineColorScheme
|
||||||
useMantineColorScheme,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
import { ChevronDown, ChevronUp, Search } from "lucide-react";
|
import { ChevronDown, ChevronUp, Search } from "lucide-react";
|
||||||
@@ -27,29 +25,35 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
|
|
||||||
// State for settings submenu collapse
|
// State for settings submenu collapse
|
||||||
const [settingsOpen, setSettingsOpen] = useState(
|
const [settingsOpen, setSettingsOpen] = useState(
|
||||||
location.pathname.startsWith("/pengaturan"),
|
location.pathname.startsWith("/dashboard/pengaturan"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Define menu items with their paths
|
// Define menu items with their paths
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ name: "Beranda", path: "/" },
|
{ name: "Beranda", path: "/dashboard" },
|
||||||
{ name: "Kinerja Divisi", path: "/kinerja-divisi" },
|
{ name: "Kinerja Divisi", path: "/dashboard/kinerja-divisi" },
|
||||||
{ name: "Pengaduan & Layanan Publik", path: "/pengaduan-layanan-publik" },
|
{
|
||||||
{ name: "Jenna Analytic", path: "/jenna-analytic" },
|
name: "Pengaduan & Layanan Publik",
|
||||||
{ name: "Demografi & Kependudukan", path: "/demografi-pekerjaan" },
|
path: "/dashboard/pengaduan-layanan-publik",
|
||||||
{ name: "Keuangan & Anggaran", path: "/keuangan-anggaran" },
|
},
|
||||||
{ name: "Bumdes & UMKM Desa", path: "/bumdes" },
|
{ name: "Jenna Analytic", path: "/dashboard/jenna-analytic" },
|
||||||
{ name: "Sosial", path: "/sosial" },
|
{
|
||||||
{ name: "Keamanan", path: "/keamanan" },
|
name: "Demografi & Kependudukan",
|
||||||
{ name: "Bantuan", path: "/bantuan" },
|
path: "/dashboard/demografi-pekerjaan",
|
||||||
|
},
|
||||||
|
{ name: "Keuangan & Anggaran", path: "/dashboard/keuangan-anggaran" },
|
||||||
|
{ name: "Bumdes & UMKM Desa", path: "/dashboard/bumdes" },
|
||||||
|
{ name: "Sosial", path: "/dashboard/sosial" },
|
||||||
|
{ name: "Keamanan", path: "/dashboard/keamanan" },
|
||||||
|
{ name: "Bantuan", path: "/dashboard/bantuan" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Settings submenu items
|
// Settings submenu items
|
||||||
const settingsItems = [
|
const settingsItems = [
|
||||||
{ name: "Umum", path: "/pengaturan/umum" },
|
{ name: "Umum", path: "/dashboard/pengaturan/umum" },
|
||||||
{ name: "Notifikasi", path: "/pengaturan/notifikasi" },
|
{ name: "Notifikasi", path: "/dashboard/pengaturan/notifikasi" },
|
||||||
{ name: "Keamanan", path: "/pengaturan/keamanan" },
|
{ name: "Keamanan", path: "/dashboard/pengaturan/keamanan" },
|
||||||
{ name: "Akses & Tim", path: "/pengaturan/akses-dan-tim" },
|
{ name: "Akses & Tim", path: "/dashboard/pengaturan/akses-dan-tim" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check if any settings submenu is active
|
// Check if any settings submenu is active
|
||||||
@@ -60,30 +64,7 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
return (
|
return (
|
||||||
<Box className={className}>
|
<Box className={className}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Box
|
<Image src={"/logo-desa-plus.png"} width={201} height={84} />
|
||||||
p="md"
|
|
||||||
style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}
|
|
||||||
>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Badge
|
|
||||||
color="dark"
|
|
||||||
variant="filled"
|
|
||||||
size="xl"
|
|
||||||
radius="md"
|
|
||||||
py="xs"
|
|
||||||
px="md"
|
|
||||||
style={{ fontSize: "1.5rem", fontWeight: "bold" }}
|
|
||||||
>
|
|
||||||
DESA
|
|
||||||
</Badge>
|
|
||||||
<Badge color="green" variant="filled" size="md" radius="md">
|
|
||||||
+
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
<Text size="xs" c="dimmed" mt="xs">
|
|
||||||
Digitalisasi Desa Transparansi Kerja
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
@@ -203,5 +184,6 @@ export function Sidebar({ className }: SidebarProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/components/ui/logo-desa-plus.png
Normal file
BIN
src/components/ui/logo-desa-plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
53
src/index.ts
53
src/index.ts
@@ -6,8 +6,22 @@ import { Elysia } from "elysia";
|
|||||||
import api from "./api";
|
import api from "./api";
|
||||||
import { openInEditor } from "./utils/open-in-editor";
|
import { openInEditor } from "./utils/open-in-editor";
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
// Auto-seed database in production (ensure admin user exists)
|
||||||
|
if (isProduction && process.env.ADMIN_EMAIL) {
|
||||||
|
try {
|
||||||
|
console.log("🌱 Running database seed in production...");
|
||||||
|
const { runSeed } = await import("../prisma/seed.ts");
|
||||||
|
await runSeed();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("⚠️ Production seed failed:", error);
|
||||||
|
// Don't crash the server if seed fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const app = new Elysia().use(api);
|
const app = new Elysia().use(api);
|
||||||
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
@@ -81,10 +95,27 @@ if (!isProduction) {
|
|||||||
getHeader(name: string) {
|
getHeader(name: string) {
|
||||||
return this.headers[name.toLowerCase()];
|
return this.headers[name.toLowerCase()];
|
||||||
},
|
},
|
||||||
|
writeHead(code: number, headers: Record<string, string>) {
|
||||||
|
this.statusCode = code;
|
||||||
|
Object.assign(this.headers, headers);
|
||||||
|
},
|
||||||
|
write(chunk: any, callback?: () => void) {
|
||||||
|
// Collect chunks for streaming responses
|
||||||
|
if (!this._chunks) this._chunks = [];
|
||||||
|
this._chunks.push(chunk);
|
||||||
|
if (callback) callback();
|
||||||
|
return true; // Indicate we can accept more data
|
||||||
|
},
|
||||||
headers: {} as Record<string, string>,
|
headers: {} as Record<string, string>,
|
||||||
end(data: any) {
|
end(data: any) {
|
||||||
// Handle potential Buffer or string data from Vite
|
// Handle potential Buffer or string data from Vite
|
||||||
let body = data;
|
let body = data;
|
||||||
|
|
||||||
|
// If we have collected chunks from write() calls, combine them
|
||||||
|
if (this._chunks && this._chunks.length > 0) {
|
||||||
|
body = Buffer.concat(this._chunks);
|
||||||
|
}
|
||||||
|
|
||||||
if (data instanceof Uint8Array) {
|
if (data instanceof Uint8Array) {
|
||||||
body = data;
|
body = data;
|
||||||
} else if (typeof data === "string") {
|
} else if (typeof data === "string") {
|
||||||
@@ -144,6 +175,11 @@ if (!isProduction) {
|
|||||||
if (fs.existsSync(srcPath)) {
|
if (fs.existsSync(srcPath)) {
|
||||||
filePath = srcPath;
|
filePath = srcPath;
|
||||||
}
|
}
|
||||||
|
// Check public folder for static assets
|
||||||
|
const publicPath = path.join("public", pathname);
|
||||||
|
if (fs.existsSync(publicPath)) {
|
||||||
|
filePath = publicPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. If not found and looks like an asset (has extension), try root of dist or src
|
// 2. If not found and looks like an asset (has extension), try root of dist or src
|
||||||
@@ -159,8 +195,18 @@ if (!isProduction) {
|
|||||||
) {
|
) {
|
||||||
filePath = fallbackDistPath;
|
filePath = fallbackDistPath;
|
||||||
}
|
}
|
||||||
|
// Try public folder
|
||||||
|
else {
|
||||||
|
const fallbackPublicPath = path.join("public", filename);
|
||||||
|
if (
|
||||||
|
fs.existsSync(fallbackPublicPath) &&
|
||||||
|
fs.statSync(fallbackPublicPath).isFile()
|
||||||
|
) {
|
||||||
|
filePath = fallbackPublicPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Special handling for PWA files in src
|
// Special handling for PWA files in src
|
||||||
else if (pathname.includes("assetlinks.json")) {
|
if (pathname.includes("assetlinks.json")) {
|
||||||
const srcFilename = pathname.includes("assetlinks.json")
|
const srcFilename = pathname.includes("assetlinks.json")
|
||||||
? ".well-known/assetlinks.json"
|
? ".well-known/assetlinks.json"
|
||||||
: filename;
|
: filename;
|
||||||
@@ -198,10 +244,11 @@ if (!isProduction) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(3000);
|
app.listen(PORT);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`🚀 Server running at http://localhost:3000 in ${isProduction ? "production" : "development"} mode`,
|
`🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`,
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ApiApp = typeof app;
|
export type ApiApp = typeof app;
|
||||||
|
|
||||||
|
|||||||
@@ -60,39 +60,16 @@ type RouteRule = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const routeRules: RouteRule[] = [
|
const routeRules: RouteRule[] = [
|
||||||
// Public routes - no auth required
|
|
||||||
{
|
|
||||||
match: (p) => p === "/" || p === "/signin" || p === "/signup",
|
|
||||||
requireAuth: false,
|
|
||||||
},
|
|
||||||
// Profile routes - auth required for all roles
|
|
||||||
{
|
{
|
||||||
match: (p) => p === "/profile" || p.startsWith("/profile/"),
|
match: (p) => p === "/profile" || p.startsWith("/profile/"),
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
redirectTo: "/signin",
|
redirectTo: "/signin",
|
||||||
},
|
},
|
||||||
// Dashboard and main pages - auth required for all roles (not just admin)
|
|
||||||
{
|
{
|
||||||
match: (p) =>
|
match: (p) => p === "/dashboard" || p.startsWith("/dashboard/"),
|
||||||
p.startsWith("/kinerja-divisi") ||
|
|
||||||
p.startsWith("/pengaduan") ||
|
|
||||||
p.startsWith("/jenna") ||
|
|
||||||
p.startsWith("/demografi") ||
|
|
||||||
p.startsWith("/keuangan") ||
|
|
||||||
p.startsWith("/bumdes") ||
|
|
||||||
p.startsWith("/sosial") ||
|
|
||||||
p.startsWith("/keamanan") ||
|
|
||||||
p.startsWith("/bantuan") ||
|
|
||||||
p.startsWith("/pengaturan"),
|
|
||||||
requireAuth: true,
|
|
||||||
redirectTo: "/signin",
|
|
||||||
},
|
|
||||||
// Admin routes - auth required with admin role only
|
|
||||||
{
|
|
||||||
match: (p) => p.startsWith("/admin"),
|
|
||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
requiredRole: "admin",
|
requiredRole: "admin",
|
||||||
redirectTo: "/signin",
|
redirectTo: "/profile",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -121,22 +98,15 @@ export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
|
|||||||
location: { pathname: string; href: string };
|
location: { pathname: string; href: string };
|
||||||
}) => {
|
}) => {
|
||||||
const rule = findRouteRule(location.pathname);
|
const rule = findRouteRule(location.pathname);
|
||||||
|
|
||||||
// If no rule matches, allow access by default
|
|
||||||
if (!rule) return;
|
if (!rule) return;
|
||||||
|
|
||||||
// If route explicitly doesn't require auth, allow access
|
|
||||||
if (rule.requireAuth === false) return;
|
|
||||||
|
|
||||||
const session = await fetchSession();
|
const session = await fetchSession();
|
||||||
const user = session?.user;
|
const user = session?.user;
|
||||||
|
|
||||||
// If auth is required but user is not logged in, redirect to login
|
|
||||||
if (rule.requireAuth && !user) {
|
if (rule.requireAuth && !user) {
|
||||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If specific role is required, check it
|
|
||||||
if (rule.requiredRole && user?.role !== rule.requiredRole) {
|
if (rule.requiredRole && user?.role !== rule.requiredRole) {
|
||||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,38 +9,35 @@
|
|||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as SosialRouteImport } from './routes/sosial'
|
|
||||||
import { Route as SignupRouteImport } from './routes/signup'
|
import { Route as SignupRouteImport } from './routes/signup'
|
||||||
import { Route as SigninRouteImport } from './routes/signin'
|
import { Route as SigninRouteImport } from './routes/signin'
|
||||||
import { Route as PengaduanLayananPublikRouteImport } from './routes/pengaduan-layanan-publik'
|
import { Route as DashboardRouteRouteImport } from './routes/dashboard/route'
|
||||||
import { Route as KinerjaDivisiRouteImport } from './routes/kinerja-divisi'
|
|
||||||
import { Route as KeuanganAnggaranRouteImport } from './routes/keuangan-anggaran'
|
|
||||||
import { Route as KeamananRouteImport } from './routes/keamanan'
|
|
||||||
import { Route as JennaAnalyticRouteImport } from './routes/jenna-analytic'
|
|
||||||
import { Route as DemografiPekerjaanRouteImport } from './routes/demografi-pekerjaan'
|
|
||||||
import { Route as BumdesRouteImport } from './routes/bumdes'
|
|
||||||
import { Route as BantuanRouteImport } from './routes/bantuan'
|
|
||||||
import { Route as PengaturanRouteRouteImport } from './routes/pengaturan/route'
|
|
||||||
import { Route as AdminRouteRouteImport } from './routes/admin/route'
|
import { Route as AdminRouteRouteImport } from './routes/admin/route'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as UsersIndexRouteImport } from './routes/users/index'
|
import { Route as UsersIndexRouteImport } from './routes/users/index'
|
||||||
import { Route as ProfileIndexRouteImport } from './routes/profile/index'
|
import { Route as ProfileIndexRouteImport } from './routes/profile/index'
|
||||||
|
import { Route as DashboardIndexRouteImport } from './routes/dashboard/index'
|
||||||
import { Route as AdminIndexRouteImport } from './routes/admin/index'
|
import { Route as AdminIndexRouteImport } from './routes/admin/index'
|
||||||
import { Route as UsersIdRouteImport } from './routes/users/$id'
|
import { Route as UsersIdRouteImport } from './routes/users/$id'
|
||||||
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
|
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
|
||||||
import { Route as PengaturanUmumRouteImport } from './routes/pengaturan/umum'
|
import { Route as DashboardSosialRouteImport } from './routes/dashboard/sosial'
|
||||||
import { Route as PengaturanNotifikasiRouteImport } from './routes/pengaturan/notifikasi'
|
import { Route as DashboardPengaduanLayananPublikRouteImport } from './routes/dashboard/pengaduan-layanan-publik'
|
||||||
import { Route as PengaturanKeamananRouteImport } from './routes/pengaturan/keamanan'
|
import { Route as DashboardKinerjaDivisiRouteImport } from './routes/dashboard/kinerja-divisi'
|
||||||
import { Route as PengaturanAksesDanTimRouteImport } from './routes/pengaturan/akses-dan-tim'
|
import { Route as DashboardKeuanganAnggaranRouteImport } from './routes/dashboard/keuangan-anggaran'
|
||||||
|
import { Route as DashboardKeamananRouteImport } from './routes/dashboard/keamanan'
|
||||||
|
import { Route as DashboardJennaAnalyticRouteImport } from './routes/dashboard/jenna-analytic'
|
||||||
|
import { Route as DashboardDemografiPekerjaanRouteImport } from './routes/dashboard/demografi-pekerjaan'
|
||||||
|
import { Route as DashboardBumdesRouteImport } from './routes/dashboard/bumdes'
|
||||||
|
import { Route as DashboardBantuanRouteImport } from './routes/dashboard/bantuan'
|
||||||
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
||||||
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
|
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
|
||||||
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
|
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
|
||||||
|
import { Route as DashboardPengaturanRouteRouteImport } from './routes/dashboard/pengaturan/route'
|
||||||
|
import { Route as DashboardPengaturanUmumRouteImport } from './routes/dashboard/pengaturan/umum'
|
||||||
|
import { Route as DashboardPengaturanNotifikasiRouteImport } from './routes/dashboard/pengaturan/notifikasi'
|
||||||
|
import { Route as DashboardPengaturanKeamananRouteImport } from './routes/dashboard/pengaturan/keamanan'
|
||||||
|
import { Route as DashboardPengaturanAksesDanTimRouteImport } from './routes/dashboard/pengaturan/akses-dan-tim'
|
||||||
|
|
||||||
const SosialRoute = SosialRouteImport.update({
|
|
||||||
id: '/sosial',
|
|
||||||
path: '/sosial',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const SignupRoute = SignupRouteImport.update({
|
const SignupRoute = SignupRouteImport.update({
|
||||||
id: '/signup',
|
id: '/signup',
|
||||||
path: '/signup',
|
path: '/signup',
|
||||||
@@ -51,49 +48,9 @@ const SigninRoute = SigninRouteImport.update({
|
|||||||
path: '/signin',
|
path: '/signin',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const PengaduanLayananPublikRoute = PengaduanLayananPublikRouteImport.update({
|
const DashboardRouteRoute = DashboardRouteRouteImport.update({
|
||||||
id: '/pengaduan-layanan-publik',
|
id: '/dashboard',
|
||||||
path: '/pengaduan-layanan-publik',
|
path: '/dashboard',
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const KinerjaDivisiRoute = KinerjaDivisiRouteImport.update({
|
|
||||||
id: '/kinerja-divisi',
|
|
||||||
path: '/kinerja-divisi',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const KeuanganAnggaranRoute = KeuanganAnggaranRouteImport.update({
|
|
||||||
id: '/keuangan-anggaran',
|
|
||||||
path: '/keuangan-anggaran',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const KeamananRoute = KeamananRouteImport.update({
|
|
||||||
id: '/keamanan',
|
|
||||||
path: '/keamanan',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const JennaAnalyticRoute = JennaAnalyticRouteImport.update({
|
|
||||||
id: '/jenna-analytic',
|
|
||||||
path: '/jenna-analytic',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const DemografiPekerjaanRoute = DemografiPekerjaanRouteImport.update({
|
|
||||||
id: '/demografi-pekerjaan',
|
|
||||||
path: '/demografi-pekerjaan',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const BumdesRoute = BumdesRouteImport.update({
|
|
||||||
id: '/bumdes',
|
|
||||||
path: '/bumdes',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const BantuanRoute = BantuanRouteImport.update({
|
|
||||||
id: '/bantuan',
|
|
||||||
path: '/bantuan',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const PengaturanRouteRoute = PengaturanRouteRouteImport.update({
|
|
||||||
id: '/pengaturan',
|
|
||||||
path: '/pengaturan',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AdminRouteRoute = AdminRouteRouteImport.update({
|
const AdminRouteRoute = AdminRouteRouteImport.update({
|
||||||
@@ -116,6 +73,11 @@ const ProfileIndexRoute = ProfileIndexRouteImport.update({
|
|||||||
path: '/profile/',
|
path: '/profile/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const DashboardIndexRoute = DashboardIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
const AdminIndexRoute = AdminIndexRouteImport.update({
|
const AdminIndexRoute = AdminIndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -131,25 +93,53 @@ const ProfileEditRoute = ProfileEditRouteImport.update({
|
|||||||
path: '/profile/edit',
|
path: '/profile/edit',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const PengaturanUmumRoute = PengaturanUmumRouteImport.update({
|
const DashboardSosialRoute = DashboardSosialRouteImport.update({
|
||||||
id: '/umum',
|
id: '/sosial',
|
||||||
path: '/umum',
|
path: '/sosial',
|
||||||
getParentRoute: () => PengaturanRouteRoute,
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const PengaturanNotifikasiRoute = PengaturanNotifikasiRouteImport.update({
|
const DashboardPengaduanLayananPublikRoute =
|
||||||
id: '/notifikasi',
|
DashboardPengaduanLayananPublikRouteImport.update({
|
||||||
path: '/notifikasi',
|
id: '/pengaduan-layanan-publik',
|
||||||
getParentRoute: () => PengaturanRouteRoute,
|
path: '/pengaduan-layanan-publik',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardKinerjaDivisiRoute = DashboardKinerjaDivisiRouteImport.update({
|
||||||
|
id: '/kinerja-divisi',
|
||||||
|
path: '/kinerja-divisi',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const PengaturanKeamananRoute = PengaturanKeamananRouteImport.update({
|
const DashboardKeuanganAnggaranRoute =
|
||||||
|
DashboardKeuanganAnggaranRouteImport.update({
|
||||||
|
id: '/keuangan-anggaran',
|
||||||
|
path: '/keuangan-anggaran',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardKeamananRoute = DashboardKeamananRouteImport.update({
|
||||||
id: '/keamanan',
|
id: '/keamanan',
|
||||||
path: '/keamanan',
|
path: '/keamanan',
|
||||||
getParentRoute: () => PengaturanRouteRoute,
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const PengaturanAksesDanTimRoute = PengaturanAksesDanTimRouteImport.update({
|
const DashboardJennaAnalyticRoute = DashboardJennaAnalyticRouteImport.update({
|
||||||
id: '/akses-dan-tim',
|
id: '/jenna-analytic',
|
||||||
path: '/akses-dan-tim',
|
path: '/jenna-analytic',
|
||||||
getParentRoute: () => PengaturanRouteRoute,
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardDemografiPekerjaanRoute =
|
||||||
|
DashboardDemografiPekerjaanRouteImport.update({
|
||||||
|
id: '/demografi-pekerjaan',
|
||||||
|
path: '/demografi-pekerjaan',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardBumdesRoute = DashboardBumdesRouteImport.update({
|
||||||
|
id: '/bumdes',
|
||||||
|
path: '/bumdes',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardBantuanRoute = DashboardBantuanRouteImport.update({
|
||||||
|
id: '/bantuan',
|
||||||
|
path: '/bantuan',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AdminUsersRoute = AdminUsersRouteImport.update({
|
const AdminUsersRoute = AdminUsersRouteImport.update({
|
||||||
id: '/users',
|
id: '/users',
|
||||||
@@ -166,192 +156,222 @@ const AdminApikeyRoute = AdminApikeyRouteImport.update({
|
|||||||
path: '/apikey',
|
path: '/apikey',
|
||||||
getParentRoute: () => AdminRouteRoute,
|
getParentRoute: () => AdminRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const DashboardPengaturanRouteRoute =
|
||||||
|
DashboardPengaturanRouteRouteImport.update({
|
||||||
|
id: '/pengaturan',
|
||||||
|
path: '/pengaturan',
|
||||||
|
getParentRoute: () => DashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardPengaturanUmumRoute = DashboardPengaturanUmumRouteImport.update({
|
||||||
|
id: '/umum',
|
||||||
|
path: '/umum',
|
||||||
|
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardPengaturanNotifikasiRoute =
|
||||||
|
DashboardPengaturanNotifikasiRouteImport.update({
|
||||||
|
id: '/notifikasi',
|
||||||
|
path: '/notifikasi',
|
||||||
|
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardPengaturanKeamananRoute =
|
||||||
|
DashboardPengaturanKeamananRouteImport.update({
|
||||||
|
id: '/keamanan',
|
||||||
|
path: '/keamanan',
|
||||||
|
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const DashboardPengaturanAksesDanTimRoute =
|
||||||
|
DashboardPengaturanAksesDanTimRouteImport.update({
|
||||||
|
id: '/akses-dan-tim',
|
||||||
|
path: '/akses-dan-tim',
|
||||||
|
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin': typeof AdminRouteRouteWithChildren
|
'/admin': typeof AdminRouteRouteWithChildren
|
||||||
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
'/dashboard': typeof DashboardRouteRouteWithChildren
|
||||||
'/bantuan': typeof BantuanRoute
|
|
||||||
'/bumdes': typeof BumdesRoute
|
|
||||||
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
|
||||||
'/jenna-analytic': typeof JennaAnalyticRoute
|
|
||||||
'/keamanan': typeof KeamananRoute
|
|
||||||
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
|
||||||
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
|
||||||
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
|
||||||
'/signin': typeof SigninRoute
|
'/signin': typeof SigninRoute
|
||||||
'/signup': typeof SignupRoute
|
'/signup': typeof SignupRoute
|
||||||
'/sosial': typeof SosialRoute
|
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
||||||
'/admin/apikey': typeof AdminApikeyRoute
|
'/admin/apikey': typeof AdminApikeyRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
'/admin/users': typeof AdminUsersRoute
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
||||||
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
||||||
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
||||||
'/pengaturan/umum': typeof PengaturanUmumRoute
|
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
||||||
|
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
||||||
|
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
||||||
|
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
||||||
|
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
||||||
|
'/dashboard/sosial': typeof DashboardSosialRoute
|
||||||
'/profile/edit': typeof ProfileEditRoute
|
'/profile/edit': typeof ProfileEditRoute
|
||||||
'/users/$id': typeof UsersIdRoute
|
'/users/$id': typeof UsersIdRoute
|
||||||
'/admin/': typeof AdminIndexRoute
|
'/admin/': typeof AdminIndexRoute
|
||||||
|
'/dashboard/': typeof DashboardIndexRoute
|
||||||
'/profile/': typeof ProfileIndexRoute
|
'/profile/': typeof ProfileIndexRoute
|
||||||
'/users/': typeof UsersIndexRoute
|
'/users/': typeof UsersIndexRoute
|
||||||
|
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
||||||
|
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
||||||
|
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
||||||
|
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
|
||||||
'/bantuan': typeof BantuanRoute
|
|
||||||
'/bumdes': typeof BumdesRoute
|
|
||||||
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
|
||||||
'/jenna-analytic': typeof JennaAnalyticRoute
|
|
||||||
'/keamanan': typeof KeamananRoute
|
|
||||||
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
|
||||||
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
|
||||||
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
|
||||||
'/signin': typeof SigninRoute
|
'/signin': typeof SigninRoute
|
||||||
'/signup': typeof SignupRoute
|
'/signup': typeof SignupRoute
|
||||||
'/sosial': typeof SosialRoute
|
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
||||||
'/admin/apikey': typeof AdminApikeyRoute
|
'/admin/apikey': typeof AdminApikeyRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
'/admin/users': typeof AdminUsersRoute
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
||||||
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
||||||
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
||||||
'/pengaturan/umum': typeof PengaturanUmumRoute
|
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
||||||
|
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
||||||
|
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
||||||
|
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
||||||
|
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
||||||
|
'/dashboard/sosial': typeof DashboardSosialRoute
|
||||||
'/profile/edit': typeof ProfileEditRoute
|
'/profile/edit': typeof ProfileEditRoute
|
||||||
'/users/$id': typeof UsersIdRoute
|
'/users/$id': typeof UsersIdRoute
|
||||||
'/admin': typeof AdminIndexRoute
|
'/admin': typeof AdminIndexRoute
|
||||||
|
'/dashboard': typeof DashboardIndexRoute
|
||||||
'/profile': typeof ProfileIndexRoute
|
'/profile': typeof ProfileIndexRoute
|
||||||
'/users': typeof UsersIndexRoute
|
'/users': typeof UsersIndexRoute
|
||||||
|
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
||||||
|
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
||||||
|
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
||||||
|
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin': typeof AdminRouteRouteWithChildren
|
'/admin': typeof AdminRouteRouteWithChildren
|
||||||
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
'/dashboard': typeof DashboardRouteRouteWithChildren
|
||||||
'/bantuan': typeof BantuanRoute
|
|
||||||
'/bumdes': typeof BumdesRoute
|
|
||||||
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
|
||||||
'/jenna-analytic': typeof JennaAnalyticRoute
|
|
||||||
'/keamanan': typeof KeamananRoute
|
|
||||||
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
|
||||||
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
|
||||||
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
|
||||||
'/signin': typeof SigninRoute
|
'/signin': typeof SigninRoute
|
||||||
'/signup': typeof SignupRoute
|
'/signup': typeof SignupRoute
|
||||||
'/sosial': typeof SosialRoute
|
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
||||||
'/admin/apikey': typeof AdminApikeyRoute
|
'/admin/apikey': typeof AdminApikeyRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/admin/settings': typeof AdminSettingsRoute
|
||||||
'/admin/users': typeof AdminUsersRoute
|
'/admin/users': typeof AdminUsersRoute
|
||||||
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
||||||
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
||||||
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
||||||
'/pengaturan/umum': typeof PengaturanUmumRoute
|
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
||||||
|
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
||||||
|
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
||||||
|
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
||||||
|
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
||||||
|
'/dashboard/sosial': typeof DashboardSosialRoute
|
||||||
'/profile/edit': typeof ProfileEditRoute
|
'/profile/edit': typeof ProfileEditRoute
|
||||||
'/users/$id': typeof UsersIdRoute
|
'/users/$id': typeof UsersIdRoute
|
||||||
'/admin/': typeof AdminIndexRoute
|
'/admin/': typeof AdminIndexRoute
|
||||||
|
'/dashboard/': typeof DashboardIndexRoute
|
||||||
'/profile/': typeof ProfileIndexRoute
|
'/profile/': typeof ProfileIndexRoute
|
||||||
'/users/': typeof UsersIndexRoute
|
'/users/': typeof UsersIndexRoute
|
||||||
|
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
||||||
|
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
||||||
|
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
||||||
|
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/pengaturan'
|
| '/dashboard'
|
||||||
| '/bantuan'
|
|
||||||
| '/bumdes'
|
|
||||||
| '/demografi-pekerjaan'
|
|
||||||
| '/jenna-analytic'
|
|
||||||
| '/keamanan'
|
|
||||||
| '/keuangan-anggaran'
|
|
||||||
| '/kinerja-divisi'
|
|
||||||
| '/pengaduan-layanan-publik'
|
|
||||||
| '/signin'
|
| '/signin'
|
||||||
| '/signup'
|
| '/signup'
|
||||||
| '/sosial'
|
| '/dashboard/pengaturan'
|
||||||
| '/admin/apikey'
|
| '/admin/apikey'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
| '/admin/users'
|
| '/admin/users'
|
||||||
| '/pengaturan/akses-dan-tim'
|
| '/dashboard/bantuan'
|
||||||
| '/pengaturan/keamanan'
|
| '/dashboard/bumdes'
|
||||||
| '/pengaturan/notifikasi'
|
| '/dashboard/demografi-pekerjaan'
|
||||||
| '/pengaturan/umum'
|
| '/dashboard/jenna-analytic'
|
||||||
|
| '/dashboard/keamanan'
|
||||||
|
| '/dashboard/keuangan-anggaran'
|
||||||
|
| '/dashboard/kinerja-divisi'
|
||||||
|
| '/dashboard/pengaduan-layanan-publik'
|
||||||
|
| '/dashboard/sosial'
|
||||||
| '/profile/edit'
|
| '/profile/edit'
|
||||||
| '/users/$id'
|
| '/users/$id'
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
|
| '/dashboard/'
|
||||||
| '/profile/'
|
| '/profile/'
|
||||||
| '/users/'
|
| '/users/'
|
||||||
|
| '/dashboard/pengaturan/akses-dan-tim'
|
||||||
|
| '/dashboard/pengaturan/keamanan'
|
||||||
|
| '/dashboard/pengaturan/notifikasi'
|
||||||
|
| '/dashboard/pengaturan/umum'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/pengaturan'
|
|
||||||
| '/bantuan'
|
|
||||||
| '/bumdes'
|
|
||||||
| '/demografi-pekerjaan'
|
|
||||||
| '/jenna-analytic'
|
|
||||||
| '/keamanan'
|
|
||||||
| '/keuangan-anggaran'
|
|
||||||
| '/kinerja-divisi'
|
|
||||||
| '/pengaduan-layanan-publik'
|
|
||||||
| '/signin'
|
| '/signin'
|
||||||
| '/signup'
|
| '/signup'
|
||||||
| '/sosial'
|
| '/dashboard/pengaturan'
|
||||||
| '/admin/apikey'
|
| '/admin/apikey'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
| '/admin/users'
|
| '/admin/users'
|
||||||
| '/pengaturan/akses-dan-tim'
|
| '/dashboard/bantuan'
|
||||||
| '/pengaturan/keamanan'
|
| '/dashboard/bumdes'
|
||||||
| '/pengaturan/notifikasi'
|
| '/dashboard/demografi-pekerjaan'
|
||||||
| '/pengaturan/umum'
|
| '/dashboard/jenna-analytic'
|
||||||
|
| '/dashboard/keamanan'
|
||||||
|
| '/dashboard/keuangan-anggaran'
|
||||||
|
| '/dashboard/kinerja-divisi'
|
||||||
|
| '/dashboard/pengaduan-layanan-publik'
|
||||||
|
| '/dashboard/sosial'
|
||||||
| '/profile/edit'
|
| '/profile/edit'
|
||||||
| '/users/$id'
|
| '/users/$id'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
|
| '/dashboard'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/users'
|
| '/users'
|
||||||
|
| '/dashboard/pengaturan/akses-dan-tim'
|
||||||
|
| '/dashboard/pengaturan/keamanan'
|
||||||
|
| '/dashboard/pengaturan/notifikasi'
|
||||||
|
| '/dashboard/pengaturan/umum'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/pengaturan'
|
| '/dashboard'
|
||||||
| '/bantuan'
|
|
||||||
| '/bumdes'
|
|
||||||
| '/demografi-pekerjaan'
|
|
||||||
| '/jenna-analytic'
|
|
||||||
| '/keamanan'
|
|
||||||
| '/keuangan-anggaran'
|
|
||||||
| '/kinerja-divisi'
|
|
||||||
| '/pengaduan-layanan-publik'
|
|
||||||
| '/signin'
|
| '/signin'
|
||||||
| '/signup'
|
| '/signup'
|
||||||
| '/sosial'
|
| '/dashboard/pengaturan'
|
||||||
| '/admin/apikey'
|
| '/admin/apikey'
|
||||||
| '/admin/settings'
|
| '/admin/settings'
|
||||||
| '/admin/users'
|
| '/admin/users'
|
||||||
| '/pengaturan/akses-dan-tim'
|
| '/dashboard/bantuan'
|
||||||
| '/pengaturan/keamanan'
|
| '/dashboard/bumdes'
|
||||||
| '/pengaturan/notifikasi'
|
| '/dashboard/demografi-pekerjaan'
|
||||||
| '/pengaturan/umum'
|
| '/dashboard/jenna-analytic'
|
||||||
|
| '/dashboard/keamanan'
|
||||||
|
| '/dashboard/keuangan-anggaran'
|
||||||
|
| '/dashboard/kinerja-divisi'
|
||||||
|
| '/dashboard/pengaduan-layanan-publik'
|
||||||
|
| '/dashboard/sosial'
|
||||||
| '/profile/edit'
|
| '/profile/edit'
|
||||||
| '/users/$id'
|
| '/users/$id'
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
|
| '/dashboard/'
|
||||||
| '/profile/'
|
| '/profile/'
|
||||||
| '/users/'
|
| '/users/'
|
||||||
|
| '/dashboard/pengaturan/akses-dan-tim'
|
||||||
|
| '/dashboard/pengaturan/keamanan'
|
||||||
|
| '/dashboard/pengaturan/notifikasi'
|
||||||
|
| '/dashboard/pengaturan/umum'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AdminRouteRoute: typeof AdminRouteRouteWithChildren
|
AdminRouteRoute: typeof AdminRouteRouteWithChildren
|
||||||
PengaturanRouteRoute: typeof PengaturanRouteRouteWithChildren
|
DashboardRouteRoute: typeof DashboardRouteRouteWithChildren
|
||||||
BantuanRoute: typeof BantuanRoute
|
|
||||||
BumdesRoute: typeof BumdesRoute
|
|
||||||
DemografiPekerjaanRoute: typeof DemografiPekerjaanRoute
|
|
||||||
JennaAnalyticRoute: typeof JennaAnalyticRoute
|
|
||||||
KeamananRoute: typeof KeamananRoute
|
|
||||||
KeuanganAnggaranRoute: typeof KeuanganAnggaranRoute
|
|
||||||
KinerjaDivisiRoute: typeof KinerjaDivisiRoute
|
|
||||||
PengaduanLayananPublikRoute: typeof PengaduanLayananPublikRoute
|
|
||||||
SigninRoute: typeof SigninRoute
|
SigninRoute: typeof SigninRoute
|
||||||
SignupRoute: typeof SignupRoute
|
SignupRoute: typeof SignupRoute
|
||||||
SosialRoute: typeof SosialRoute
|
|
||||||
ProfileEditRoute: typeof ProfileEditRoute
|
ProfileEditRoute: typeof ProfileEditRoute
|
||||||
UsersIdRoute: typeof UsersIdRoute
|
UsersIdRoute: typeof UsersIdRoute
|
||||||
ProfileIndexRoute: typeof ProfileIndexRoute
|
ProfileIndexRoute: typeof ProfileIndexRoute
|
||||||
@@ -360,13 +380,6 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
'/sosial': {
|
|
||||||
id: '/sosial'
|
|
||||||
path: '/sosial'
|
|
||||||
fullPath: '/sosial'
|
|
||||||
preLoaderRoute: typeof SosialRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/signup': {
|
'/signup': {
|
||||||
id: '/signup'
|
id: '/signup'
|
||||||
path: '/signup'
|
path: '/signup'
|
||||||
@@ -381,67 +394,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SigninRouteImport
|
preLoaderRoute: typeof SigninRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/pengaduan-layanan-publik': {
|
'/dashboard': {
|
||||||
id: '/pengaduan-layanan-publik'
|
id: '/dashboard'
|
||||||
path: '/pengaduan-layanan-publik'
|
path: '/dashboard'
|
||||||
fullPath: '/pengaduan-layanan-publik'
|
fullPath: '/dashboard'
|
||||||
preLoaderRoute: typeof PengaduanLayananPublikRouteImport
|
preLoaderRoute: typeof DashboardRouteRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/kinerja-divisi': {
|
|
||||||
id: '/kinerja-divisi'
|
|
||||||
path: '/kinerja-divisi'
|
|
||||||
fullPath: '/kinerja-divisi'
|
|
||||||
preLoaderRoute: typeof KinerjaDivisiRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/keuangan-anggaran': {
|
|
||||||
id: '/keuangan-anggaran'
|
|
||||||
path: '/keuangan-anggaran'
|
|
||||||
fullPath: '/keuangan-anggaran'
|
|
||||||
preLoaderRoute: typeof KeuanganAnggaranRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/keamanan': {
|
|
||||||
id: '/keamanan'
|
|
||||||
path: '/keamanan'
|
|
||||||
fullPath: '/keamanan'
|
|
||||||
preLoaderRoute: typeof KeamananRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/jenna-analytic': {
|
|
||||||
id: '/jenna-analytic'
|
|
||||||
path: '/jenna-analytic'
|
|
||||||
fullPath: '/jenna-analytic'
|
|
||||||
preLoaderRoute: typeof JennaAnalyticRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/demografi-pekerjaan': {
|
|
||||||
id: '/demografi-pekerjaan'
|
|
||||||
path: '/demografi-pekerjaan'
|
|
||||||
fullPath: '/demografi-pekerjaan'
|
|
||||||
preLoaderRoute: typeof DemografiPekerjaanRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/bumdes': {
|
|
||||||
id: '/bumdes'
|
|
||||||
path: '/bumdes'
|
|
||||||
fullPath: '/bumdes'
|
|
||||||
preLoaderRoute: typeof BumdesRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/bantuan': {
|
|
||||||
id: '/bantuan'
|
|
||||||
path: '/bantuan'
|
|
||||||
fullPath: '/bantuan'
|
|
||||||
preLoaderRoute: typeof BantuanRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/pengaturan': {
|
|
||||||
id: '/pengaturan'
|
|
||||||
path: '/pengaturan'
|
|
||||||
fullPath: '/pengaturan'
|
|
||||||
preLoaderRoute: typeof PengaturanRouteRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/admin': {
|
'/admin': {
|
||||||
@@ -472,6 +429,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProfileIndexRouteImport
|
preLoaderRoute: typeof ProfileIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/dashboard/': {
|
||||||
|
id: '/dashboard/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/dashboard/'
|
||||||
|
preLoaderRoute: typeof DashboardIndexRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteRoute
|
||||||
|
}
|
||||||
'/admin/': {
|
'/admin/': {
|
||||||
id: '/admin/'
|
id: '/admin/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -493,33 +457,68 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ProfileEditRouteImport
|
preLoaderRoute: typeof ProfileEditRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/pengaturan/umum': {
|
'/dashboard/sosial': {
|
||||||
id: '/pengaturan/umum'
|
id: '/dashboard/sosial'
|
||||||
path: '/umum'
|
path: '/sosial'
|
||||||
fullPath: '/pengaturan/umum'
|
fullPath: '/dashboard/sosial'
|
||||||
preLoaderRoute: typeof PengaturanUmumRouteImport
|
preLoaderRoute: typeof DashboardSosialRouteImport
|
||||||
parentRoute: typeof PengaturanRouteRoute
|
parentRoute: typeof DashboardRouteRoute
|
||||||
}
|
}
|
||||||
'/pengaturan/notifikasi': {
|
'/dashboard/pengaduan-layanan-publik': {
|
||||||
id: '/pengaturan/notifikasi'
|
id: '/dashboard/pengaduan-layanan-publik'
|
||||||
path: '/notifikasi'
|
path: '/pengaduan-layanan-publik'
|
||||||
fullPath: '/pengaturan/notifikasi'
|
fullPath: '/dashboard/pengaduan-layanan-publik'
|
||||||
preLoaderRoute: typeof PengaturanNotifikasiRouteImport
|
preLoaderRoute: typeof DashboardPengaduanLayananPublikRouteImport
|
||||||
parentRoute: typeof PengaturanRouteRoute
|
parentRoute: typeof DashboardRouteRoute
|
||||||
}
|
}
|
||||||
'/pengaturan/keamanan': {
|
'/dashboard/kinerja-divisi': {
|
||||||
id: '/pengaturan/keamanan'
|
id: '/dashboard/kinerja-divisi'
|
||||||
|
path: '/kinerja-divisi'
|
||||||
|
fullPath: '/dashboard/kinerja-divisi'
|
||||||
|
preLoaderRoute: typeof DashboardKinerjaDivisiRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/keuangan-anggaran': {
|
||||||
|
id: '/dashboard/keuangan-anggaran'
|
||||||
|
path: '/keuangan-anggaran'
|
||||||
|
fullPath: '/dashboard/keuangan-anggaran'
|
||||||
|
preLoaderRoute: typeof DashboardKeuanganAnggaranRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/keamanan': {
|
||||||
|
id: '/dashboard/keamanan'
|
||||||
path: '/keamanan'
|
path: '/keamanan'
|
||||||
fullPath: '/pengaturan/keamanan'
|
fullPath: '/dashboard/keamanan'
|
||||||
preLoaderRoute: typeof PengaturanKeamananRouteImport
|
preLoaderRoute: typeof DashboardKeamananRouteImport
|
||||||
parentRoute: typeof PengaturanRouteRoute
|
parentRoute: typeof DashboardRouteRoute
|
||||||
}
|
}
|
||||||
'/pengaturan/akses-dan-tim': {
|
'/dashboard/jenna-analytic': {
|
||||||
id: '/pengaturan/akses-dan-tim'
|
id: '/dashboard/jenna-analytic'
|
||||||
path: '/akses-dan-tim'
|
path: '/jenna-analytic'
|
||||||
fullPath: '/pengaturan/akses-dan-tim'
|
fullPath: '/dashboard/jenna-analytic'
|
||||||
preLoaderRoute: typeof PengaturanAksesDanTimRouteImport
|
preLoaderRoute: typeof DashboardJennaAnalyticRouteImport
|
||||||
parentRoute: typeof PengaturanRouteRoute
|
parentRoute: typeof DashboardRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/demografi-pekerjaan': {
|
||||||
|
id: '/dashboard/demografi-pekerjaan'
|
||||||
|
path: '/demografi-pekerjaan'
|
||||||
|
fullPath: '/dashboard/demografi-pekerjaan'
|
||||||
|
preLoaderRoute: typeof DashboardDemografiPekerjaanRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/bumdes': {
|
||||||
|
id: '/dashboard/bumdes'
|
||||||
|
path: '/bumdes'
|
||||||
|
fullPath: '/dashboard/bumdes'
|
||||||
|
preLoaderRoute: typeof DashboardBumdesRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/bantuan': {
|
||||||
|
id: '/dashboard/bantuan'
|
||||||
|
path: '/bantuan'
|
||||||
|
fullPath: '/dashboard/bantuan'
|
||||||
|
preLoaderRoute: typeof DashboardBantuanRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteRoute
|
||||||
}
|
}
|
||||||
'/admin/users': {
|
'/admin/users': {
|
||||||
id: '/admin/users'
|
id: '/admin/users'
|
||||||
@@ -542,6 +541,41 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AdminApikeyRouteImport
|
preLoaderRoute: typeof AdminApikeyRouteImport
|
||||||
parentRoute: typeof AdminRouteRoute
|
parentRoute: typeof AdminRouteRoute
|
||||||
}
|
}
|
||||||
|
'/dashboard/pengaturan': {
|
||||||
|
id: '/dashboard/pengaturan'
|
||||||
|
path: '/pengaturan'
|
||||||
|
fullPath: '/dashboard/pengaturan'
|
||||||
|
preLoaderRoute: typeof DashboardPengaturanRouteRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/pengaturan/umum': {
|
||||||
|
id: '/dashboard/pengaturan/umum'
|
||||||
|
path: '/umum'
|
||||||
|
fullPath: '/dashboard/pengaturan/umum'
|
||||||
|
preLoaderRoute: typeof DashboardPengaturanUmumRouteImport
|
||||||
|
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/pengaturan/notifikasi': {
|
||||||
|
id: '/dashboard/pengaturan/notifikasi'
|
||||||
|
path: '/notifikasi'
|
||||||
|
fullPath: '/dashboard/pengaturan/notifikasi'
|
||||||
|
preLoaderRoute: typeof DashboardPengaturanNotifikasiRouteImport
|
||||||
|
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/pengaturan/keamanan': {
|
||||||
|
id: '/dashboard/pengaturan/keamanan'
|
||||||
|
path: '/keamanan'
|
||||||
|
fullPath: '/dashboard/pengaturan/keamanan'
|
||||||
|
preLoaderRoute: typeof DashboardPengaturanKeamananRouteImport
|
||||||
|
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||||
|
}
|
||||||
|
'/dashboard/pengaturan/akses-dan-tim': {
|
||||||
|
id: '/dashboard/pengaturan/akses-dan-tim'
|
||||||
|
path: '/akses-dan-tim'
|
||||||
|
fullPath: '/dashboard/pengaturan/akses-dan-tim'
|
||||||
|
preLoaderRoute: typeof DashboardPengaturanAksesDanTimRouteImport
|
||||||
|
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,39 +597,64 @@ const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren(
|
|||||||
AdminRouteRouteChildren,
|
AdminRouteRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface PengaturanRouteRouteChildren {
|
interface DashboardPengaturanRouteRouteChildren {
|
||||||
PengaturanAksesDanTimRoute: typeof PengaturanAksesDanTimRoute
|
DashboardPengaturanAksesDanTimRoute: typeof DashboardPengaturanAksesDanTimRoute
|
||||||
PengaturanKeamananRoute: typeof PengaturanKeamananRoute
|
DashboardPengaturanKeamananRoute: typeof DashboardPengaturanKeamananRoute
|
||||||
PengaturanNotifikasiRoute: typeof PengaturanNotifikasiRoute
|
DashboardPengaturanNotifikasiRoute: typeof DashboardPengaturanNotifikasiRoute
|
||||||
PengaturanUmumRoute: typeof PengaturanUmumRoute
|
DashboardPengaturanUmumRoute: typeof DashboardPengaturanUmumRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const PengaturanRouteRouteChildren: PengaturanRouteRouteChildren = {
|
const DashboardPengaturanRouteRouteChildren: DashboardPengaturanRouteRouteChildren =
|
||||||
PengaturanAksesDanTimRoute: PengaturanAksesDanTimRoute,
|
{
|
||||||
PengaturanKeamananRoute: PengaturanKeamananRoute,
|
DashboardPengaturanAksesDanTimRoute: DashboardPengaturanAksesDanTimRoute,
|
||||||
PengaturanNotifikasiRoute: PengaturanNotifikasiRoute,
|
DashboardPengaturanKeamananRoute: DashboardPengaturanKeamananRoute,
|
||||||
PengaturanUmumRoute: PengaturanUmumRoute,
|
DashboardPengaturanNotifikasiRoute: DashboardPengaturanNotifikasiRoute,
|
||||||
|
DashboardPengaturanUmumRoute: DashboardPengaturanUmumRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardPengaturanRouteRouteWithChildren =
|
||||||
|
DashboardPengaturanRouteRoute._addFileChildren(
|
||||||
|
DashboardPengaturanRouteRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface DashboardRouteRouteChildren {
|
||||||
|
DashboardPengaturanRouteRoute: typeof DashboardPengaturanRouteRouteWithChildren
|
||||||
|
DashboardBantuanRoute: typeof DashboardBantuanRoute
|
||||||
|
DashboardBumdesRoute: typeof DashboardBumdesRoute
|
||||||
|
DashboardDemografiPekerjaanRoute: typeof DashboardDemografiPekerjaanRoute
|
||||||
|
DashboardJennaAnalyticRoute: typeof DashboardJennaAnalyticRoute
|
||||||
|
DashboardKeamananRoute: typeof DashboardKeamananRoute
|
||||||
|
DashboardKeuanganAnggaranRoute: typeof DashboardKeuanganAnggaranRoute
|
||||||
|
DashboardKinerjaDivisiRoute: typeof DashboardKinerjaDivisiRoute
|
||||||
|
DashboardPengaduanLayananPublikRoute: typeof DashboardPengaduanLayananPublikRoute
|
||||||
|
DashboardSosialRoute: typeof DashboardSosialRoute
|
||||||
|
DashboardIndexRoute: typeof DashboardIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const PengaturanRouteRouteWithChildren = PengaturanRouteRoute._addFileChildren(
|
const DashboardRouteRouteChildren: DashboardRouteRouteChildren = {
|
||||||
PengaturanRouteRouteChildren,
|
DashboardPengaturanRouteRoute: DashboardPengaturanRouteRouteWithChildren,
|
||||||
|
DashboardBantuanRoute: DashboardBantuanRoute,
|
||||||
|
DashboardBumdesRoute: DashboardBumdesRoute,
|
||||||
|
DashboardDemografiPekerjaanRoute: DashboardDemografiPekerjaanRoute,
|
||||||
|
DashboardJennaAnalyticRoute: DashboardJennaAnalyticRoute,
|
||||||
|
DashboardKeamananRoute: DashboardKeamananRoute,
|
||||||
|
DashboardKeuanganAnggaranRoute: DashboardKeuanganAnggaranRoute,
|
||||||
|
DashboardKinerjaDivisiRoute: DashboardKinerjaDivisiRoute,
|
||||||
|
DashboardPengaduanLayananPublikRoute: DashboardPengaduanLayananPublikRoute,
|
||||||
|
DashboardSosialRoute: DashboardSosialRoute,
|
||||||
|
DashboardIndexRoute: DashboardIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren(
|
||||||
|
DashboardRouteRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AdminRouteRoute: AdminRouteRouteWithChildren,
|
AdminRouteRoute: AdminRouteRouteWithChildren,
|
||||||
PengaturanRouteRoute: PengaturanRouteRouteWithChildren,
|
DashboardRouteRoute: DashboardRouteRouteWithChildren,
|
||||||
BantuanRoute: BantuanRoute,
|
|
||||||
BumdesRoute: BumdesRoute,
|
|
||||||
DemografiPekerjaanRoute: DemografiPekerjaanRoute,
|
|
||||||
JennaAnalyticRoute: JennaAnalyticRoute,
|
|
||||||
KeamananRoute: KeamananRoute,
|
|
||||||
KeuanganAnggaranRoute: KeuanganAnggaranRoute,
|
|
||||||
KinerjaDivisiRoute: KinerjaDivisiRoute,
|
|
||||||
PengaduanLayananPublikRoute: PengaduanLayananPublikRoute,
|
|
||||||
SigninRoute: SigninRoute,
|
SigninRoute: SigninRoute,
|
||||||
SignupRoute: SignupRoute,
|
SignupRoute: SignupRoute,
|
||||||
SosialRoute: SosialRoute,
|
|
||||||
ProfileEditRoute: ProfileEditRoute,
|
ProfileEditRoute: ProfileEditRoute,
|
||||||
UsersIdRoute: UsersIdRoute,
|
UsersIdRoute: UsersIdRoute,
|
||||||
ProfileIndexRoute: ProfileIndexRoute,
|
ProfileIndexRoute: ProfileIndexRoute,
|
||||||
|
|||||||
@@ -7,20 +7,8 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
|
|||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
beforeLoad: async ({ location }) => {
|
beforeLoad: protectedRouteMiddleware,
|
||||||
// Only apply auth middleware for routes that need it
|
onEnter({ context }) {
|
||||||
// Public routes: /, /signin, /signup
|
|
||||||
const isPublicRoute =
|
|
||||||
location.pathname === "/" ||
|
|
||||||
location.pathname === "/signin" ||
|
|
||||||
location.pathname === "/signup";
|
|
||||||
|
|
||||||
if (isPublicRoute) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply protected route middleware for all other routes
|
|
||||||
const context = await protectedRouteMiddleware({ location });
|
|
||||||
authStore.user = context?.user as any;
|
authStore.user = context?.user as any;
|
||||||
authStore.session = context?.session as any;
|
authStore.session = context?.session as any;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Header } from "@/components/header";
|
|
||||||
import HelpPage from "@/components/help-page";
|
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/bantuan")({
|
|
||||||
component: BantuanPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
function BantuanPage() {
|
|
||||||
const [opened, { toggle }] = useDisclosure();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
header={{ height: 60 }}
|
|
||||||
navbar={{
|
|
||||||
width: 300,
|
|
||||||
breakpoint: "sm",
|
|
||||||
collapsed: { mobile: !opened },
|
|
||||||
}}
|
|
||||||
padding="md"
|
|
||||||
>
|
|
||||||
<AppShell.Header bg={headerBgColor}>
|
|
||||||
<Group h="100%" px="md">
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
|
||||||
<Header />
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar
|
|
||||||
p="md"
|
|
||||||
bg={navbarBgColor}
|
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
|
||||||
<HelpPage />
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/bumdes")({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/bumdes"!</div>;
|
|
||||||
}
|
|
||||||
6
src/routes/dashboard/bantuan.ts
Normal file
6
src/routes/dashboard/bantuan.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import HelpPage from "@/components/help-page";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/bantuan")({
|
||||||
|
component: HelpPage,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/bumdes.ts
Normal file
6
src/routes/dashboard/bumdes.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import BumdesPage from "@/components/bumdes-page";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/bumdes")({
|
||||||
|
component: BumdesPage,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/demografi-pekerjaan.ts
Normal file
6
src/routes/dashboard/demografi-pekerjaan.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import DemografiPekerjaan from "../../components/demografi-pekerjaan";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/demografi-pekerjaan")({
|
||||||
|
component: DemografiPekerjaan,
|
||||||
|
});
|
||||||
5
src/routes/dashboard/index.ts
Normal file
5
src/routes/dashboard/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { DashboardContent } from "@/components/dashboard-content";
|
||||||
|
export const Route = createFileRoute("/dashboard/")({
|
||||||
|
component: DashboardContent,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/jenna-analytic.ts
Normal file
6
src/routes/dashboard/jenna-analytic.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import JennaAnalytic from "@/components/jenna-analytic";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/jenna-analytic")({
|
||||||
|
component: JennaAnalytic,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/keamanan.ts
Normal file
6
src/routes/dashboard/keamanan.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import KeamananPage from "@/components/keamanan-page";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/keamanan")({
|
||||||
|
component: KeamananPage,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/keuangan-anggaran.ts
Normal file
6
src/routes/dashboard/keuangan-anggaran.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import KeuanganAnggaran from "@/components/keuangan-anggaran";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/keuangan-anggaran")({
|
||||||
|
component: KeuanganAnggaran,
|
||||||
|
});
|
||||||
5
src/routes/dashboard/kinerja-divisi.ts
Normal file
5
src/routes/dashboard/kinerja-divisi.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import KinerjaDivisi from "@/components/kinerja-divisi";
|
||||||
|
export const Route = createFileRoute("/dashboard/kinerja-divisi")({
|
||||||
|
component: KinerjaDivisi,
|
||||||
|
});
|
||||||
5
src/routes/dashboard/pengaduan-layanan-publik.ts
Normal file
5
src/routes/dashboard/pengaduan-layanan-publik.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
||||||
|
export const Route = createFileRoute("/dashboard/pengaduan-layanan-publik")({
|
||||||
|
component: PengaduanLayananPublik,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/pengaturan/akses-dan-tim.ts
Normal file
6
src/routes/dashboard/pengaturan/akses-dan-tim.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import AksesDanTimSettings from "@/components/pengaturan/akses-dan-tim";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/pengaturan/akses-dan-tim")({
|
||||||
|
component: AksesDanTimSettings,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/pengaturan/keamanan.ts
Normal file
6
src/routes/dashboard/pengaturan/keamanan.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import KeamananSettings from "@/components/pengaturan/keamanan";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/pengaturan/keamanan")({
|
||||||
|
component: KeamananSettings,
|
||||||
|
});
|
||||||
6
src/routes/dashboard/pengaturan/notifikasi.ts
Normal file
6
src/routes/dashboard/pengaturan/notifikasi.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import NotifikasiSettings from "@/components/pengaturan/notifikasi";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/pengaturan/notifikasi")({
|
||||||
|
component: NotifikasiSettings,
|
||||||
|
});
|
||||||
9
src/routes/dashboard/pengaturan/route.tsx
Normal file
9
src/routes/dashboard/pengaturan/route.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/pengaturan")({
|
||||||
|
component: () => (
|
||||||
|
<div className="p-2">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import UmumSettings from "@/components/pengaturan/umum";
|
import UmumSettings from "@/components/pengaturan/umum";
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan/umum")({
|
export const Route = createFileRoute("/dashboard/pengaturan/umum")({
|
||||||
component: UmumSettings,
|
component: UmumSettings,
|
||||||
});
|
});
|
||||||
80
src/routes/dashboard/route.tsx
Normal file
80
src/routes/dashboard/route.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
AppShell,
|
||||||
|
Burger,
|
||||||
|
Group,
|
||||||
|
useMantineColorScheme,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
||||||
|
import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const [opened, { toggle, close }] = useDisclosure();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const routerState = useRouterState();
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery("(max-width: 48em)");
|
||||||
|
|
||||||
|
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||||
|
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||||
|
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||||
|
|
||||||
|
// ✅ AUTO CLOSE NAVBAR ON ROUTE CHANGE (MOBILE ONLY)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMobile && opened) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}, [routerState.location.pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header bg={headerBgColor}>
|
||||||
|
<Group
|
||||||
|
h="100%"
|
||||||
|
px="lg"
|
||||||
|
align="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggle}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Navbar
|
||||||
|
p="md"
|
||||||
|
bg={navbarBgColor}
|
||||||
|
style={{ display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main bg={mainBgColor}>
|
||||||
|
<Outlet />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
src/routes/dashboard/sosial.ts
Normal file
6
src/routes/dashboard/sosial.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import SocialPage from "@/components/sosial-page";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/dashboard/sosial")({
|
||||||
|
component: SocialPage,
|
||||||
|
});
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Header } from "@/components/header";
|
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
import DemografiPekerjaan from "../components/demografi-pekerjaan";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/demografi-pekerjaan")({
|
|
||||||
component: DemografiPekerjaanPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
function DemografiPekerjaanPage() {
|
|
||||||
const [opened, { toggle }] = useDisclosure();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
header={{ height: 60 }}
|
|
||||||
navbar={{
|
|
||||||
width: 300,
|
|
||||||
breakpoint: "sm",
|
|
||||||
collapsed: { mobile: !opened },
|
|
||||||
}}
|
|
||||||
padding="md"
|
|
||||||
>
|
|
||||||
<AppShell.Header bg={headerBgColor}>
|
|
||||||
<Group h="100%" px="md">
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
|
||||||
<Header />
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar
|
|
||||||
p="md"
|
|
||||||
bg={navbarBgColor}
|
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
|
||||||
<DemografiPekerjaan />
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +1,788 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
import {
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
ActionIcon,
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
Avatar,
|
||||||
import { DashboardContent } from "@/components/dashboard-content";
|
Box,
|
||||||
import { Header } from "@/components/header";
|
Button,
|
||||||
import { Sidebar } from "@/components/sidebar";
|
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";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: DashboardPage,
|
component: HomePage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function DashboardPage() {
|
// Navigation items
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const NAV_ITEMS = [
|
||||||
const { colorScheme } = useMantineColorScheme();
|
{ label: "Home", link: "/" },
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
{ label: "Features", link: "#features" },
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
{ label: "Testimonials", link: "#testimonials" },
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
{ 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 (
|
return (
|
||||||
<AppShell
|
<Box
|
||||||
header={{ height: 60 }}
|
h={70}
|
||||||
navbar={{
|
px="md"
|
||||||
width: 300,
|
style={{
|
||||||
breakpoint: "sm",
|
borderBottom: "1px solid var(--mantine-color-gray-2)",
|
||||||
collapsed: { mobile: !opened },
|
transition: "all 0.3s ease",
|
||||||
|
boxShadow: scrolled ? "0 2px 10px rgba(0,0,0,0.1)" : "none",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
padding="md"
|
|
||||||
>
|
>
|
||||||
<AppShell.Header bg={headerBgColor}>
|
<Group h="100%" justify="space-between">
|
||||||
<Group h="100%" px="md">
|
<Group>
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Link to="/" style={{ textDecoration: "none" }}>
|
||||||
<Header />
|
<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>
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar
|
<Group>
|
||||||
p="md"
|
<ActionIcon
|
||||||
bg={navbarBgColor}
|
variant="default"
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
onClick={() => toggleColorScheme()}
|
||||||
>
|
size="lg"
|
||||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
>
|
||||||
<Sidebar />
|
{colorScheme === "dark" ? (
|
||||||
</div>
|
<IconSun size={18} />
|
||||||
</AppShell.Navbar>
|
) : (
|
||||||
|
<IconMoon size={18} />
|
||||||
<AppShell.Main bg={mainBgColor}>
|
)}
|
||||||
<DashboardContent />
|
</ActionIcon>
|
||||||
</AppShell.Main>
|
<Button component={Link} to="/signin" variant="light" size="sm">
|
||||||
</AppShell>
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/jenna-analytic")({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/jenna-analytic"!</div>;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/keamanan")({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/keamanan"!</div>;
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Header } from "@/components/header";
|
|
||||||
import KeuanganAnggaran from "@/components/keuangan-anggaran";
|
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/keuangan-anggaran")({
|
|
||||||
component: KeuanganAnggaranPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
function KeuanganAnggaranPage() {
|
|
||||||
const [opened, { toggle }] = useDisclosure();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
header={{ height: 60 }}
|
|
||||||
navbar={{
|
|
||||||
width: 300,
|
|
||||||
breakpoint: "sm",
|
|
||||||
collapsed: { mobile: !opened },
|
|
||||||
}}
|
|
||||||
padding="md"
|
|
||||||
>
|
|
||||||
<AppShell.Header bg={headerBgColor}>
|
|
||||||
<Group h="100%" px="md">
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
|
||||||
<Header />
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar
|
|
||||||
p="md"
|
|
||||||
bg={navbarBgColor}
|
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
|
||||||
<KeuanganAnggaran />
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Header } from "@/components/header";
|
|
||||||
import KinerjaDivisi from "@/components/kinerja-divisi";
|
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/kinerja-divisi")({
|
|
||||||
component: KinerjaDivisiPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
function KinerjaDivisiPage() {
|
|
||||||
const [opened, { toggle }] = useDisclosure();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
header={{ height: 60 }}
|
|
||||||
navbar={{
|
|
||||||
width: 300,
|
|
||||||
breakpoint: "sm",
|
|
||||||
collapsed: { mobile: !opened },
|
|
||||||
}}
|
|
||||||
padding="md"
|
|
||||||
>
|
|
||||||
<AppShell.Header bg={headerBgColor}>
|
|
||||||
<Group h="100%" px="md">
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
|
||||||
<Header />
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar
|
|
||||||
p="md"
|
|
||||||
bg={navbarBgColor}
|
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
|
||||||
<KinerjaDivisi />
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Header } from "@/components/header";
|
|
||||||
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaduan-layanan-publik")({
|
|
||||||
component: PengaduanLayananPublikPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
function PengaduanLayananPublikPage() {
|
|
||||||
const [opened, { toggle }] = useDisclosure();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
header={{ height: 60 }}
|
|
||||||
navbar={{
|
|
||||||
width: 300,
|
|
||||||
breakpoint: "sm",
|
|
||||||
collapsed: { mobile: !opened },
|
|
||||||
}}
|
|
||||||
padding="md"
|
|
||||||
>
|
|
||||||
<AppShell.Header bg={headerBgColor}>
|
|
||||||
<Group h="100%" px="md">
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
|
||||||
<Header />
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar
|
|
||||||
p="md"
|
|
||||||
bg={navbarBgColor}
|
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
|
||||||
<PengaduanLayananPublik />
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan/akses-dan-tim")({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/pengaturan/akses-dan-tim"!</div>;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan/keamanan")({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/pengaturan/keamanan"!</div>;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan/notifikasi")({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/pengaturan/notifikasi"!</div>;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
||||||
import { Header } from "@/components/header";
|
|
||||||
import { Sidebar } from "@/components/sidebar";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/pengaturan")({
|
|
||||||
component: PengaturanLayout,
|
|
||||||
});
|
|
||||||
|
|
||||||
function PengaturanLayout() {
|
|
||||||
const [opened, { toggle }] = useDisclosure();
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
|
||||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
|
||||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
header={{ height: 60 }}
|
|
||||||
navbar={{
|
|
||||||
width: 300,
|
|
||||||
breakpoint: "sm",
|
|
||||||
collapsed: { mobile: !opened },
|
|
||||||
}}
|
|
||||||
padding="md"
|
|
||||||
>
|
|
||||||
<AppShell.Header bg={headerBgColor}>
|
|
||||||
<Group h="100%" px="md">
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
|
||||||
<Header />
|
|
||||||
</Group>
|
|
||||||
</AppShell.Header>
|
|
||||||
|
|
||||||
<AppShell.Navbar
|
|
||||||
p="md"
|
|
||||||
bg={navbarBgColor}
|
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
|
|
||||||
<AppShell.Main bg={mainBgColor}>
|
|
||||||
<div className="p-2">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/sosial")({
|
|
||||||
component: RouteComponent,
|
|
||||||
});
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/sosial"!</div>;
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,7 @@ import createClient from "openapi-fetch";
|
|||||||
import type { paths } from "../../generated/api";
|
import type { paths } from "../../generated/api";
|
||||||
import { VITE_PUBLIC_URL } from "./env";
|
import { VITE_PUBLIC_URL } from "./env";
|
||||||
|
|
||||||
const baseUrl =
|
const baseUrl = VITE_PUBLIC_URL;
|
||||||
VITE_PUBLIC_URL ||
|
|
||||||
(typeof window !== "undefined"
|
|
||||||
? window.location.origin
|
|
||||||
: "http://localhost:3000");
|
|
||||||
|
|
||||||
export const apiClient = createClient<paths>({
|
export const apiClient = createClient<paths>({
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||||
import { PrismaClient } from "../../generated/prisma";
|
import { PrismaClient } from "../../generated/prisma";
|
||||||
import logger from "./logger";
|
import { VITE_PUBLIC_URL } from "./env";
|
||||||
|
|
||||||
const baseUrl = process.env.VITE_PUBLIC_URL;
|
const baseUrl = VITE_PUBLIC_URL;
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
if (!baseUrl) {
|
|
||||||
logger.error("VITE_PUBLIC_URL is not defined");
|
|
||||||
throw new Error("VITE_PUBLIC_URL is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger.info('Initializing Better Auth with Prisma adapter');
|
// logger.info('Initializing Better Auth with Prisma adapter');
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
@@ -37,8 +32,24 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
databaseHooks: {
|
||||||
|
user: {
|
||||||
|
create: {
|
||||||
|
before: async (user) => {
|
||||||
|
if (user.email === process.env.ADMIN_EMAIL) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
...user,
|
||||||
|
role: "admin",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { data: user };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
secret: process.env.BETTER_AUTH_SECRET,
|
secret: process.env.BETTER_AUTH_SECRET,
|
||||||
trustedOrigins: ["http://localhost:5173", "http://localhost:3000"],
|
|
||||||
session: {
|
session: {
|
||||||
cookieCache: {
|
cookieCache: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -48,5 +59,6 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
advanced: {
|
advanced: {
|
||||||
cookiePrefix: "bun-react",
|
cookiePrefix: "bun-react",
|
||||||
|
trustProxy: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,10 +20,21 @@ export const getEnv = (key: string, defaultValue = ""): string => {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VITE_PUBLIC_URL = getEnv(
|
export const VITE_PUBLIC_URL = (() => {
|
||||||
"VITE_PUBLIC_URL",
|
// Priority:
|
||||||
"http://localhost:3000",
|
// 1. BETTER_AUTH_URL (standard for better-auth)
|
||||||
);
|
// 2. VITE_PUBLIC_URL (our app standard)
|
||||||
|
// 3. window.location.origin (browser fallback)
|
||||||
|
const envUrl = getEnv("BETTER_AUTH_URL") || getEnv("VITE_PUBLIC_URL");
|
||||||
|
if (envUrl) return envUrl;
|
||||||
|
|
||||||
|
// Fallback for browser
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http://localhost:3000";
|
||||||
|
})();
|
||||||
|
|
||||||
export const IS_DEV = (() => {
|
export const IS_DEV = (() => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createServer as createViteServer } from "vite";
|
|||||||
export async function createVite() {
|
export async function createVite() {
|
||||||
return createViteServer({
|
return createViteServer({
|
||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
|
publicDir: "public",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(process.cwd(), "./src"),
|
"@": path.resolve(process.cwd(), "./src"),
|
||||||
|
|||||||
Reference in New Issue
Block a user