Skip to content

Commit 345a05d

Browse files
committed
feat: switch to static JSON cache for data loading
1 parent bf50677 commit 345a05d

File tree

16 files changed

+10773
-11623
lines changed

16 files changed

+10773
-11623
lines changed

dist/appverse.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/appverse.es.js

Lines changed: 10571 additions & 10822 deletions
Large diffs are not rendered by default.

dist/appverse.es.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/appverse.umd.js

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

dist/appverse.umd.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/detail/AppRow.jsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,23 @@ export default function AppRow({ app, isExpanded, onToggle }) {
1818
const { getFlagCountAdjustment } = useFlag();
1919
const track = useTracking();
2020

21-
const title = app.attributes?.title || 'Untitled App';
22-
const githubUrl = app.attributes?.field_appverse_github_url?.uri;
21+
const title = app.title || 'Untitled App';
22+
const githubUrl = app.githubUrl;
2323
// Raw markdown content for README
24-
const readme = app.attributes?.field_appverse_readme?.value;
25-
const lastUpdated = app.attributes?.field_appverse_lastupdated;
26-
const baseFlagCount = app.attributes?.flag_count || 0;
24+
const readme = app.readme;
25+
const lastUpdated = app.lastUpdated;
26+
const baseFlagCount = app.flagCount || 0;
2727
// Adjust flag count based on user's flag actions (updated after server confirms)
2828
const flagCount = baseFlagCount + getFlagCountAdjustment(app.id);
29-
const githubStars = app.attributes?.field_appverse_stars ?? 0;
29+
const githubStars = app.stars ?? 0;
3030

3131
// Resolved taxonomy terms from API
3232
const organization = app.organization;
3333
const tags = app.tags || [];
3434

3535
// App identifiers for flagging
3636
const appId = app.id; // UUID
37-
const nid = app.attributes?.drupal_internal__nid; // needed for entity_id when creating a flagging
37+
const nid = app.nid; // needed for entity_id when creating a flagging
3838

3939
// For smooth height animation of README panel
4040
const readmeRef = useRef(null);

src/components/detail/SoftwareHeader.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { useTracking } from '../../hooks/useTracking';
1111

1212
export default function SoftwareHeader({ software }) {
1313
const track = useTracking();
14-
const title = software.attributes?.title || 'Untitled Software';
15-
const description = software.attributes?.body?.processed || software.attributes?.body?.value || '';
14+
const title = software.title || 'Untitled Software';
15+
const description = software.body || '';
1616
const logoUrl = software.logoUrl;
17-
const websiteUrl = software.attributes?.field_appverse_software_website?.uri;
18-
const docsUrl = software.attributes?.field_appverse_software_doc?.uri;
17+
const websiteUrl = software.websiteUrl;
18+
const docsUrl = software.docsUrl;
1919

2020
// Resolved taxonomy terms from API
2121
const license = software.license;

src/components/home/SoftwareGrid.jsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
*
55
* Props:
66
* @param {Array} software - Array of software items to display
7-
* @param {Object} appsBySoftwareId - Apps grouped by software ID
87
*/
98
import SoftwareTile from './SoftwareTile';
109
import SkeletonTile from './SkeletonTile';
1110
import { Search } from 'react-bootstrap-icons';
1211

13-
export default function SoftwareGrid({ software, appsBySoftwareId, loading, appsLoading }) {
12+
export default function SoftwareGrid({ software, loading }) {
1413
// Debug: Log render state
1514
// console.log('[SoftwareGrid] Render - loading:', loading, '| software count:', software?.length ?? 0);
1615

@@ -44,18 +43,12 @@ export default function SoftwareGrid({ software, appsBySoftwareId, loading, apps
4443

4544
return (
4645
<div className="grid gap-6" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(199px, 1fr))' }}>
47-
{software.map((softwareItem) => {
48-
const appCount = (appsBySoftwareId[softwareItem.id] || []).length;
49-
50-
return (
51-
<SoftwareTile
52-
key={softwareItem.id}
53-
software={softwareItem}
54-
appCount={appCount}
55-
appsLoading={appsLoading}
56-
/>
57-
);
58-
})}
46+
{software.map((softwareItem) => (
47+
<SoftwareTile
48+
key={softwareItem.id}
49+
software={softwareItem}
50+
/>
51+
))}
5952
</div>
6053
);
6154
}

src/components/home/SoftwareTile.jsx

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import { StarFill } from 'react-bootstrap-icons';
1111
import { slugify } from '../../utils/slugify';
1212
import { useTracking } from '../../hooks/useTracking';
1313

14-
export default function SoftwareTile({ software, appCount = 0, appsLoading = false }) {
14+
export default function SoftwareTile({ software }) {
1515
const track = useTracking();
16-
const softwareTitle = software.attributes?.title || 'Untitled Software';
16+
const softwareTitle = software.title || 'Untitled Software';
1717
const logoUrl = software.logoUrl;
18+
const appCount = software.appCount || 0;
1819

1920
// Generate slug from title for semantic URL
2021
const slug = slugify(softwareTitle);
@@ -66,39 +67,32 @@ export default function SoftwareTile({ software, appCount = 0, appsLoading = fal
6667

6768
{/* App count badge or Add Repos button */}
6869
<div className="mt-auto">
69-
{appsLoading ? (
70-
<div className="inline-flex items-center gap-1.5 animate-pulse">
71-
<div className="w-5 h-5 bg-gray-200 rounded-full" />
72-
<div className="w-12 h-4 bg-gray-200 rounded" />
73-
</div>
74-
) : (
75-
<span
76-
role={appCount === 0 ? 'button' : undefined}
77-
onClick={appCount === 0 ? (e) => {
78-
e.preventDefault();
79-
e.stopPropagation();
80-
window.location.href = '/node/add/appverse_app';
81-
} : undefined}
82-
className={`inline-flex items-center gap-1.5 text-base font-medium ${
83-
appCount === 0 ? 'cursor-pointer hover:opacity-80 transition-opacity' : ''
84-
}`}
85-
>
86-
<span className={`grid place-items-center w-5 h-5 rounded-full ${
87-
appCount === 0 ? 'bg-appverse-green' : 'bg-appverse-blue'
88-
}`}>
89-
{appCount === 0 ? (
90-
<StarFill className="w-2.5 h-2.5 text-white -translate-y-[0.5px]" />
91-
) : (
92-
<span className="text-[10px] font-bold text-white -translate-y-[2px]">
93-
{appCount}
94-
</span>
95-
)}
96-
</span>
97-
<span className={appCount === 0 ? 'text-appverse-green' : 'text-appverse-black'}>
98-
{appCount === 0 ? 'Add an app' : appCountText}
99-
</span>
70+
<span
71+
role={appCount === 0 ? 'button' : undefined}
72+
onClick={appCount === 0 ? (e) => {
73+
e.preventDefault();
74+
e.stopPropagation();
75+
window.location.href = '/node/add/appverse_app';
76+
} : undefined}
77+
className={`inline-flex items-center gap-1.5 text-base font-medium ${
78+
appCount === 0 ? 'cursor-pointer hover:opacity-80 transition-opacity' : ''
79+
}`}
80+
>
81+
<span className={`grid place-items-center w-5 h-5 rounded-full ${
82+
appCount === 0 ? 'bg-appverse-green' : 'bg-appverse-blue'
83+
}`}>
84+
{appCount === 0 ? (
85+
<StarFill className="w-2.5 h-2.5 text-white -translate-y-[0.5px]" />
86+
) : (
87+
<span className="text-[10px] font-bold text-white -translate-y-[2px]">
88+
{appCount}
89+
</span>
90+
)}
10091
</span>
101-
)}
92+
<span className={appCount === 0 ? 'text-appverse-green' : 'text-appverse-black'}>
93+
{appCount === 0 ? 'Add an app' : appCountText}
94+
</span>
95+
</span>
10296
</div>
10397
</div>
10498
</Link>

src/contexts/AppverseDataContext.jsx

Lines changed: 17 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -2,139 +2,45 @@
22
* AppverseDataContext
33
*
44
* Provides global data for software and apps throughout the application.
5-
* Fetches both endpoints on mount and makes data available via context.
5+
* Fetches from static JSON cache on mount.
66
*
77
* Usage:
88
* import { useAppverseData } from '../hooks/useAppverseData'
9-
* const { software, apps, appsBySoftwareId, loading, error } = useAppverseData()
9+
* const { software, appsBySoftwareId, loading, error } = useAppverseData()
1010
*/
1111
import { createContext, useState, useEffect, useMemo, useCallback } from 'react';
12-
import { fetchAllSoftware, fetchAllApps, fetchAllAppTypes, groupAppsBySoftware, extractFilterOptionsFromApps, extractFilterOptionsFromSoftware } from '../utils/api';
12+
import { fetchStaticCache } from '../utils/api';
1313
import { slugify } from '../utils/slugify';
1414
import { useConfig } from './ConfigContext';
1515

1616
export const AppverseDataContext = createContext(null);
1717

18-
/**
19-
* @typedef {Object} SoftwareItem
20-
* @property {string} id - UUID
21-
* @property {string} type - "node--appverse_software"
22-
* @property {Object} attributes
23-
* @property {string} attributes.title - Software name
24-
* @property {Object} attributes.body - {value: string, format: string, processed: string}
25-
* @property {Object} attributes.field_appverse_software_doc - {uri: string, title: string}
26-
* @property {Object} attributes.field_appverse_software_website - {uri: string, title: string}
27-
* @property {number} attributes.drupal_internal__nid - Drupal node ID
28-
* @property {string} attributes.created - ISO timestamp
29-
* @property {string} attributes.changed - ISO timestamp
30-
* @property {Object} relationships
31-
* @property {Object|null} relationships.field_appverse_logo - Logo media (type: "media--svg")
32-
* @property {Array} relationships.field_appverse_topics - Topics (taxonomy)
33-
* @property {Object|null} relationships.field_license - License (taxonomy)
34-
* @property {Array} relationships.field_tags - Tags (taxonomy)
35-
* @property {Array} relationships.field_domain_access - Domains
36-
* @property {string|null} logoUrl - Resolved logo URL (added by api.js)
37-
*/
38-
39-
/**
40-
* @typedef {Object} AppItem
41-
* @property {string} id - UUID
42-
* @property {string} type - "node--appverse_app"
43-
* @property {Object} attributes
44-
* @property {string} attributes.title - App name
45-
* @property {Object} attributes.field_implementation_details - {value: string, format: string, processed: string}
46-
* @property {number} attributes.drupal_internal__nid - Drupal node ID
47-
* @property {string} attributes.created - ISO timestamp
48-
* @property {string} attributes.changed - ISO timestamp
49-
* @property {Object} relationships
50-
* @property {Object} relationships.field_appverse_software_implemen - Software reference
51-
* @property {Object|null} relationships.field_appverse_app_type - App type (taxonomy)
52-
* @property {Array} relationships.field_add_implementation_tags - Tags (taxonomy)
53-
* @property {Object|null} relationships.field_appverse_organization - Organization (taxonomy)
54-
* @property {Object|null} relationships.field_license - License (taxonomy)
55-
*/
56-
57-
/**
58-
* @typedef {Object} AppverseData
59-
* @property {SoftwareItem[]} software - All software items with logo URLs resolved
60-
* @property {AppItem[]} apps - All app items
61-
* @property {Object.<string, AppItem[]>} appsBySoftwareId - Apps grouped by software UUID
62-
* @property {boolean} loading - Loading state
63-
* @property {Error|null} error - Error object if fetch failed
64-
* @property {Function} refetch - Function to manually refetch data
65-
*/
66-
6718
export function AppverseDataProvider({ children }) {
6819
const config = useConfig();
6920
const [data, setData] = useState({
7021
software: [],
71-
apps: [],
7222
appsBySoftwareId: {},
7323
filterOptions: { tags: [], appType: [], topics: [], license: [] },
74-
softwareLoading: true,
75-
appsLoading: true,
24+
loading: true,
7625
error: null
7726
});
7827

79-
/**
80-
* Merge tags from two filter option sources, deduplicating by ID
81-
*/
82-
const mergeTags = (existingTags, newTags) => {
83-
const tagMap = {};
84-
for (const tag of existingTags) tagMap[tag.id] = tag;
85-
for (const tag of newTags) tagMap[tag.id] = tag;
86-
return Object.values(tagMap).sort((a, b) => a.name.localeCompare(b.name));
87-
};
88-
89-
/**
90-
* Fetch all data from API
91-
* Software and apps are fetched concurrently but update state independently
92-
* so the grid can render as soon as software arrives.
93-
*/
9428
const fetchData = () => {
95-
setData(prev => ({ ...prev, softwareLoading: true, appsLoading: true, error: null }));
29+
setData(prev => ({ ...prev, loading: true, error: null }));
9630

97-
// Fetch software — update state as soon as it resolves
98-
fetchAllSoftware(config)
99-
.then(({ software, included: softwareIncluded }) => {
100-
const softwareFilterOptions = extractFilterOptionsFromSoftware(softwareIncluded);
101-
setData(prev => ({
102-
...prev,
31+
fetchStaticCache(config)
32+
.then(({ software, appsBySoftwareId, filterOptions }) => {
33+
setData({
10334
software,
104-
softwareLoading: false,
105-
filterOptions: {
106-
...prev.filterOptions,
107-
topics: softwareFilterOptions.topics,
108-
license: softwareFilterOptions.license,
109-
tags: mergeTags(prev.filterOptions.tags, softwareFilterOptions.tags)
110-
}
111-
}));
112-
})
113-
.catch(error => {
114-
console.error('Failed to fetch software:', error);
115-
setData(prev => ({ ...prev, softwareLoading: false, error }));
116-
});
117-
118-
// Fetch apps and all app types concurrently
119-
Promise.all([fetchAllApps(config), fetchAllAppTypes(config)])
120-
.then(([{ apps, included: appsIncluded }, allAppTypes]) => {
121-
const appsFilterOptions = extractFilterOptionsFromApps(appsIncluded);
122-
const appsBySoftwareId = groupAppsBySoftware(apps);
123-
setData(prev => ({
124-
...prev,
125-
apps,
12635
appsBySoftwareId,
127-
appsLoading: false,
128-
filterOptions: {
129-
...prev.filterOptions,
130-
appType: allAppTypes,
131-
tags: mergeTags(prev.filterOptions.tags, appsFilterOptions.tags)
132-
}
133-
}));
36+
filterOptions,
37+
loading: false,
38+
error: null,
39+
});
13440
})
13541
.catch(error => {
136-
console.error('Failed to fetch apps:', error);
137-
setData(prev => ({ ...prev, appsLoading: false, error }));
42+
console.error('Failed to fetch appverse data:', error);
43+
setData(prev => ({ ...prev, loading: false, error }));
13844
});
13945
};
14046

@@ -144,31 +50,25 @@ export function AppverseDataProvider({ children }) {
14450
}, []);
14551

14652
// Build slug map: slugify(title) → software object
147-
// This allows /appverse/abaqus to resolve to the correct software
14853
const slugMap = useMemo(() => {
14954
const map = {};
15055
for (const sw of data.software) {
151-
const title = sw.attributes?.title;
152-
if (title) {
153-
const slug = slugify(title);
154-
map[slug] = sw;
56+
if (sw.title) {
57+
map[slugify(sw.title)] = sw;
15558
}
15659
}
15760
return map;
15861
}, [data.software]);
15962

160-
// Helper to get software by slug (memoized to prevent useEffect re-triggers)
16163
const getSoftwareBySlug = useCallback((slug) => {
16264
return slugMap[slug] || null;
16365
}, [slugMap]);
16466

16567
const contextValue = {
16668
...data,
167-
// Backward-compatible loading: true only while software is loading (grid not yet visible)
168-
loading: data.softwareLoading,
16969
slugMap,
17070
getSoftwareBySlug,
171-
refetch: fetchData // Allow manual refetch if needed
71+
refetch: fetchData,
17272
};
17373

17474
return (

0 commit comments

Comments
 (0)