diff --git a/frontend/.env.example b/frontend/.env.example index 1f12d62..30023cd 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -28,3 +28,7 @@ NEXT_PUBLIC_CONTRACT_ID= # Optional app environment label for diagnostics (not a secret). # NODE_ENV is set automatically by Next.js (development | production | test). + +# Set to true to auto-seed the in-memory demo data when the local task API is first read. +# This is ignored in production. +NEXT_PUBLIC_SEED_DEMO_DATA=false diff --git a/frontend/README.md b/frontend/README.md index a031db7..4f69111 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -112,3 +112,13 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + +## Local demo data + +The frontend includes deterministic in-memory demo data for local development. It covers two organizations and three bounties: an open bounty, a bounty awaiting review with a submission and comment, and a completed bounty with an approved submission. The seed also creates representative notifications and contributor activity history. + +From the `frontend` directory, run `pnpm db:seed` to reset the in-memory stores and load the sample records in the current process. Run `pnpm db:reset` when a clean in-memory state is needed. Because the project’s development stores are intentionally in memory, these commands are useful for scripts and tests; the data is not written to a production database or Stellar ledger. + +To have the task API load the sample data automatically when its process starts, set `NEXT_PUBLIC_SEED_DEMO_DATA=true` in the local environment. This flag is ignored in production, and normal development behavior remains unchanged when it is omitted. + +The reusable implementation is in `src/lib/demo-seed.ts`, making it straightforward to extend the fixtures or call `seedDemoData()` from additional local tooling. diff --git a/frontend/package.json b/frontend/package.json index aeb0aed..d5192eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,9 @@ "start": "next start", "lint": "eslint", "test": "vitest run", - "test:security-headers": "node --test security-headers.test.mjs" + "test:security-headers": "node --test security-headers.test.mjs", + "db:seed": "tsx scripts/seed-demo-data.ts", + "db:reset": "tsx scripts/seed-demo-data.ts --reset" }, "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.9.5", @@ -50,6 +52,7 @@ "eslint": "^9", "eslint-config-next": "16.1.3", "tailwindcss": "^4", + "tsx": "^4.23.13", "typescript": "^5", "vitest": "^4.1.10" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 34d52bc..48db65b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -120,12 +120,15 @@ importers: tailwindcss: specifier: ^4 version: 4.1.18 + tsx: + specifier: ^4.23.13 + version: 4.23.13 typescript: specifier: ^5 version: 5.9.3 vitest: specifier: ^4.1.10 - version: 4.1.10(@types/node@20.19.30)(vite@8.1.5(@types/node@20.19.30)(jiti@2.6.1)) + version: 4.1.10(@types/node@20.19.30)(vite@8.1.5(@types/node@20.19.30)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13)) packages: @@ -241,6 +244,162 @@ packages: '@emurgo/cardano-serialization-lib-nodejs@13.2.0': resolution: {integrity: sha512-Bz1zLGEqBQ0BVkqt1OgMxdBOE3BdUWUd7Ly9Ecr/aUwkA8AV1w1XzBMe4xblmJHnB1XXNlPH4SraXCvO+q0Mig==} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3114,6 +3273,11 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4891,6 +5055,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + tweetnacl-util@0.15.1: resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} @@ -5516,6 +5685,84 @@ snapshots: '@emurgo/cardano-serialization-lib-nodejs@13.2.0': {} + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: eslint: 9.39.2(jiti@2.6.1) @@ -8009,13 +8256,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@20.19.30)(jiti@2.6.1))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@20.19.30)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@20.19.30)(jiti@2.6.1) + vite: 8.1.5(@types/node@20.19.30)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13) '@vitest/pretty-format@4.1.10': dependencies: @@ -9013,6 +9260,35 @@ snapshots: dependencies: es6-promise: 4.2.8 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -11022,6 +11298,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + tweetnacl-util@0.15.1: {} tweetnacl@1.0.3: {} @@ -11232,7 +11514,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@8.1.5(@types/node@20.19.30)(jiti@2.6.1): + vite@8.1.5(@types/node@20.19.30)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 @@ -11241,13 +11523,15 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 20.19.30 + esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.6.1 + tsx: 4.23.13 - vitest@4.1.10(@types/node@20.19.30)(vite@8.1.5(@types/node@20.19.30)(jiti@2.6.1)): + vitest@4.1.10(@types/node@20.19.30)(vite@8.1.5(@types/node@20.19.30)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@20.19.30)(jiti@2.6.1)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@20.19.30)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -11264,7 +11548,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@20.19.30)(jiti@2.6.1) + vite: 8.1.5(@types/node@20.19.30)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.30 diff --git a/frontend/scripts/seed-demo-data.ts b/frontend/scripts/seed-demo-data.ts new file mode 100644 index 0000000..6029cac --- /dev/null +++ b/frontend/scripts/seed-demo-data.ts @@ -0,0 +1,12 @@ +import { resetDemoData, seedDemoData } from "../src/lib/demo-seed"; + +const shouldResetOnly = process.argv.includes("--reset"); + +if (shouldResetOnly) { + resetDemoData(); + console.log("Demo data reset for this process."); +} else { + const summary = seedDemoData(); + console.table(summary); + console.log("Demo data seeded for this process."); +} diff --git a/frontend/src/app/api/tasks/route.ts b/frontend/src/app/api/tasks/route.ts index 889467d..8ddcc30 100644 --- a/frontend/src/app/api/tasks/route.ts +++ b/frontend/src/app/api/tasks/route.ts @@ -1,4 +1,5 @@ import { createTask, listTasks } from "@/lib/task-workflow"; +import { isDemoSeedEnabled, seedDemoData } from "@/lib/demo-seed"; import { buildNoStoreJson } from "@/lib/api-response"; import { checkRateLimit } from "@/lib/rate-limit"; import type { TaskDifficulty, TaskSortOrder } from "@/types/task-workflow"; @@ -16,6 +17,9 @@ function parseOptionalNumber(value: string | null): number | undefined { } export async function GET(request: Request) { + if (isDemoSeedEnabled() && listTasks({}).total === 0) { + seedDemoData(); + } const { response: rateLimitResponse, headers: rateLimitHeaders } = checkRateLimit(request); if (rateLimitResponse) { diff --git a/frontend/src/lib/demo-seed.test.ts b/frontend/src/lib/demo-seed.test.ts new file mode 100644 index 0000000..471189a --- /dev/null +++ b/frontend/src/lib/demo-seed.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { listUserActivities } from "@/lib/activity-store"; +import { listOrganizations } from "@/lib/organization-store"; +import { getUnreadCount, listNotifications } from "@/lib/notification-store"; +import { getTask, listContributorSubmissions, listTasks } from "@/lib/task-workflow"; +import { DEMO_USERS, resetDemoData, seedDemoData } from "@/lib/demo-seed"; + +describe("demo seed data", () => { + const now = new Date(); + + it("seeds realistic organizations and task workflow scenarios", () => { + const summary = seedDemoData(now); + + expect(summary).toEqual({ + organizations: 2, + tasks: 3, + submissions: 2, + comments: 1, + notifications: 9, + activities: 2, + }); + expect(listOrganizations()).toHaveLength(2); + expect(listTasks({}).tasks).toHaveLength(3); + expect(listTasks({ difficulty: "advanced" }).tasks[0]?.status).toBe("completed"); + expect(listTasks({ technology: "typescript" }).tasks).toHaveLength(1); + expect(listContributorSubmissions(DEMO_USERS.contributor).ok).toBe(true); + expect(listNotifications(DEMO_USERS.contributor)).toHaveLength(7); + expect(getUnreadCount(DEMO_USERS.contributor)).toBe(7); + expect(listUserActivities({ userId: DEMO_USERS.contributor }).total).toBe(1); + }); + + it("can reset all seeded records and restart identifiers", () => { + seedDemoData(now); + resetDemoData(); + + expect(listOrganizations()).toEqual([]); + expect(listTasks({}).tasks).toEqual([]); + expect(listNotifications(DEMO_USERS.contributor)).toEqual([]); + expect(listUserActivities({ userId: DEMO_USERS.contributor }).total).toBe(0); + expect(getTask("1").ok).toBe(false); + + const summary = seedDemoData(now); + expect(summary.tasks).toBe(3); + expect(listTasks({ sort: "newest" }).tasks[0]?.id).toBe("1"); + }); +}); diff --git a/frontend/src/lib/demo-seed.ts b/frontend/src/lib/demo-seed.ts new file mode 100644 index 0000000..143120b --- /dev/null +++ b/frontend/src/lib/demo-seed.ts @@ -0,0 +1,225 @@ +import type { ValidatedTaskSubmissionFile } from "@/lib/task-submission-files"; +import { + createOrganization, + resetOrganizationStore, +} from "@/lib/organization-store"; +import { + resetActivityStore, + recordActivity, +} from "@/lib/activity-store"; +import { + addComment, + approveSubmission, + createTask, + resetTaskWorkflowStore, + submitTaskWork, +} from "@/lib/task-workflow"; +import { + createNotification, + resetNotificationStore, +} from "@/lib/notification-store"; + +export const DEMO_USERS = { + foundation: "GCFND7DEMOFOUNDATION", + poster: "GPOSTER7DEMOACCOUNT", + contributor: "GCONTR7DEMOCONTRIB", + reviewer: "GREVIEW7DEMOREVIEWER", +} as const; + +export type DemoSeedSummary = { + organizations: number; + tasks: number; + submissions: number; + comments: number; + notifications: number; + activities: number; +}; + +const DEMO_FILE: ValidatedTaskSubmissionFile = { + name: "implementation-notes.md", + size: 1_024, + extension: "md", + kind: "md", + detectedMimeType: "text/markdown", + providedMimeType: "text/markdown", +}; + +/** + * Clears every in-memory application store used by the local demo and tests. + * This intentionally does not touch Stellar ledger state or production data. + */ +export function resetDemoData(): void { + resetOrganizationStore(); + resetTaskWorkflowStore(); + resetNotificationStore(); + resetActivityStore(); +} + +/** + * Replaces the current local data with a deterministic set of common bounty + * marketplace scenarios: an open bounty, an in-review bounty, and a completed + * bounty with a contributor, comment, notification, and activity history. + */ +export function seedDemoData(now: Date = new Date()): DemoSeedSummary { + resetDemoData(); + + const organization = createOrganization( + { + name: "Stellar Builders Collective", + description: "A community fund supporting open-source Stellar tooling.", + website: "https://stellar.builders.example", + }, + now, + ); + if (!organization.ok) throw new Error(organization.error); + + const secondOrganization = createOrganization( + { + name: "Open Payments Lab", + description: "Research and product experiments for accessible payments.", + website: "https://payments-lab.example", + }, + new Date(now.getTime() - 86_400_000), + ); + if (!secondOrganization.ok) throw new Error(secondOrganization.error); + + const create = (input: Parameters[0], createdAt: Date) => { + const result = createTask(input, createdAt); + if (!result.ok) throw new Error(result.error); + return result.task; + }; + + const openTask = create( + { + poster: DEMO_USERS.poster, + title: "Build a Soroban transaction history component", + description: + "Create a responsive React component that groups wallet transactions by date and clearly shows loading, empty, and error states.", + reward: 250_000_000, + deadline: Math.floor(now.getTime() / 1_000) + 21 * 86_400, + maxSubmissions: 3, + difficulty: "intermediate", + technologies: ["React", "TypeScript", "Soroban"], + organizationId: organization.organization.id, + }, + new Date(now.getTime() - 2 * 3_600_000), + ); + + const reviewTask = create( + { + poster: DEMO_USERS.foundation, + title: "Write an SDK quick-start guide", + description: + "Turn the existing API examples into a concise onboarding guide with copy-paste snippets and troubleshooting notes.", + reward: 150_000_000, + deadline: Math.floor(now.getTime() / 1_000) + 10 * 86_400, + maxSubmissions: 2, + difficulty: "beginner", + technologies: ["Documentation", "JavaScript", "Stellar"], + organizationId: organization.organization.id, + }, + new Date(now.getTime() - 3 * 86_400_000), + ); + + const completedTask = create( + { + poster: DEMO_USERS.reviewer, + title: "Audit token payment edge cases", + description: + "Review the payment flow for rounding, insufficient balance, and failed transfer handling, then report actionable fixes.", + reward: 500_000_000, + deadline: Math.floor(now.getTime() / 1_000) + 3 * 86_400, + maxSubmissions: 2, + difficulty: "advanced", + technologies: ["Rust", "Soroban", "Security"], + organizationId: secondOrganization.organization.id, + }, + new Date(now.getTime() - 14 * 86_400_000), + ); + + const reviewSubmission = submitTaskWork( + { + taskId: reviewTask.id, + contributor: DEMO_USERS.contributor, + workUrl: "https://github.com/stellar-builders/sdk-guide/pull/12", + description: "Added the quick-start guide, examples, and a troubleshooting section.", + }, + [DEMO_FILE], + new Date(now.getTime() - 2 * 3_600_000), + ); + if (!reviewSubmission.ok) throw new Error(reviewSubmission.error); + + const completedSubmission = submitTaskWork( + { + taskId: completedTask.id, + contributor: DEMO_USERS.contributor, + workUrl: "https://github.com/open-payments-lab/token-audit/pull/7", + description: "Documented three edge cases and supplied regression tests for each.", + }, + [DEMO_FILE], + new Date(now.getTime() - 7 * 86_400_000), + ); + if (!completedSubmission.ok) throw new Error(completedSubmission.error); + const approved = approveSubmission( + completedTask.id, + completedSubmission.submission.id, + completedTask.poster, + new Date(now.getTime() - 6 * 86_400_000), + ); + if (!approved.ok) throw new Error(approved.error); + + const comment = addComment( + { + taskId: reviewTask.id, + submissionId: reviewSubmission.submission.id, + author: reviewTask.poster, + message: "Thanks! Please add one example for failed transactions before approval.", + }, + new Date(now.getTime() - 3_600_000), + ); + if (!comment.ok) throw new Error(comment.error); + + createNotification( + { + userId: DEMO_USERS.contributor, + type: "bounty_created", + title: "New bounty available", + message: "A new Soroban transaction history bounty is ready for submissions.", + taskId: openTask.id, + }, + new Date(now.getTime() - 30 * 60_000), + ); + recordActivity( + { + userId: DEMO_USERS.contributor, + type: "submission_reviewed", + title: "Submission approved", + description: `Your audit submission for “${completedTask.title}” was approved.`, + metadata: { taskId: completedTask.id, taskTitle: completedTask.title, status: "approved" }, + }, + new Date(now.getTime() - 6 * 86_400_000), + ); + recordActivity( + { + userId: DEMO_USERS.poster, + type: "bounty_created", + title: "Bounty published", + description: openTask.title, + metadata: { taskId: openTask.id, taskTitle: openTask.title }, + }, + new Date(now.getTime() - 2 * 3_600_000), + ); + + return { + organizations: 2, + tasks: 3, + submissions: 2, + comments: 1, + notifications: 9, + activities: 2, + }; +} + +export function isDemoSeedEnabled(): boolean { + return process.env.NODE_ENV !== "production" && process.env.NEXT_PUBLIC_SEED_DEMO_DATA === "true"; +}