Portfolio Page v0.0.1 with NocoDB Integration

Public portfolio page refactored to fetch companies from NocoDB with multi-mode logo switching and transparent SVG color treatment.

Claude
Claude Claude

Changelog - 2025-12-25 (#1)

Dark Matter Site: Portfolio Page v0.0.1 with NocoDB Integration

Overview

Refactored the public portfolio page to dynamically fetch portfolio companies from NocoDB while maintaining a static JSON fallback. Implemented sophisticated logo handling that supports light mode, dark mode, and vibrant mode with automatic switching—plus special CSS filter treatment for transparent SVG logos.

Key deliverables:

  • Dynamic Data Fetching: Portfolio page now pulls from NocoDB at build time
  • Graceful Fallback: Falls back to static JSON when NocoDB not configured
  • Multi-Mode Logo Switching: Separate logos for light, dark, and vibrant themes
  • Transparent Logo Treatment: CSS filters colorize monochrome transparent SVGs
  • 19 Portfolio Company Logos: Added trademark assets for portfolio companies

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                           Portfolio Page Data Flow                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                         Build Time                                   │   │
│   ├─────────────────────────────────────────────────────────────────────┤   │
│   │                                                                      │   │
│   │   isNocoDBConfigured()?                                             │   │
│   │         │                                                            │   │
│   │         ├──── YES ──▶ getPortfolioCompanies() ──▶ NocoDB API        │   │
│   │         │                                              │             │   │
│   │         │                                              ▼             │   │
│   │         │                                    transformToPortfolio()  │   │
│   │         │                                              │             │   │
│   │         └──── NO ───▶ import staticPortfolioData ──────┤             │   │
│   │                                                        │             │   │
│   │                                                        ▼             │   │
│   │                                              portfolioData[]         │   │
│   │                                                        │             │   │
│   └────────────────────────────────────────────────────────┼─────────────┘   │
│                                                            │                 │
│   ┌────────────────────────────────────────────────────────┼─────────────┐   │
│   │                         Render Time                    │             │   │
│   ├────────────────────────────────────────────────────────┼─────────────┤   │
│   │                                                        ▼             │   │
│   │   ┌─────────────────┐    ┌─────────────────┐    ┌────────────────┐  │   │
│   │   │ directInvestments│    │  lpCommitments  │    │  Company Card  │  │   │
│   │   │    .filter()     │    │    .filter()    │───▶│  with Logos    │  │   │
│   │   └─────────────────┘    └─────────────────┘    └────────────────┘  │   │
│   │                                                                      │   │
│   └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Changes by Area

1. Dynamic Data Fetching (src/pages/portfolio/index.astro)

Replaced static import with conditional NocoDB fetch:

Before:

---
import portfolioData from '@content/portfolio/portfolio-companies.json';
---

After:

---
import { getPortfolioCompanies, isNocoDBConfigured } from '@lib/nocodb';
import staticPortfolioData from '@content/portfolio/portfolio-companies.json';

export const prerender = true;

let portfolioData;
if (isNocoDBConfigured()) {
  console.log('[portfolio] Fetching from NocoDB...');
  portfolioData = await getPortfolioCompanies();
  console.log(`[portfolio] Loaded ${portfolioData.length} companies from NocoDB`);
} else {
  console.log('[portfolio] NocoDB not configured, using static data');
  portfolioData = staticPortfolioData;
}
---

Benefits:

  • Single source of truth in NocoDB for portfolio data
  • Non-technical team members can update via NocoDB UI
  • Static fallback ensures builds work without API access
  • Console logging aids debugging

2. Multi-Mode Logo System

Implemented logo switching across three theme modes using CSS classes:

HTML Structure:

<!-- Logo for dark/vibrant modes -->
<img
  src={company.logoDarkMode || company.logoLightMode}
  alt={company.conventionalName}
  class:list={[
    "h-10 w-auto object-contain logo-for-dark",
    company.logoIsTransparent && "logo-transparent"
  ]}
  loading="lazy"
/>

<!-- Logo for light mode -->
<img
  src={company.logoLightMode}
  alt={company.conventionalName}
  class:list={[
    "h-10 w-auto object-contain logo-for-light",
    company.logoIsTransparent && "logo-transparent"
  ]}
  loading="lazy"
/>

CSS Mode Switching:

/* Light mode: show light-mode logos, hide dark-mode logos */
:global([data-mode="light"]) .logo-for-dark {
  display: none !important;
}
:global([data-mode="light"]) .logo-for-light {
  display: block !important;
}

/* Dark mode: show dark-mode logos, hide light-mode logos */
:global([data-mode="dark"]) .logo-for-light {
  display: none !important;
}
:global([data-mode="dark"]) .logo-for-dark {
  display: block !important;
}

/* Vibrant mode: treat same as dark mode */
:global([data-mode="vibrant"]) .logo-for-light {
  display: none !important;
}
:global([data-mode="vibrant"]) .logo-for-dark {
  display: block !important;
}

3. Transparent SVG Color Treatment

For logos that are transparent (monochrome with no background), apply CSS filters to match the theme:

/* Transparent logo color treatment */
.logo-transparent {
  transition: filter 0.2s ease;
}

/* Light mode: render as black */
:global([data-mode="light"]) .logo-transparent {
  filter: brightness(0);
}

/* Dark mode: render as white */
:global([data-mode="dark"]) .logo-transparent {
  filter: brightness(0) invert(1);
}

/* Vibrant mode: render as white */
:global([data-mode="vibrant"]) .logo-transparent {
  filter: brightness(0) invert(1);
}

How it works:

┌──────────────────────────────────────────────────────────────────────────┐
│                    CSS Filter Pipeline for Transparent Logos              │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                           │
│   Original SVG              brightness(0)           invert(1)            │
│   (any color)              (force black)         (flip to white)         │
│                                                                           │
│   ┌─────────┐              ┌─────────┐            ┌─────────┐           │
│   │  🔵🟢🔴  │  ─────────▶  │  ⬛⬛⬛  │  ────────▶  │  ⬜⬜⬜  │           │
│   │  Logo   │   Step 1     │  Black  │   Step 2   │  White  │           │
│   └─────────┘              └─────────┘            └─────────┘           │
│                                                                           │
│   Light Mode: brightness(0) only          → Black logo                   │
│   Dark Mode:  brightness(0) + invert(1)   → White logo                   │
│                                                                           │
└──────────────────────────────────────────────────────────────────────────┘

4. Portfolio Logo Assets

Added 19 trademark/logo files for portfolio companies:

Company Variants Format
AmberCycle Dark-Mode, Transparent SVG
Diadem Biotheraputics Dark-Mode, Light-Mode WebP
Encellin Dark-Mode PNG
ExoLux Dark-Mode, Light-Mode, Transparent SVG
ImYoo Brand AVIF
Inaru Dark-Mode, Light-Mode WebP, PNG
JungleTea Transparent SVG
MiniCircle Transparent SVG
Mudita Transparent SVG
Napigen Brand WebP
ParallelBio Brand SVG
Petri Dark-Mode, Full WebP, PNG
Trilobio Dark-Mode, Light-Mode SVG
Venice Beach Wine Club Brand WebP

Naming Convention:

trademark__{CompanyName}--{Variant}.{ext}

Variants:
  - Dark-Mode     : Designed for dark backgrounds
  - Light-Mode    : Designed for light backgrounds
  - Transparent   : Monochrome, needs CSS color treatment
  - Brand         : Original brand colors (any background)

5. Company Category Separation

The page separates companies into two sections based on investment type:

// In NocoDB transformation (src/lib/nocodb.ts)
const entityType = fields['Entity-Type'] || null;
let category: 'direct' | 'lp' = 'direct';
if (entityType === 'LP') {
  category = 'lp';
}
<!-- In portfolio page -->
const directInvestments = portfolioData.filter(c => c.category === 'direct');
const lpCommitments = portfolioData.filter(c => c.category === 'lp');

Page Structure:

Portfolio
├── Direct Investments (C-Corp, LLC entities)
│   └── Grid of company cards
├── LP Commitments (LP/Fund entities)
│   └── Grid of company cards
└── Confidential Access CTA
    └── Link to /portfolio-gate

Files Changed Summary

New

Logo Assets (public/portfolio/logos/):

  • trademark__AmberCycle--Dark-Mode.svg
  • trademark__AmberCycle--Transparent.svg
  • trademark__Diadem-Biotheraputics--Dark-Mode.webp
  • trademark__Diadem-Biotheraputics--Light-Mode.webp
  • trademark__Encellin--Dark-Mode.png
  • trademark__ExoLux--Dark-Mode.svg
  • trademark__ExoLux--Light-Mode.svg
  • trademark__ExoLux--Transparent.svg
  • trademark__ImYoo--Brand.avif
  • trademark__Inaru--Dark-Mode.webp
  • trademark__Inaru--Light-Mode.png
  • trademark__JungleTea--Transparent.svg
  • trademark__MiniCircle--Transparent.svg
  • trademark__Mudita--Transparent.svg
  • trademark__Napigen--Brand.webp
  • trademark__ParallelBio--Brand.svg
  • trademark__Petri--Dark-Mode.webp
  • trademark__Petri.png
  • trademark__Trilobio--Dark-Mode.svg
  • trademark__Trilobio--Light-Mode.svg
  • trademark__Venice-Beach-Wine-Club--Brand.webp

Modified

  • src/pages/portfolio/index.astro — Added NocoDB integration, multi-mode logo CSS

Technical Details

Prerender Compatibility

The page uses export const prerender = true for static generation. NocoDB fetch happens at build time:

// This runs during build, not on each request
if (isNocoDBConfigured()) {
  portfolioData = await getPortfolioCompanies();
}

Logo Fallback Chain

logoDarkMode ─┬─ exists? ──▶ Use dark mode logo
              │
              └─ missing? ──▶ Fall back to logoLightMode ─┬─ exists? ──▶ Use light logo
                                                          │
                                                          └─ missing? ──▶ placeholder-dark.svg

Transparent Detection

The NocoDB transformation detects transparent logos by filename convention:

const logoIsTransparent = Boolean(
  trademarks.lightMode?.includes('--Transparent') ||
  trademarks.darkMode?.includes('--Transparent')
);

This flag triggers the .logo-transparent class application.

Theme Mode Detection

The site uses data-mode attribute on a parent element:

<html data-mode="dark">
  <!-- or "light" or "vibrant" -->
</html>

CSS selectors use :global() to escape Astro's scoped styles:

:global([data-mode="dark"]) .logo-for-light {
  display: none !important;
}

Visual Reference

Company Card Layout

┌─────────────────────────────────────────┐
│                                         │
│   ┌─────────────────────────────────┐   │
│   │                                 │   │
│   │       [Company Logo]            │   │
│   │        h-10 w-auto              │   │
│   │                                 │   │
│   └─────────────────────────────────┘   │
│                                         │
│   Company Name                          │
│   text-sm font-medium                   │
│                                         │
│   Short description of what the         │
│   company does, limited to 2 lines...   │
│   text-xs text-foreground/50            │
│                                         │
└─────────────────────────────────────────┘
     ↑
     └── Hover: border-primary/50, lift effect

Grid Layout

Desktop (lg): 4 columns
┌─────┬─────┬─────┬─────┐
│     │     │     │     │
│     │     │     │     │
└─────┴─────┴─────┴─────┘

Tablet (md): 3 columns
┌─────┬─────┬─────┐
│     │     │     │
└─────┴─────┴─────┘

Mobile: 2 columns
┌─────┬─────┐
│     │     │
└─────┴─────┘

Notes / Follow-Ups

  1. Missing Logos: Some companies may still use placeholder logos. Run NocoDB query to identify companies without trademarksSlugs populated.

  2. Logo Optimization: Consider adding image optimization pipeline for WebP/AVIF conversion of PNG logos.

  3. Vibrant Mode Logos: The logoVibrantMode field is supported in the data model but not yet used in the UI. Could show brand-colored logos in vibrant mode.

  4. Logo Size Consistency: Some logos may appear larger/smaller due to aspect ratios. Consider adding width constraints or aspect-ratio containers.

  5. Lazy Loading: All logos use loading="lazy" for performance. Consider intersection observer for more control.

  6. Access at: /portfolio