glam/docs/plan/frontend/07-quick-start-guide.md
kempersc fa5680f0dd Add initial versions of custodian hub UML diagrams in Mermaid and PlantUML formats
- Introduced custodian_hub_v3.mmd, custodian_hub_v4_final.mmd, and custodian_hub_v5_FINAL.mmd for Mermaid representation.
- Created custodian_hub_FINAL.puml and custodian_hub_v3.puml for PlantUML representation.
- Defined entities such as CustodianReconstruction, Identifier, TimeSpan, Agent, CustodianName, CustodianObservation, ReconstructionActivity, Appellation, ConfidenceMeasure, Custodian, LanguageCode, and SourceDocument.
- Established relationships and associations between entities, including temporal extents, observations, and reconstruction activities.
- Incorporated enumerations for various types, statuses, and classifications relevant to custodians and their activities.
2025-11-22 14:33:51 +01:00

20 KiB

GLAM Frontend Quick Start Guide

🚀 Get Started in 15 Minutes

This guide will help you set up and start developing the GLAM frontend immediately.


Prerequisites Check

Before starting, ensure you have:

# Check Node.js version (need 18+)
node --version  # Should show v18.x.x or higher

# Check npm version
npm --version   # Should show 9.x.x or higher

# Check Git
git --version   # Any recent version

If any are missing:


Step 1: Project Initialization (5 minutes)

# Navigate to the GLAM project
cd /Users/kempersc/apps/glam

# Create frontend directory
mkdir -p frontend
cd frontend

# Initialize Vite project with React + TypeScript
npm create vite@latest . -- --template react-ts

# Install dependencies (this will take 2-3 minutes)
npm install

Verify Installation

# Start the dev server
npm run dev

# You should see:
# ➜  Local:   http://localhost:5173/
# ➜  press h to show help

Open http://localhost:5173/ in your browser. You should see the Vite + React welcome page. 🎉

Stop the server (Ctrl+C) before continuing.


Step 2: Project Structure Setup (3 minutes)

# Create the directory structure
mkdir -p src/{components,lib,hooks,stores,types,utils,pages,styles}
mkdir -p src/components/{layout,visualizations,forms,ui}
mkdir -p src/lib/{rdf,storage,api,utils}
mkdir -p src/components/visualizations/{GraphView,Timeline,Map,Hierarchy}
mkdir -p tests/{unit,integration,e2e}
mkdir -p public/assets

# Verify structure
tree -L 3 src/  # or use 'ls -R src/' if tree not installed

Expected output:

src/
├── components/
│   ├── forms/
│   ├── layout/
│   ├── ui/
│   └── visualizations/
│       ├── GraphView/
│       ├── Hierarchy/
│       ├── Map/
│       └── Timeline/
├── hooks/
├── lib/
│   ├── api/
│   ├── rdf/
│   ├── storage/
│   └── utils/
├── pages/
├── stores/
├── styles/
├── types/
└── utils/

Step 3: Install Core Dependencies (5 minutes)

# Core dependencies
npm install d3 @types/d3 n3 zustand @tanstack/react-query
npm install react-router-dom axios
npm install leaflet @types/leaflet
npm install date-fns lodash @types/lodash

# Dev dependencies
npm install -D vitest @vitest/ui @testing-library/react
npm install -D @testing-library/jest-dom @testing-library/user-event
npm install -D @playwright/test
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install -D prettier eslint-config-prettier eslint-plugin-prettier
npm install -D @types/node

# Install Playwright browsers (takes 1-2 minutes)
npx playwright install

Verify Dependencies

# Check package.json
cat package.json | grep -A 20 '"dependencies"'

Step 4: Configuration Files (2 minutes)

TypeScript Configuration

cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "strict": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* Path aliases */
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
EOF

Vite Configuration

cat > vite.config.ts << 'EOF'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
      },
    },
  },
})
EOF

Vitest Configuration

cat > vitest.config.ts << 'EOF'
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './tests/setup.ts',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'tests/',
        '**/*.test.{ts,tsx}',
        '**/*.spec.{ts,tsx}',
      ],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})
EOF

ESLint Configuration

cat > .eslintrc.json << 'EOF'
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "react", "react-hooks"],
  "rules": {
    "react/react-in-jsx-scope": "off",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}
EOF

Prettier Configuration

cat > .prettierrc << 'EOF'
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false
}
EOF

Step 5: First Component - IndexedDB Manager

Let's migrate the first utility from example_ld.

Create Type Definitions

cat > src/types/database.ts << 'EOF'
export interface RdfData {
  id: string;
  data: string;
  format: RdfFormat;
  metadata: RdfMetadata;
  timestamp: Date;
}

export interface RdfMetadata {
  tripleCount: number;
  source: string;
  description?: string;
}

export type RdfFormat = 
  | 'text/turtle'
  | 'application/n-triples'
  | 'application/ld+json'
  | 'application/rdf+xml';

export interface DatabaseConfig {
  name: string;
  version: number;
  stores: StoreConfig[];
}

export interface StoreConfig {
  name: string;
  keyPath: string;
  indexes?: IndexConfig[];
}

export interface IndexConfig {
  name: string;
  keyPath: string;
  unique: boolean;
}
EOF

Create IndexedDB Manager

cat > src/lib/storage/indexed-db.ts << 'EOF'
import type { RdfData, DatabaseConfig } from '@/types/database';

export class IndexedDBManager {
  private db: IDBDatabase | null = null;
  private readonly config: DatabaseConfig = {
    name: 'GLAM_RDF_DB',
    version: 1,
    stores: [
      {
        name: 'rdf_data',
        keyPath: 'id',
        indexes: [
          { name: 'timestamp', keyPath: 'timestamp', unique: false },
          { name: 'format', keyPath: 'format', unique: false },
        ],
      },
    ],
  };

  async initialize(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.config.name, this.config.version);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        
        this.config.stores.forEach((storeConfig) => {
          if (!db.objectStoreNames.contains(storeConfig.name)) {
            const store = db.createObjectStore(storeConfig.name, {
              keyPath: storeConfig.keyPath,
            });

            storeConfig.indexes?.forEach((index) => {
              store.createIndex(index.name, index.keyPath, {
                unique: index.unique,
              });
            });
          }
        });
      };
    });
  }

  async save(data: RdfData): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(['rdf_data'], 'readwrite');
      const store = transaction.objectStore('rdf_data');
      const request = store.put(data);

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  async get(id: string): Promise<RdfData | null> {
    if (!this.db) throw new Error('Database not initialized');

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(['rdf_data'], 'readonly');
      const store = transaction.objectStore('rdf_data');
      const request = store.get(id);

      request.onsuccess = () => resolve(request.result || null);
      request.onerror = () => reject(request.error);
    });
  }

  async getAll(): Promise<RdfData[]> {
    if (!this.db) throw new Error('Database not initialized');

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(['rdf_data'], 'readonly');
      const store = transaction.objectStore('rdf_data');
      const request = store.getAll();

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async delete(id: string): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(['rdf_data'], 'readwrite');
      const store = transaction.objectStore('rdf_data');
      const request = store.delete(id);

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  async clear(): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(['rdf_data'], 'readwrite');
      const store = transaction.objectStore('rdf_data');
      const request = store.clear();

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
}

// Singleton instance
export const db = new IndexedDBManager();
EOF

Create React Hook

cat > src/hooks/useDatabase.ts << 'EOF'
import { useState, useEffect } from 'react';
import { db } from '@/lib/storage/indexed-db';
import type { RdfData } from '@/types/database';

export const useDatabase = () => {
  const [isReady, setIsReady] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    db.initialize()
      .then(() => setIsReady(true))
      .catch(setError);
  }, []);

  const saveData = async (data: RdfData) => {
    try {
      await db.save(data);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  };

  const getData = async (id: string) => {
    try {
      return await db.get(id);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  };

  const getAllData = async () => {
    try {
      return await db.getAll();
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  };

  const deleteData = async (id: string) => {
    try {
      await db.delete(id);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  };

  const clearAll = async () => {
    try {
      await db.clear();
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  };

  return {
    isReady,
    error,
    saveData,
    getData,
    getAllData,
    deleteData,
    clearAll,
  };
};
EOF

Write Tests

# Create test setup
cat > tests/setup.ts << 'EOF'
import '@testing-library/jest-dom';
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';

afterEach(() => {
  cleanup();
});
EOF

# Create test for IndexedDB manager
cat > src/lib/storage/indexed-db.test.ts << 'EOF'
import { describe, it, expect, beforeEach } from 'vitest';
import { IndexedDBManager } from './indexed-db';
import type { RdfData } from '@/types/database';

describe('IndexedDBManager', () => {
  let dbManager: IndexedDBManager;

  beforeEach(async () => {
    dbManager = new IndexedDBManager();
    await dbManager.initialize();
    await dbManager.clear();
  });

  it('should initialize database', async () => {
    expect(dbManager).toBeDefined();
  });

  it('should save and retrieve data', async () => {
    const testData: RdfData = {
      id: 'test-1',
      data: '<http://example.org/s> <http://example.org/p> <http://example.org/o> .',
      format: 'text/turtle',
      metadata: {
        tripleCount: 1,
        source: 'test',
      },
      timestamp: new Date(),
    };

    await dbManager.save(testData);
    const retrieved = await dbManager.get('test-1');

    expect(retrieved).toBeDefined();
    expect(retrieved?.id).toBe('test-1');
    expect(retrieved?.format).toBe('text/turtle');
  });

  it('should delete data', async () => {
    const testData: RdfData = {
      id: 'test-2',
      data: 'test',
      format: 'text/turtle',
      metadata: { tripleCount: 0, source: 'test' },
      timestamp: new Date(),
    };

    await dbManager.save(testData);
    await dbManager.delete('test-2');
    const retrieved = await dbManager.get('test-2');

    expect(retrieved).toBeNull();
  });

  it('should get all data', async () => {
    const testData1: RdfData = {
      id: 'test-3',
      data: 'test1',
      format: 'text/turtle',
      metadata: { tripleCount: 0, source: 'test' },
      timestamp: new Date(),
    };

    const testData2: RdfData = {
      id: 'test-4',
      data: 'test2',
      format: 'application/n-triples',
      metadata: { tripleCount: 0, source: 'test' },
      timestamp: new Date(),
    };

    await dbManager.save(testData1);
    await dbManager.save(testData2);

    const all = await dbManager.getAll();
    expect(all).toHaveLength(2);
  });
});
EOF

Run Tests

# Run the test
npm test

# You should see output like:
# ✓ src/lib/storage/indexed-db.test.ts (4 tests) 
#   ✓ IndexedDBManager
#     ✓ should initialize database
#     ✓ should save and retrieve data
#     ✓ should delete data
#     ✓ should get all data

Step 6: Create First Page Component

cat > src/pages/Home.tsx << 'EOF'
import { useDatabase } from '@/hooks/useDatabase';

export const Home = () => {
  const { isReady, error, getAllData } = useDatabase();

  const handleLoadData = async () => {
    try {
      const data = await getAllData();
      console.log('Loaded data:', data);
      alert(`Loaded ${data.length} RDF datasets`);
    } catch (err) {
      console.error('Error loading data:', err);
    }
  };

  if (error) {
    return (
      <div style={{ padding: '2rem' }}>
        <h1>Error</h1>
        <p>{error.message}</p>
      </div>
    );
  }

  if (!isReady) {
    return (
      <div style={{ padding: '2rem' }}>
        <h1>Loading...</h1>
        <p>Initializing database...</p>
      </div>
    );
  }

  return (
    <div style={{ padding: '2rem' }}>
      <h1>GLAM Heritage Custodian Ontology</h1>
      <p>Welcome to the RDF visualization platform.</p>
      
      <div style={{ marginTop: '2rem' }}>
        <button 
          onClick={handleLoadData}
          style={{
            padding: '0.5rem 1rem',
            fontSize: '1rem',
            cursor: 'pointer',
          }}
        >
          Load Data
        </button>
      </div>

      <div style={{ marginTop: '2rem' }}>
        <h2>Quick Stats</h2>
        <ul>
          <li>Database Status: {isReady ? '✅ Ready' : '❌ Not Ready'}</li>
          <li>RDF Schemas: Available at /schemas/20251121/rdf/</li>
          <li>UML Diagrams: Available at /schemas/20251121/uml/</li>
        </ul>
      </div>
    </div>
  );
};
EOF

Update App.tsx

cat > src/App.tsx << 'EOF'
import { Home } from './pages/Home';

function App() {
  return <Home />;
}

export default App;
EOF

Step 7: Test Your Application

# Start the development server
npm run dev

Visit http://localhost:5173/ and you should see:

  • "GLAM Heritage Custodian Ontology" heading
  • "Database Status: Ready"
  • A "Load Data" button

Click the button - it should show an alert with "Loaded 0 RDF datasets" (since we haven't added any data yet).


Step 8: Update package.json Scripts

# Add these scripts to package.json (merge with existing)
cat >> package.json << 'EOF'
Add these to your "scripts" section:

"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "test": "vitest",
  "test:ui": "vitest --ui",
  "test:coverage": "vitest --coverage",
  "test:e2e": "playwright test",
  "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
  "lint:fix": "eslint src --ext ts,tsx --fix",
  "format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"",
  "type-check": "tsc --noEmit"
}
EOF

Success! You're Ready to Develop

You now have:

  • A working Vite + React + TypeScript project
  • Complete directory structure
  • All core dependencies installed
  • TypeScript, ESLint, Prettier configured
  • First utility (IndexedDB manager) migrated from example_ld
  • First React hook (useDatabase)
  • First page component (Home)
  • Working tests with Vitest

Next Steps

Follow the master checklist at docs/plan/frontend/05-master-checklist.md:

  • Day 6-7: Complete RDF Parser migration
  • Day 8-9: Utilities migration
  • Day 10: API client migration

Option 2: Jump to Visualizations

If you want to see something visual quickly:

  • Skip to Phase 3 (Week 5) in the master checklist
  • Start building the D3 force-directed graph
  • Load actual RDF data from /schemas/20251121/rdf/

Option 3: Explore Example LD

Study the original implementation:

cd /Users/kempersc/apps/example_ld
cat js/db.js          # Compare with your implementation
cat js/rdfParser.js   # Next migration target

Useful Commands Reference

# Development
npm run dev              # Start dev server
npm run build            # Production build
npm run preview          # Preview production build

# Testing
npm test                 # Run tests in watch mode
npm run test:ui          # Visual test interface
npm run test:coverage    # Coverage report

# Code Quality
npm run lint             # Check for errors
npm run lint:fix         # Auto-fix errors
npm run format           # Format code
npm run type-check       # TypeScript check

# Debugging
npm run dev -- --debug   # Debug mode
npm run dev -- --host    # Expose to network

Troubleshooting

Problem: "Cannot find module '@/...'"

Solution: Make sure tsconfig.json has the path alias configured and restart your IDE.

Problem: IndexedDB tests fail

Solution: Tests run in jsdom environment which has limited IndexedDB support. Consider using fake-indexeddb package for testing.

Problem: Port 5173 already in use

Solution:

# Kill the process using the port
lsof -ti:5173 | xargs kill -9

# Or use a different port
npm run dev -- --port 3000

Problem: Type errors with D3

Solution: Make sure you installed both d3 and @types/d3:

npm install d3 @types/d3

Getting Help

  • Documentation: docs/plan/frontend/ directory
  • Master Checklist: docs/plan/frontend/05-master-checklist.md
  • Visual Roadmap: docs/plan/frontend/06-visual-roadmap.md
  • Example LD Reference: /Users/kempersc/apps/example_ld

Pro Tips

  1. Use TypeScript Strict Mode: It catches bugs early
  2. Write Tests First: TDD leads to better design
  3. Keep Components Small: Easy to test and maintain
  4. Use React Query: Simplifies server state management
  5. Profile Performance: Use React DevTools Profiler

Congratulations! 🎉 You're now ready to build the GLAM frontend.

Estimated Time to Full Implementation: 16 weeks following the master checklist.

Next milestone: Complete Phase 1 (Core Libraries) in 2 weeks.


Last Updated: 2025-11-22
Version: 1.0
Guide Completion Time: ~15 minutes