Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ SUPER_ADMIN_NAME=Administrator

# Cron
CRON_SECRET=

# Axiom — Log Storage (https://axiom.co/docs/guides/pino)
AXIOM_TOKEN=AXIOM_TOKEN
AXIOM_DATASET=DATA_SET

# Cloudflare R2 — Object Storage
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=
R2_PUBLIC_URL=
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,28 @@ env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

GDRIVE_SERVICE_ACCOUNT_KEY: ${{secrets.GDRIVE_SERVICE_ACCOUNT_KEY}}
R2_ACCOUNT_ID: ${{secrets.R2_ACCOUNT_ID}}
R2_ACCESS_KEY_ID: ${{secrets.R2_ACCESS_KEY_ID}}
R2_SECRET_ACCESS_KEY: ${{secrets.R2_SECRET_ACCESS_KEY}}
R2_BUCKET_NAME: ${{secrets.R2_BUCKET_NAME}}
R2_PUBLIC_URL: ${{secrets.R2_PUBLIC_URL}}
AXIOM_TOKEN: ${{secrets.AXIOM_TOKEN}}
AXIOM_DATASET: ${{secrets.AXIOM_DATASET}}
CRON_SECRET: ${{secrets.CRON_SECRET}}
BETTER_AUTH_SECRET: ${{secrets.BETTER_AUTH_SECRET}}
BETTER_AUTH_URL: http://localhost:3000
BETTER_AUTH_COOKIE_PREFIX: domus_auth_test
GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}}
GOOGLE_CLIENT_SECRET: ${{secrets.GOOGLE_CLIENT_SECRET}}
SUPER_ADMIN_EMAIL: superadmin@test.com
SUPER_ADMIN_PASSWORD: testing123
SUPER_ADMIN_NAME: Super Admin
NEXT_PUBLIC_DOMUS_VERSION: 0.0.0-dev
CF_API_TOKEN: ${{secrets.CF_API_TOKEN}}
CF_ACCOUNT_ID: ${{secrets.CF_ACCOUNT_ID}}
WHATSAPP_TOKEN: ${{secrets.WHATSAPP_TOKEN}}

jobs:
check:
name: Lint & Type Check
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

jobs:
deploy:
name: Vercel Deploy
dash:
name: Dashboard
runs-on: ubuntu-latest
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand Down Expand Up @@ -55,4 +55,4 @@ jobs:
NEXT_PUBLIC_DOMUS_VERSION: ${{ github.ref_name }}

- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,4 @@ See → [docs/tdd.md — Deployment & Infrastructure](docs/tdd.md#9-deployment--

## License

MIT — see → [docs/tdd.md — License](docs/tdd.md#11-license)
MIT — see → [docs/tdd.md — License](docs/tdd.md#12-license)
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pnpm dev
```
domus/
├── apps/
│ ├── dash/ # Next.js 15 — main application (dash.pkrbt.id)
│ ├── dash/ # Next.js 15 — main application (pkrbt.id)
│ └── cron/ # Cloudflare Workers — scheduled tasks
├── packages/
│ ├── core/ # Framework-agnostic business logic (Clean Architecture)
Expand Down
1 change: 1 addition & 0 deletions apps/cron/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.wrangler
9 changes: 6 additions & 3 deletions apps/cron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
"name": "@domus/cron",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"build": "tsdown",
"dev": "tsdown --watch",
"deploy": "pnpm run build && wrangler deploy dist/index.mjs",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"nanoid": "^5.1.7"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250224.0",
"tsdown": "^0.21.7",
"typescript": "^5.7.3",
"wrangler": "^3.111.0"
"wrangler": "^4.79.0"
}
}
16 changes: 16 additions & 0 deletions apps/cron/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'tsdown';

export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: 'node20',
outDir: 'dist',
clean: true,
// Equivalent to previous bundle: true
unbundle: false,
deps: {
// Equivalent to previous external: [/^cloudflare:/]
neverBundle: [/^cloudflare:/],
},
minify: true,
});
6 changes: 3 additions & 3 deletions apps/cron/wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name = "domus-cron"
main = "src/index.ts"
main = "dist/index.mjs"
compatibility_date = "2026-04-01"

[triggers]
crons = ["0 0 * * 0"]
crons = ["0 0 * * 7"]

[vars]
DASH_API_URL = "https://dash.pkrbt.id"
DASH_API_URL = "https://pkrbt.id"

# CRON_SECRET must be set via: wrangler secret put CRON_SECRET
2 changes: 1 addition & 1 deletion apps/dash/app/api/cron/rotate-join-id/route.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Thin export only — no logic here.
export { POST } from "@/app/api-routes/cron/rotate-join-id";
export { POST } from "@/src/api-routes/cron/rotate-join-id";
40 changes: 40 additions & 0 deletions apps/dash/app/api/upload/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { type NextRequest, NextResponse } from "next/server";
import { logger } from "@/shared/core/logger";

/**
* Mock upload API for E2E testing and development.
* This should be replaced with a real storage implementation (e.g. R2, GDrive) in production.
*/
export async function POST(req: NextRequest) {
try {
const formData = await req.formData();
const file = formData.get("file") as File;
const type = formData.get("type") as string;
const id = formData.get("id") as string;

logger.info("Mock Upload API received file", {
name: file?.name,
size: file?.size,
type,
id,
});

// Dummy delay to simulate network latency for the test progress bar
await new Promise((resolve) => setTimeout(resolve, 500));

// Return a dummy URL (pointing back to a placeholder or something valid)
// In a real implementation, this would be the URL from the storage provider.
const dummyUrl = `https://images.unsplash.com/photo-1518173946687-a4c8a9833d8e?q=80&w=1000&auto=format&fit=crop`;

return NextResponse.json({
success: true,
url: dummyUrl,
});
} catch (error) {
logger.error("Mock Upload API error", { error });
return NextResponse.json(
{ success: false, message: "Upload failed" },
{ status: 500 },
);
}
}
11 changes: 11 additions & 0 deletions apps/dash/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,17 @@
--font-heading: "Plus Jakarta Sans", sans-serif;
--font-body: "Inter", sans-serif;
--font-sans: "Inter", sans-serif;

/* Animations */
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
--animate-shimmer: shimmer 2s infinite linear;
}

/* -------------------------------------------------------------
Expand Down
19 changes: 15 additions & 4 deletions apps/dash/e2e/features/org/join.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ test.describe("Organization Join Page", () => {
await expect(page).toHaveURL(new RegExp(`/join/${TEST_JOIN_ID}/success`), {
timeout: 10000,
});
await expect(page.getByText(/Pendaftaran Berhasil!/i)).toBeVisible();
await expect(
page.getByRole("heading", { name: /Pendaftaran Berhasil!/i }),
).toBeVisible();
});

test("should show already registered message when joining twice", async ({
Expand All @@ -76,7 +78,12 @@ test.describe("Organization Join Page", () => {
await page.getByRole("button", { name: /Bergabung Sekarang/i }).click();

// Verify first one is "Success"
await expect(page.getByText(/Pendaftaran Berhasil!/i)).toBeVisible();
await expect(page).toHaveURL(new RegExp(`/join/${TEST_JOIN_ID}/success`), {
timeout: 20000,
});
await expect(
page.getByRole("heading", { name: /Pendaftaran Berhasil!/i }),
).toBeVisible({ timeout: 10000 });

// 3. Perform second registration (same user, same org)
await page.goto(`/join/${TEST_JOIN_ID}`);
Expand All @@ -91,7 +98,9 @@ test.describe("Organization Join Page", () => {
new RegExp(`/join/${TEST_JOIN_ID}/success\\?exists=true`),
{ timeout: 10000 },
);
await expect(page.getByText(/Pendaftaran Sudah Ada/i)).toBeVisible();
await expect(
page.getByRole("heading", { name: /Pendaftaran Sudah Ada/i }),
).toBeVisible({ timeout: 15000 });
await expect(page.getByText(/Anda sudah terdaftar/i)).toBeVisible();
});

Expand All @@ -102,6 +111,8 @@ test.describe("Organization Join Page", () => {
accountStatus: AccountStatus.Pending,
});
await page.goto("/join/invalid-id-999");
await expect(page.getByText(/Link Tidak Valid/i)).toBeVisible();
await expect(
page.getByRole("heading", { name: /Link Tidak Valid/i }),
).toBeVisible();
});
});
86 changes: 84 additions & 2 deletions apps/dash/e2e/features/org/manage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ test.describe("Organization Update & Remove Page", () => {

test("should soft remove an existing organization", async ({ page }) => {
// 1. Create a dummy organization to test removal
const randomSuffix = Math.random().toString(36).substring(7);
const orgName = `Organisasi Untuk Dihapus ${randomSuffix}`;
await page.goto("/org/new");
await page.locator("#name").fill("Organisasi Untuk Dihapus");
await page.locator("#name").fill(orgName);
await page.getByRole("button", { name: /Simpan/i }).click();

// 2. Wait for redirect to details page
Expand All @@ -74,7 +76,87 @@ test.describe("Organization Update & Remove Page", () => {
page
.getByTestId("org-grid")
.locator("> div")
.filter({ hasText: "Organisasi Untuk Dihapus" }),
.filter({ hasText: orgName }),
).toHaveCount(0);
});

test("should inline edit organization name, description, cover, and logo", async ({
page,
}) => {
// 1. Navigate to /org and click on the first organization to edit
await page.goto("/org");
const firstCardLink = page.getByTestId("org-grid").locator("a").first();
const orgUrlPath = await firstCardLink.getAttribute("href");
if (!orgUrlPath) throw new Error("No URL found");

await page.goto(orgUrlPath);

// 2. Edit Name
const nameGroup = page.locator(".group\\/name");
await nameGroup.hover();
await page.getByRole("button", { name: "Ubah Nama" }).click();

const nameInput = page.locator("input.font-heading");
await expect(nameInput).toBeVisible();
await nameInput.fill("Nama Inline Baru");
await page.keyboard.press("Enter");

await expect(
page.getByRole("heading", { name: "Nama Inline Baru" }),
).toBeVisible();

// 3. Edit Description
const descGroup = page.locator(".group\\/desc");
await descGroup.hover();
await page.getByRole("button", { name: "Ubah Deskripsi" }).click();

const descTextarea = page.locator("textarea.font-body");
await expect(descTextarea).toBeVisible();
await descTextarea.fill("Deskripsi Inline Baru");
// Click the check button
await page.locator("textarea.font-body + div > button:first-child").click();

await expect(page.getByText("Deskripsi Inline Baru")).toBeVisible();

// 4. Edit Cover
const headerGroup = page.locator(".group\\/header");
await headerGroup.hover();
const ubahCoverBtn = page.getByRole("button", { name: "Ubah Cover" });
await expect(ubahCoverBtn).toBeVisible();

// In Playwright, we can handle the file chooser before clicking
const fileChooserPromise = page.waitForEvent("filechooser");
await ubahCoverBtn.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles({
name: "cover.jpg",
mimeType: "image/jpeg",
buffer: Buffer.from("fake image content"),
});

// We expect the progress overlay to appear and then disappear (upload completes)
await expect(page.getByText(/Uploading/i)).toBeVisible();
await expect(page.getByText(/Uploading/i)).toBeHidden();

// 5. Edit Logo
const logoGroup = page
.locator("div.w-24.h-24.group, div.w-28.h-28.group")
.first();
await logoGroup.hover();
const ubahLogoBtn = page.getByRole("button", { name: "Ubah Logo" });
await expect(ubahLogoBtn).toBeVisible();

const logoChooserPromise = page.waitForEvent("filechooser");
await ubahLogoBtn.click();
const logoChooser = await logoChooserPromise;
await logoChooser.setFiles({
name: "logo.jpg",
mimeType: "image/jpeg",
buffer: Buffer.from("fake image content"),
});

// Wait for upload progress to finish
await expect(page.getByText(/Uploading/i)).toBeVisible();
await expect(page.getByText(/Uploading/i)).toBeHidden();
});
});
10 changes: 10 additions & 0 deletions apps/dash/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ const nextConfig: NextConfig = {
protocol: "https",
hostname: "placehold.co",
},
{
protocol: "https",
hostname: "**.pkrbt.id",
},
],
},
serverExternalPackages: [
"@axiomhq/pino",
"pino",
"thread-stream",
"pino-pretty",
],
};

export default withNextIntl(nextConfig);
Loading
Loading