Skip to content

Commit cc3e2da

Browse files
committed
feat: Create random demo, personas in groups, BDD no webServer, docs
1 parent 64f8c1a commit cc3e2da

File tree

13 files changed

+329
-45
lines changed

13 files changed

+329
-45
lines changed

AGENTS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Context for AI assistants working on this repo.
77
**tb-solid-pod** is a browser-based personal data pod inspired by the [Solid Project](https://solidproject.org/), built with [TinyBase](https://tinybase.org/) for reactive state and LocalStorage. It is **not** a real Solid server (no LDP, no WebID-TLS); it simulates Solid-style data (personas, contacts, groups, type indexes, file metadata) in a single-page app.
88

99
- **Dual interface**: graphical UI (tabs, forms, lists) + CLI terminal.
10-
- **Data**: personas (WebID-style), contacts (including agents), groups (org/team/group), type indexes, virtual file system with metadata, settings/preferences.
10+
- **Data**: personas (WebID-style), contacts (including agents), groups (org/team/group) with membership of contacts and your personas, type indexes, virtual file system with metadata, settings/preferences.
1111
- **Stack**: React, TinyBase, Zod, Vite, TypeScript. Vocabularies: FOAF, vCard, Dublin Core, W3C Org, `@inrupt/vocab-*`.
1212

1313
## Repo layout
@@ -21,7 +21,7 @@ Context for AI assistants working on this repo.
2121
| `src/utils/` | settings, storeExport, typeIndex helpers, validation. |
2222
| `src/cli/` | CliTerminal, command registry, parse-args, types. |
2323
| `src/components/` | PersonaList/Form, ContactList/Form, GroupList/Form, MembershipManager, FileMetadataPanel. |
24-
| `docs/` | CODING_GUIDELINES.md, DESIGN.md, IMPLEMENTATION_PLAN.md, testing/. |
24+
| `docs/` | CODING_GUIDELINES.md, DESIGN.md, IMPLEMENTATION_PLAN.md, TEST_PLAN.md, testing/. |
2525

2626
## What’s done so far
2727

@@ -70,7 +70,9 @@ When someone wants to use this in an app they’re working on, point them to the
7070

7171
## Useful docs
7272

73-
- **README.md** – Overview, limitations, Use as a library (Zod + JSON Schema), Integration Guide (copy-paste vs install-from-GitHub), Getting Started (Node note, Live demo + 404 troubleshooting), CLI command list.
73+
- **README.md** – Overview, limitations, Use as a library (Zod + JSON Schema), Integration Guide (copy-paste vs install-from-GitHub), Getting Started (Node note, Live demo + 404 troubleshooting), Testing (unit, BDD, Storybook link), CLI command list.
7474
- **docs/CODING_GUIDELINES.md** – TypeScript (strict types, no sloppy types), short functions, simple React components, naming, file length.
7575
- **docs/IMPLEMENTATION_PLAN.md** – Feature/phases.
76+
- **docs/TEST_PLAN.md** – Test phases and verification.
77+
- **docs/testing/** – Unit (unit-tests.md), BDD/E2E (bdd-tests.md), Storybook (storybook.md); BDD does not start the dev server (start it manually).
7678
- **DESIGN.md** – Design notes.

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ This library provides a complete foundation for **user-owned social data** in we
2525
### Groups & Organizations
2626
- Three group types: Organizations, Teams, and informal Groups
2727
- W3C Organization Ontology (org:) vocabulary
28-
- Membership management with contact linking
28+
- Membership management: add contacts and your own personas to groups
2929
- Group metadata: name, description, URL, logo
3030

3131
### File Storage with Metadata
@@ -47,7 +47,7 @@ This library provides a complete foundation for **user-owned social data** in we
4747
- Persona schema includes optional `solid:publicTypeIndex` and `solid:privateTypeIndex` links
4848

4949
### Dual Interface
50-
- **Graphical UI**: Tab-based navigation with forms and lists
50+
- **Graphical UI**: Tab-based navigation with forms and lists. Personas, Contacts, and Groups tabs each have a **Create random** button that opens the form with sample data for quick try-out.
5151
- **CLI Terminal**: Full command-line interface for power users
5252

5353
## Benefits for Social Applications
@@ -441,13 +441,14 @@ clear Clear terminal
441441
## Testing
442442

443443
- **Unit tests (Vitest):** `npm test` or `npm run test:run`; coverage: `npm run test:coverage`
444+
- **Storybook:** `npm run storybook`http://localhost:6006 (component development). See [docs/testing/](docs/testing/README.md).
444445
- **BDD / E2E (Playwright):** Generate specs from Gherkin, then run Playwright:
445446
```bash
446447
npx bddgen && npx playwright test
447448
```
448449
Or use the scripts: `npm run test:e2e` or `npm run test:bdd` (same thing). With browser visible: `npm run test:e2e:headed`.
449450

450-
**Start the server manually (recommended if E2E hangs):** In one terminal start the app; in another run the tests. Playwright will reuse the existing server on port 5173 (`reuseExistingServer` when not in CI).
451+
**Start the server first (required):** The BDD/E2E command does not start the dev server. In one terminal start the app; in another run the tests.
451452
1. **Terminal 1:** `npm run dev` — leave it running (app at http://localhost:5173).
452453
2. **Terminal 2:** `npx bddgen && npx playwright test` (or `npm run test:e2e`).
453454

docs/TEST_PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
- @playwright/test, playwright-bdd; `npx playwright install` (chromium)
108108

109109
### Configuration
110-
- `playwright.config.ts` — Playwright config with defineBddConfig, webServer (port 5173), baseURL
110+
- `playwright.config.ts` — Playwright config with defineBddConfig, baseURL; no webServer (start dev server manually)
111111

112112
### Feature Files and Steps
113113
- `tests/features/*.feature` — app, cli-contacts, cli-personas, cli-navigation, contacts, personas

docs/testing/bdd-tests.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ To inspect the latest run: open the HTML report, or look at `test-results/.last-
2424

2525
## Layout
2626

27-
- **Config:** `playwright.config.ts` (defineBddConfig, baseURL, webServer)
27+
- **Config:** `playwright.config.ts` (defineBddConfig, baseURL; no webServer — start the app yourself)
2828
- **Features:** `tests/features/*.feature` (Gherkin)
2929
- **Steps:** `tests/features/steps/*.ts` (Given/When/Then)
3030
- **Generated specs:** `.features-gen/` (output of `npx bddgen`)
3131

32-
## Running with the app already up
32+
## Starting the app (required)
3333

34-
If E2E hangs when Playwright starts the dev server, run the app yourself and reuse it:
34+
The BDD/E2E command does **not** start the dev server. Start the app first, then run the tests:
3535

3636
1. **Terminal 1:** `npm run dev` (leave running; app at http://localhost:5173).
3737
2. **Terminal 2:** `npx bddgen && npx playwright test`.
3838

39-
Playwright will reuse the server on 5173 when not in CI. For a different port, set `E2E_BASE_URL` (e.g. `E2E_BASE_URL=http://localhost:3000 npx playwright test`).
39+
For a different port, set `E2E_BASE_URL` (e.g. `E2E_BASE_URL=http://localhost:3000 npx playwright test`).
4040

4141
## Manual BDD steps
4242

playwright.config.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,5 @@ export default defineConfig({
2424
projects: [
2525
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
2626
],
27-
webServer: {
28-
command: 'npm run dev',
29-
url: process.env.E2E_BASE_URL || 'http://localhost:5173',
30-
reuseExistingServer: !process.env.CI,
31-
timeout: 60_000,
32-
},
27+
// No webServer: start the dev server yourself (e.g. npm run dev) before running BDD/E2E tests.
3328
});

src/App.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,73 @@ const getDefaultContent = (): [Record<string, Record<string, Record<string, unkn
6262
{}
6363
];
6464

65+
// ==========================================
66+
// RANDOM DEMO DATA (for "Add random" buttons)
67+
// ==========================================
68+
function pick<T>(arr: readonly T[]): T {
69+
return arr[Math.floor(Math.random() * arr.length)];
70+
}
71+
72+
function getRandomPersonaFormValues(): Record<string, string> {
73+
const first = pick(['Alex', 'Sam', 'Jordan', 'Casey', 'Riley', 'Morgan', 'Quinn', 'Avery']);
74+
const last = pick(['Smith', 'Chen', 'Williams', 'Brown', 'Lee', 'Jones', 'Garcia']);
75+
const name = `${first} ${last}`;
76+
const nick = pick([first.toLowerCase(), `${first}${last.slice(0, 1)}`.toLowerCase(), `${first}_${last}`.toLowerCase()]);
77+
return {
78+
name,
79+
nickname: nick,
80+
givenName: first,
81+
familyName: last,
82+
email: `${nick}@example.com`,
83+
phone: `+1-555-${Math.floor(100 + Math.random() * 900)}-${Math.floor(1000 + Math.random() * 9000)}`,
84+
bio: pick(['Developer and designer.', 'Works on the Solid ecosystem.', 'Building for the decentralized web.']),
85+
homepage: `https://${nick}.example.com`,
86+
image: '',
87+
oidcIssuer: '',
88+
inbox: '',
89+
preferencesFile: '',
90+
publicTypeIndex: '',
91+
privateTypeIndex: '',
92+
};
93+
}
94+
95+
function getRandomContactFormValues(): Record<string, string | boolean> {
96+
const first = pick(['Jamie', 'Taylor', 'Robin', 'Drew', 'Skyler', 'Parker']);
97+
const last = pick(['Miller', 'Davis', 'Wilson', 'Martinez', 'Anderson']);
98+
const name = `${first} ${last}`;
99+
const nick = first.toLowerCase();
100+
const isAgent = Math.random() > 0.7;
101+
return {
102+
name,
103+
nickname: nick,
104+
email: `${nick}@company.com`,
105+
phone: `+1-555-${Math.floor(200 + Math.random() * 800)}-${Math.floor(1000 + Math.random() * 9000)}`,
106+
url: `https://${nick}.company.com`,
107+
photo: '',
108+
notes: pick(['Met at conference.', 'Collaborator on project X.', '']),
109+
organization: pick(['Acme Inc', 'Tech Corp', 'Solid Labs', '']),
110+
role: pick(['Engineer', 'Designer', 'PM', '']),
111+
webId: '',
112+
isAgent,
113+
agentCategory: isAgent ? pick(['DeveloperApplication', 'SocialNetworking', 'Utilities']) : '',
114+
};
115+
}
116+
117+
type GroupType = 'group' | 'organization' | 'team';
118+
function getRandomGroupFormValues(): Record<string, string> {
119+
const adj = pick(['Core', 'Product', 'Platform', 'Research', 'Community']);
120+
const noun = pick(['Team', 'Squad', 'Group', 'Org', 'Circle']);
121+
const name = `${adj} ${noun}`;
122+
const type: GroupType = pick(['group', 'organization', 'team']);
123+
return {
124+
name,
125+
type,
126+
description: pick([`The ${name} works on key initiatives.`, `Internal ${type}.`, '']),
127+
url: `https://${name.toLowerCase().replace(/\s+/g, '-')}.company.com`,
128+
logo: '',
129+
};
130+
}
131+
65132
// ==========================================
66133
// MIME & IMAGE HELPERS
67134
// ==========================================
@@ -450,12 +517,15 @@ export default function App() {
450517
const [selectedPersonaId, setSelectedPersonaId] = useState<string | undefined>();
451518
const [personaFormOpen, setPersonaFormOpen] = useState(false);
452519
const [editingPersonaId, setEditingPersonaId] = useState<string | undefined>();
520+
const [personaFormInitial, setPersonaFormInitial] = useState<Record<string, string> | undefined>();
453521
const [selectedContactId, setSelectedContactId] = useState<string | undefined>();
454522
const [contactFormOpen, setContactFormOpen] = useState(false);
455523
const [editingContactId, setEditingContactId] = useState<string | undefined>();
524+
const [contactFormInitial, setContactFormInitial] = useState<Record<string, string | boolean> | undefined>();
456525
const [selectedGroupId, setSelectedGroupId] = useState<string | undefined>();
457526
const [groupFormOpen, setGroupFormOpen] = useState(false);
458527
const [editingGroupId, setEditingGroupId] = useState<string | undefined>();
528+
const [groupFormInitial, setGroupFormInitial] = useState<Record<string, string> | undefined>();
459529
const [managingMembersGroupId, setManagingMembersGroupId] = useState<string | undefined>();
460530

461531
const row = useRow('resources', currentUrl, store ?? undefined) as ResourceRow | undefined;
@@ -734,6 +804,12 @@ export default function App() {
734804
}}
735805
onCreate={() => {
736806
setEditingPersonaId(undefined);
807+
setPersonaFormInitial(undefined);
808+
setPersonaFormOpen(true);
809+
}}
810+
onCreateRandom={() => {
811+
setEditingPersonaId(undefined);
812+
setPersonaFormInitial(getRandomPersonaFormValues());
737813
setPersonaFormOpen(true);
738814
}}
739815
/>
@@ -742,13 +818,16 @@ export default function App() {
742818
store={store}
743819
baseUrl={BASE_URL}
744820
personaId={editingPersonaId}
821+
initialValues={personaFormInitial}
745822
onSave={() => {
746823
setPersonaFormOpen(false);
747824
setEditingPersonaId(undefined);
825+
setPersonaFormInitial(undefined);
748826
}}
749827
onCancel={() => {
750828
setPersonaFormOpen(false);
751829
setEditingPersonaId(undefined);
830+
setPersonaFormInitial(undefined);
752831
}}
753832
/>
754833
)}
@@ -774,6 +853,12 @@ export default function App() {
774853
}}
775854
onCreate={() => {
776855
setEditingContactId(undefined);
856+
setContactFormInitial(undefined);
857+
setContactFormOpen(true);
858+
}}
859+
onCreateRandom={() => {
860+
setEditingContactId(undefined);
861+
setContactFormInitial(getRandomContactFormValues());
777862
setContactFormOpen(true);
778863
}}
779864
/>
@@ -782,13 +867,16 @@ export default function App() {
782867
store={store}
783868
baseUrl={BASE_URL}
784869
contactId={editingContactId}
870+
initialValues={contactFormInitial}
785871
onSave={() => {
786872
setContactFormOpen(false);
787873
setEditingContactId(undefined);
874+
setContactFormInitial(undefined);
788875
}}
789876
onCancel={() => {
790877
setContactFormOpen(false);
791878
setEditingContactId(undefined);
879+
setContactFormInitial(undefined);
792880
}}
793881
/>
794882
)}
@@ -817,6 +905,12 @@ export default function App() {
817905
}}
818906
onCreate={() => {
819907
setEditingGroupId(undefined);
908+
setGroupFormInitial(undefined);
909+
setGroupFormOpen(true);
910+
}}
911+
onCreateRandom={() => {
912+
setEditingGroupId(undefined);
913+
setGroupFormInitial(getRandomGroupFormValues());
820914
setGroupFormOpen(true);
821915
}}
822916
/>
@@ -825,13 +919,16 @@ export default function App() {
825919
store={store}
826920
baseUrl={BASE_URL}
827921
groupId={editingGroupId}
922+
initialValues={groupFormInitial}
828923
onSave={() => {
829924
setGroupFormOpen(false);
830925
setEditingGroupId(undefined);
926+
setGroupFormInitial(undefined);
831927
}}
832928
onCancel={() => {
833929
setGroupFormOpen(false);
834930
setEditingGroupId(undefined);
931+
setGroupFormInitial(undefined);
835932
}}
836933
/>
837934
)}

src/components/ContactForm.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ interface ContactFormProps {
1515
store: Store;
1616
baseUrl: string;
1717
contactId?: string; // If provided, we're editing; otherwise creating
18+
/** Pre-fill form when creating (e.g. "Add random" demo). */
19+
initialValues?: Partial<FormData>;
1820
onSave: () => void;
1921
onCancel: () => void;
2022
}
@@ -53,16 +55,19 @@ const ContactForm: React.FC<ContactFormProps> = ({
5355
store,
5456
baseUrl,
5557
contactId,
58+
initialValues,
5659
onSave,
5760
onCancel,
5861
}) => {
5962
const isEditing = !!contactId;
6063
const [form, setForm] = useState<FormData>(emptyForm);
6164
const [error, setError] = useState<string | null>(null);
65+
const appliedInitialRef = React.useRef(false);
6266

6367
// Load existing contact data when editing
6468
useEffect(() => {
6569
if (contactId) {
70+
appliedInitialRef.current = false;
6671
const record = store.getRow(TABLE_NAME, contactId) as Record<string, unknown>;
6772
if (record) {
6873
const email = record[VCARD.hasEmail] as string | undefined;
@@ -90,6 +95,15 @@ const ContactForm: React.FC<ContactFormProps> = ({
9095
}
9196
}, [contactId, store]);
9297

98+
// Pre-fill form when creating with initialValues (e.g. "Add random" demo)
99+
useEffect(() => {
100+
if (!contactId && initialValues && !appliedInitialRef.current) {
101+
setForm((prev) => ({ ...emptyForm, ...prev, ...initialValues }));
102+
appliedInitialRef.current = true;
103+
}
104+
if (contactId) appliedInitialRef.current = false;
105+
}, [contactId, initialValues]);
106+
93107
const handleChange = (field: keyof FormData) => (
94108
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
95109
) => {

src/components/ContactList.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ interface ContactListProps {
1111
onEdit: (id: string) => void;
1212
onDelete: (id: string) => void;
1313
onCreate: () => void;
14+
/** Open create form with random demo values pre-filled. */
15+
onCreateRandom?: () => void;
1416
selectedId?: string;
1517
}
1618

@@ -28,6 +30,7 @@ const ContactList: React.FC<ContactListProps> = ({
2830
onEdit,
2931
onDelete,
3032
onCreate,
33+
onCreateRandom,
3134
selectedId,
3235
}) => {
3336
const contacts = useTable(TABLE_NAME, store) as Record<string, Record<string, unknown>>;
@@ -66,9 +69,16 @@ const ContactList: React.FC<ContactListProps> = ({
6669
<div style={styles.container}>
6770
<div style={styles.header}>
6871
<h3 style={styles.title}>Contacts</h3>
69-
<button style={styles.createBtn} onClick={onCreate}>
70-
+ Add Contact
71-
</button>
72+
<div style={styles.headerActions}>
73+
{onCreateRandom && (
74+
<button style={styles.randomBtn} onClick={onCreateRandom} title="Open form with random demo values">
75+
Create random
76+
</button>
77+
)}
78+
<button style={styles.createBtn} onClick={onCreate}>
79+
+ Add Contact
80+
</button>
81+
</div>
7282
</div>
7383

7484
<div style={styles.toolbar}>
@@ -197,6 +207,21 @@ const styles: Record<string, CSSProperties> = {
197207
background: '#fafafa',
198208
borderBottom: '1px solid #eee',
199209
},
210+
headerActions: {
211+
display: 'flex',
212+
gap: 8,
213+
alignItems: 'center',
214+
},
215+
randomBtn: {
216+
padding: '8px 14px',
217+
background: '#fff',
218+
color: '#555',
219+
border: '1px solid #ccc',
220+
borderRadius: 6,
221+
cursor: 'pointer',
222+
fontWeight: 500,
223+
fontSize: 13,
224+
},
200225
title: {
201226
margin: 0,
202227
fontSize: 16,

0 commit comments

Comments
 (0)