📄 findProp.js
/home/palash/git/site/node_modules/new_site/src/config/findProp.js
Language: js • Lines: 130
// findProp: local function that queries site data.
// Common config is bundled locally (baseConfig.js) so the page renders correctly.
// Only blog/article contents (swe, music, drafts) are fetched from GitHub Pages at runtime.

import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import baseConfig from './baseConfig';

let mergedData = { ...baseConfig };
let contentsPromise = null;
let contentsLoadError = null;
const configListeners = new Set();

const ConfigContext = createContext({
  config: mergedData,
  findProp: () => undefined,
  isContentsLoaded: false,
});

function notifyConfigListeners() {
  configListeners.forEach((listener) => listener());
}

function resolveConfigValue(config, path) {
  if (!config) return undefined;
  const parts = path.split('.');
  let obj = config;
  for (const part of parts) {
    if (obj == null || typeof obj !== 'object') return undefined;
    obj = obj[part];
  }
  return obj !== undefined ? obj : undefined;
}

export function findProp(path) {
  return resolveConfigValue(mergedData, path);
}

export function subscribeToConfig(listener) {
  configListeners.add(listener);
  return () => configListeners.delete(listener);
}

export function useConfig() {
  return useContext(ConfigContext);
}

export function ConfigProvider({ children }) {
  const [config, setConfig] = useState(mergedData);
  const [isContentsLoaded, setIsContentsLoaded] = useState(false);

  useEffect(() => {
    const unsubscribe = subscribeToConfig(() => {
      setConfig({ ...mergedData });
    });

    let cancelled = false;
    loadContents()
      .catch(() => undefined)
      .finally(() => {
        if (!cancelled) {
          setIsContentsLoaded(true);
        }
      });

    return () => {
      cancelled = true;
      unsubscribe();
    };
  }, []);

  const value = useMemo(() => ({
    config,
    findProp: (path) => resolveConfigValue(config, path),
    isContentsLoaded,
  }), [config, isContentsLoaded]);

  return <ConfigContext.Provider value={value}>{children}</ConfigContext.Provider>;
}

export async function loadContents() {
  if (contentsPromise) return contentsPromise;
  contentsPromise = (async () => {
    const url = 'https://palash90.github.io/site-assets/data.json';
    const fallbackUrl = 'https://raw.githubusercontent.com/palash90/site-assets/main/data.json';

    async function tryFetch(fetchUrl) {
      const res = await fetch(fetchUrl);
      if (!res.ok) {
        let detail = `HTTP ${res.status}`;
        try {
          await res.clone().text();
        } catch (_) {
          detail = `CORS blocked (${res.status})`;
        }
        throw new Error(detail);
      }
      return res.json();
    }

    try {
      let remote;
      try {
        remote = await tryFetch(url);
      } catch (primaryErr) {
        console.warn('Primary content URL failed, trying fallback:', primaryErr.message);
        try {
          remote = await tryFetch(fallbackUrl);
        } catch (fbErr) {
          throw new Error(`Primary: ${primaryErr.message} | Fallback: ${fbErr.message}`);
        }
      }
      if (remote.contents) {
        mergedData = { ...baseConfig, ...remote, contents: remote.contents };
        notifyConfigListeners();
      }
      return mergedData.contents;
    } catch (err) {
      contentsLoadError = err.message;
      console.warn('Failed to load contents from GitHub Pages:', err.message);
      return null;
    }
  })();
  return contentsPromise;
}

export function getContentsLoadError() {
  return contentsLoadError;
}

export default findProp;