Skip to content

Commit d8ad25a

Browse files
mohitbadwalclaude
andcommitted
docs: add app screenshots to README
Add a Screenshots section (builder hero + dashboard/results/runtime/contacts gallery) with 7 retina screenshots under docs/screenshots/, plus a reusable Playwright capture script (scripts/capture_screenshots.mjs) that drives a running instance read-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 95bd89a commit d8ad25a

11 files changed

Lines changed: 226 additions & 0 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ segments, teams, and roles — all from a single, fully-owned, open-source codeb
88
It’s a self-hosted alternative to proprietary survey SaaS: features that are usually paywalled
99
(Teams & RBAC, Contacts/Segments, webhooks, API keys, white-label) are first-class here.
1010

11+
## Screenshots
12+
13+
<p align="center">
14+
<img src="docs/screenshots/builder.png" alt="OpenSurvey survey builder — question palette, settings, and a live preview" width="900">
15+
</p>
16+
17+
<table>
18+
<tr>
19+
<td width="50%"><img src="docs/screenshots/dashboard.png" alt="Dashboard"><br><sub><b>Dashboard</b> — active surveys, responses, completion rate & NPS at a glance.</sub></td>
20+
<td width="50%"><img src="docs/screenshots/results.png" alt="Results & analytics"><br><sub><b>Results & analytics</b> — completion funnel, drop-off, and response trends.</sub></td>
21+
</tr>
22+
<tr>
23+
<td width="50%"><img src="docs/screenshots/runtime.png" alt="Public survey runtime"><br><sub><b>Public survey</b> — what respondents see (shareable link or embed).</sub></td>
24+
<td width="50%"><img src="docs/screenshots/contacts.png" alt="Contacts & segments"><br><sub><b>Contacts & segments</b> — typed attributes, dynamic segments, PII masking.</sub></td>
25+
</tr>
26+
</table>
27+
28+
> Screenshots can be regenerated against any running instance with `scripts/capture_screenshots.mjs`.
29+
1130
## Features
1231

1332
- **Survey builder** — visual editor with 15+ question types (text, single/multi-select, rating,

docs/screenshots/builder.png

274 KB
Loading

docs/screenshots/contacts.png

155 KB
Loading

docs/screenshots/dashboard.png

195 KB
Loading

docs/screenshots/login.png

433 KB
Loading

docs/screenshots/results.png

227 KB
Loading

docs/screenshots/runtime.png

40.4 KB
Loading

docs/screenshots/surveys.png

156 KB
Loading

scripts/capture_screenshots.mjs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// ============================================================================
2+
// scripts/capture_screenshots.mjs — capture README screenshots of a RUNNING
3+
// OpenSurvey instance, using Playwright (Chromium, retina @2x).
4+
//
5+
// This drives a live server READ-ONLY (it logs in and navigates; it never
6+
// mutates data). Point it at any instance via env vars; defaults match the
7+
// local demo org seeded by `manage.py create_demo_tenant`.
8+
//
9+
// cd scripts
10+
// npm install
11+
// BASE_URL=http://127.0.0.1:8000 \
12+
// EMAIL=owner@example.com PASSWORD=... ORG_SLUG=demo \
13+
// SURVEY_ID=<a published survey id> \
14+
// npm run capture
15+
//
16+
// Output: docs/screenshots/*.png (1440x900 @2x).
17+
// ============================================================================
18+
import { chromium } from "@playwright/test";
19+
import { fileURLToPath } from "node:url";
20+
import { dirname, resolve } from "node:path";
21+
import { mkdirSync } from "node:fs";
22+
23+
const __dirname = dirname(fileURLToPath(import.meta.url));
24+
const REPO_ROOT = resolve(__dirname, "..");
25+
26+
const BASE_URL = process.env.BASE_URL ?? "http://127.0.0.1:8011";
27+
const EMAIL = process.env.EMAIL ?? "owner@example.com";
28+
const PASSWORD = process.env.PASSWORD ?? "opensurvey2026";
29+
const ORG_SLUG = process.env.ORG_SLUG ?? "demo";
30+
const SURVEY_ID = process.env.SURVEY_ID ?? "67ecadce-1d8e-4a68-b02d-6f90f7a1ed06";
31+
const OUT_DIR = process.env.OUT_DIR ?? resolve(REPO_ROOT, "docs", "screenshots");
32+
33+
mkdirSync(OUT_DIR, { recursive: true });
34+
35+
/** Wait for the page to settle: network idle, React-island hydration, then a short beat. */
36+
async function settle(page, { island = false } = {}) {
37+
await page.waitForLoadState("networkidle").catch(() => {});
38+
if (island) {
39+
// Islands mount into a #opensurvey-* container; wait until it has children.
40+
await page
41+
.waitForFunction(
42+
() => {
43+
const el = document.querySelector('[id^="opensurvey-"]');
44+
return el && el.childElementCount > 0;
45+
},
46+
{ timeout: 15000 },
47+
)
48+
.catch(() => {});
49+
}
50+
await page.waitForTimeout(1300); // animations / late layout
51+
}
52+
53+
async function shot(page, name) {
54+
const path = resolve(OUT_DIR, `${name}.png`);
55+
await page.screenshot({ path, fullPage: false });
56+
console.log(` ✓ ${name}.png`);
57+
}
58+
59+
async function main() {
60+
const browser = await chromium.launch();
61+
const ctx = await browser.newContext({
62+
viewport: { width: 1440, height: 900 },
63+
deviceScaleFactor: 2,
64+
});
65+
const page = await ctx.newPage();
66+
67+
console.log(`Capturing ${BASE_URL} -> ${OUT_DIR}`);
68+
69+
// --- Logged-OUT shots first (login page + the public respondent runtime) ---
70+
await page.goto(`${BASE_URL}/auth/login`);
71+
await settle(page);
72+
await shot(page, "login");
73+
74+
await page.goto(`${BASE_URL}/${ORG_SLUG}/s/${SURVEY_ID}`);
75+
await settle(page, { island: true });
76+
await shot(page, "runtime");
77+
78+
// --- Log in (read-only thereafter) ---
79+
await page.goto(`${BASE_URL}/auth/login`);
80+
await page.fill('input[name="email"]', EMAIL);
81+
await page.fill('input[name="password"]', PASSWORD);
82+
await Promise.all([
83+
page.waitForLoadState("networkidle").catch(() => {}),
84+
page.click('button[type="submit"]'),
85+
]);
86+
await page.waitForTimeout(800);
87+
if (/\/auth\/login/.test(page.url())) {
88+
throw new Error(`Login failed (still on ${page.url()}). Check EMAIL/PASSWORD.`);
89+
}
90+
91+
// --- Authenticated admin shots ---
92+
await page.goto(`${BASE_URL}/${ORG_SLUG}/`);
93+
await settle(page);
94+
await shot(page, "dashboard");
95+
96+
await page.goto(`${BASE_URL}/${ORG_SLUG}/surveys`);
97+
await settle(page);
98+
await shot(page, "surveys");
99+
100+
await page.goto(`${BASE_URL}/${ORG_SLUG}/surveys/${SURVEY_ID}/edit`);
101+
await settle(page, { island: true });
102+
await shot(page, "builder");
103+
104+
await page.goto(`${BASE_URL}/${ORG_SLUG}/surveys/${SURVEY_ID}/results`);
105+
await settle(page);
106+
await shot(page, "results");
107+
108+
await page.goto(`${BASE_URL}/${ORG_SLUG}/contacts`);
109+
await settle(page);
110+
await shot(page, "contacts");
111+
112+
await browser.close();
113+
console.log("Done.");
114+
}
115+
116+
main().catch((err) => {
117+
console.error(err);
118+
process.exit(1);
119+
});

scripts/package-lock.json

Lines changed: 76 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)