diff --git a/.env b/.env
new file mode 100644
index 0000000..624c7b1
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+PROJECT_UPDATE_URL_BASE=https://ecosystem.khronos.org/update/
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 491bd5d..6eafe68 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -26,9 +26,3 @@ jobs:
run: yarn
- name: Build
run: yarn build
- - name: Deploy
- uses: peaceiris/actions-gh-pages@v3
- if: ${{ github.ref == 'refs/heads/main' }}
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ./build
diff --git a/package.json b/package.json
index 1ac877d..dd9711a 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "project-explorer",
"version": "0.1.0",
"private": true,
- "homepage": "https://github.khronos.org/glTF-Project-Explorer",
+ "homepage": "https://ecosystem.khronos.org/wp-content/plugins/glTF-project-explorer/",
"dependencies": {
"@types/dompurify": "^2.3.3",
"@types/flexsearch": "^0.7.2",
@@ -18,7 +18,6 @@
"marked": "^4.0.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
- "react-github-fork-ribbon": "^0.6.0",
"react-redux": "^7.2.6",
"redux": "^4.1.2",
"redux-devtools-extension": "^2.13.9",
diff --git a/public/data/glTF-projects-data.json b/public/data/glTF-projects-data.json
index 7843f0e..28ac09a 100644
--- a/public/data/glTF-projects-data.json
+++ b/public/data/glTF-projects-data.json
@@ -1372,8 +1372,7 @@
"link" : "https://github.com/BabylonJS/Babylon.js/tree/master/packages/dev/loaders/src/glTF",
"task" : [ "import", "view" ],
"type" : [ "engine" ],
- "inputs" : [ "glTF 2.0" ],
- "tags" : [ "Staff Picks" ]
+ "inputs" : [ "glTF 2.0" ]
}, {
"name" : "OSG.JS glTF loader",
"description" : "WebGL engine",
@@ -1541,7 +1540,7 @@
"name" : "Microsoft.glTF.CPP",
"description" : "A C++ library for serializing and deserializing gltf/glb files.",
"link" : "https://github.com/microsoft/glTF-SDK",
- "language" : [ "2.0" ],
+ "language" : [ "C++" ],
"inputs" : [ "glTF 2.0" ]
}, {
"name" : "Qt 3D",
@@ -2108,7 +2107,7 @@
"link" : "https://prototechsolutions.com/3d-products/inventor/gltf-exporter/",
"task" : [ "export" ],
"type" : [ "plugin", "sdk" ],
- "language" : [ "c#" ],
+ "language" : [ "C#" ],
"outputs" : [ "glTF 2.0" ]
}, {
"name" : "ProtoTech glTF Exporter For Fusion 360",
@@ -2116,7 +2115,7 @@
"link" : "https://prototechsolutions.com/3d-products/fusion-360/gltf-exporter/",
"task" : [ "export" ],
"type" : [ "plugin", "sdk" ],
- "language" : [ "c#" ],
+ "language" : [ "C#" ],
"outputs" : [ "glTF 2.0" ]
}, {
"name" : "ProtoTech glTF Exporter For Revit",
@@ -2124,7 +2123,7 @@
"link" : "https://prototechsolutions.com/3d-products/revit/gltf-exporter/",
"task" : [ "export" ],
"type" : [ "plugin", "sdk" ],
- "language" : [ "c#" ],
+ "language" : [ "C#" ],
"outputs" : [ "glTF 2.0" ]
}, {
"name" : "ProtoTech glTF Exporter For SolidEdge",
@@ -2132,7 +2131,7 @@
"link" : "https://prototechsolutions.com/3d-products/solidedge/gltf-exporter/",
"task" : [ "export" ],
"type" : [ "plugin", "sdk" ],
- "language" : [ "c#" ],
+ "language" : [ "C#" ],
"outputs" : [ "glTF 2.0" ]
}, {
"name" : "ProtoTech glTF Exporter For SolidWorks",
@@ -2140,7 +2139,7 @@
"link" : "https://prototechsolutions.com/3d-products/solidworks/gltf-exporter/",
"task" : [ "export" ],
"type" : [ "plugin", "sdk" ],
- "language" : [ "c#" ],
+ "language" : [ "C#" ],
"outputs" : [ "glTF 2.0" ]
}, {
"name" : "F3D",
@@ -2148,7 +2147,7 @@
"link" : "https://f3d-app.github.io/f3d/",
"task" : [ "view" ],
"type" : [ "application" ],
- "language" : [ "c++" ],
+ "language" : [ "C++" ],
"inputs" : [ "glTF 2.0" ]
}, {
"name": "Cesium for Unreal",
diff --git a/public/data/glTF-projects-metadata.json b/public/data/glTF-projects-metadata.json
new file mode 100644
index 0000000..ca281c1
--- /dev/null
+++ b/public/data/glTF-projects-metadata.json
@@ -0,0 +1,45 @@
+{
+ "name": {
+ "type": "string",
+ "isArray": false
+ },
+ "link": {
+ "type": "url",
+ "isArray": false
+ },
+ "description": {
+ "type": "markdown",
+ "isArray": false
+ },
+ "tags": {
+ "task": {
+ "type": "string",
+ "isArray": true
+ },
+ "type": {
+ "type": "string",
+ "isArray": true
+ },
+ "license": {
+ "type": "string",
+ "isArray": true
+ },
+ "language": {
+ "type": "string",
+ "isArray": true
+ },
+ "inputs": {
+ "type": "string",
+ "isArray": true
+ },
+ "outputs": {
+ "type": "string",
+ "isArray": true
+ },
+ "tags": {
+ "type": "string",
+ "isArray": true
+ }
+ },
+ "filterTags": ["tags", "task", "type", "language", "license"]
+}
diff --git a/public/github-mark-white.svg b/public/github-mark-white.svg
new file mode 100644
index 0000000..d5e6491
--- /dev/null
+++ b/public/github-mark-white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index c8875e2..7236a99 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,6 @@
import ProjectList from "./components/ProjectList";
import FilterBar from "./components/FilterBar";
import SearchBar from "./components/SearchBar";
-import GitHubForkRibbon from "react-github-fork-ribbon";
import { useWindowDimensions } from "./utils/WindowWidthHook";
const App: React.FC = () => {
@@ -18,7 +17,20 @@ const App: React.FC = () => {
src={`${process.env.PUBLIC_URL}/glTF_RGB_June16.svg`}
alt="glTF"
/>
-
Project Explorer
+ Project Explorer
+
+ Submit New Project
+
+
+
+
@@ -29,13 +41,6 @@ const App: React.FC = () => {
-
- Fork us on GitHub
-
);
};
diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx
index dceb760..226d9a1 100644
--- a/src/components/FilterBar.tsx
+++ b/src/components/FilterBar.tsx
@@ -6,7 +6,6 @@ import { updateSelectedFilters } from "../store/filters/Actions";
import FilterBarOptions from "./FilterBarOptions";
import "./FilterBar.css";
import FilterBarSelected from "./FilterBarSelected";
-import { ProjectFilterProperties } from "../interfaces/IProjectInfo";
interface IFilterBarOwnProps {
allowCollapse: boolean;
@@ -44,7 +43,7 @@ const FilterBar: React.FC = (props) => {
const handleFilterAddClick = useCallback(
(filter: IFilter) => (_: React.MouseEvent) => {
selectedFilters.add(filter);
- const options = filterOptions.get(filter.propertyName);
+ const options = filterOptions.get(filter.tagName);
if (options) {
const index = options.indexOf(filter);
options.splice(index, 1);
@@ -57,7 +56,7 @@ const FilterBar: React.FC = (props) => {
const handleFilterRemoveClick = useCallback(
(filter: IFilter) => (_: React.MouseEvent) => {
selectedFilters.delete(filter);
- const options = filterOptions.get(filter.propertyName);
+ const options = filterOptions.get(filter.tagName);
if (options) {
options.push(filter);
options.sort((f0, f1) => f0.value.localeCompare(f1.value));
@@ -70,7 +69,7 @@ const FilterBar: React.FC = (props) => {
const handleFilterResetClick = useCallback(
(_) => {
for (const filter of selectedFilters) {
- const options = filterOptions.get(filter.propertyName);
+ const options = filterOptions.get(filter.tagName);
if (options) {
options.push(filter);
}
@@ -106,7 +105,7 @@ const FilterBar: React.FC = (props) => {
diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx
index d647ec4..906f38d 100644
--- a/src/components/ProjectCard.tsx
+++ b/src/components/ProjectCard.tsx
@@ -1,4 +1,4 @@
-import { IProjectInfo, ProjectProperties } from "../interfaces/IProjectInfo";
+import { IProjectInfo } from "../interfaces/IProjectInfo";
import ProjectDetailList from "./ProjectDetailList";
import ProjectCardHeader from "./ProjectCardHeader";
import "./ProjectCard.css";
@@ -13,7 +13,7 @@ interface IProjectCardProps {
const ProjectCard: React.FC = (props) => {
const { project } = props;
return (
-
+
{project.description && (
@@ -23,13 +23,13 @@ const ProjectCard: React.FC
= (props) => {
/>
)}
- {Object.keys(project.properties).map((propName) => {
- const items = project.properties[propName];
+ {Object.keys(project.tags).map((propName) => {
+ const items = project.tags[propName];
return (
shouldShowSection(items) && (
)
diff --git a/src/components/ProjectCardHeader.tsx b/src/components/ProjectCardHeader.tsx
index af868cc..8ff2381 100644
--- a/src/components/ProjectCardHeader.tsx
+++ b/src/components/ProjectCardHeader.tsx
@@ -7,10 +7,22 @@ interface IProjectCardHeader {
const ProjectCardHeader: React.FC
= (props) => {
const { project } = props;
+ const updateUrlBase = process.env.PROJECT_UPDATE_URL_BASE;
+
return (
-
- {project.link ? {project.name} : project.name}
-
+
);
};
diff --git a/src/components/ProjectList.tsx b/src/components/ProjectList.tsx
index cad3e45..4341bb2 100644
--- a/src/components/ProjectList.tsx
+++ b/src/components/ProjectList.tsx
@@ -9,7 +9,6 @@ interface IProjectListProps {
const ProjectList: React.FC = (props) => {
const { projects } = props;
-
return (
<>
{projects && projects.map((p) => )}
diff --git a/src/index.css b/src/index.css
index 428442d..b52de4d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -51,13 +51,13 @@ a:focus {
color: theme("colors.link-focus");
}
-a[href*="http"] {
+a.external[href*="http"] {
background: url(./external-link-icon.svg) no-repeat 100% 0;
background-size: 1rem 1rem;
padding-right: 1.25rem;
}
-h1 a[href*="http"] {
+h1 a.external[href*="http"] {
background-size: 1.5rem 1.5rem;
padding-right: 2rem;
}
diff --git a/src/interfaces/IAppState.ts b/src/interfaces/IAppState.ts
index a0fadd4..e68d5d0 100644
--- a/src/interfaces/IAppState.ts
+++ b/src/interfaces/IAppState.ts
@@ -1,6 +1,7 @@
import { Document } from "flexsearch";
import { IFilter } from "./IFilter";
import { IProjectInfo } from "./IProjectInfo";
+import { IProjectsMetadata } from "./IProjectsMetadata";
/**
* This is the document layout used for indexing the Projects.
@@ -24,12 +25,14 @@ export interface IProjectsState {
isFetchingProjects: boolean;
values: IProjectInfo[];
searchIndex?: Document;
+ isFetchingProjectsMetadata: boolean;
+ projectsMetadata?: IProjectsMetadata
}
/**
* The part of the state that summarizes the filter options and selection.
*
- * @property filterOptions A record that maps ProjectProperties keys to the
+ * @property filterOptions A record that maps projectsMetadata tags to the
* list of filters that are available for this project property. These are
* one filter object for each possible value of this property.
* @property titleSubstring The string that was entered in the free-text search box
diff --git a/src/interfaces/IFilter.ts b/src/interfaces/IFilter.ts
index ec48327..50c9eb8 100644
--- a/src/interfaces/IFilter.ts
+++ b/src/interfaces/IFilter.ts
@@ -1,13 +1,11 @@
export interface IFilter {
- propertyName: string;
+ tagName: string;
value: string;
- // selected: boolean;
}
-export function createNewFilter(propertyName: string, value: string): IFilter {
+export function createNewFilter(tagName: string, value: string): IFilter {
return {
- propertyName,
+ tagName,
value,
- // selected: false
};
}
diff --git a/src/interfaces/IProjectInfo.ts b/src/interfaces/IProjectInfo.ts
index 313450d..e8c2cb4 100644
--- a/src/interfaces/IProjectInfo.ts
+++ b/src/interfaces/IProjectInfo.ts
@@ -1,68 +1,23 @@
-// Aliases as these are the four possible types of filters.
-type ProjectTask = string;
-type ProjectType = string;
-type ProjectLanguage = string;
-type ProjectLicense = string;
-type ProjectTag = string;
-
-/**
- * TODO_GENERALIZATION:
- *
- * The IProjectInfo COULD already be an entity description generated
- * using https://github.com/typeorm/typeorm, but
- * - we don't know yet how persistence will be implemented
- * - the entities have to be dynamic in terms of the "columns"
- * But instances of the IProjectInfo could probably be mapped to
- * actual TypeORM entities in the persistence layer, while
- * the UI solely operates on the IProjectInfo.
- *
- * The ProjectProperties and ProjectFilterProperties are the
- * point of configuration for different project types. For now
- * they are hard-coded here.
- */
-
-/**
- * The map of keys for the IProjectInfo#properties map,
- * mapped to a human-readable form that is shown in the UI
- */
-export const ProjectProperties = new Map([
- ["task", "Task"],
- ["license", "License"],
- ["type", "Type"],
- ["language", "Language"],
- ["inputs", "Inputs"],
- ["outputs", "Outputs"],
- ["tags", "Tags"],
-]);
-
-/**
- * A map defining the ProjectProperties by which the projects
- * can be filtered, which is a subset of the ProjectProperties
- */
-export const ProjectFilterProperties = new Map([
- ["tags", "Tags"],
- ["task", "Task"],
- ["type", "Type"],
- ["language", "Language"],
- ["license", "License"],
-]);
-
export interface IProjectInfo {
id: number;
+ key: string;
name: string;
description?: string;
link?: string;
+ tags: Record;
+}
- properties: Record;
-
- // TODO_GENERALIZATION These are supposed to be removed.
- // Right now, they are "migrated" and written into the
- // "properties" record in the DataService.ts
- task?: ProjectTask[];
- license?: ProjectLicense[];
- type?: ProjectType[];
- language?: ProjectLanguage[];
+export interface ILegacyProjectInfo {
+ id: number;
+ key: string;
+ name: string;
+ description?: string;
+ link?: string;
+ task?: string[];
+ license?: string[];
+ type?: string[];
+ language?: string[];
inputs?: string[];
outputs?: string[];
- tags?: ProjectTag[];
+ tags?: string[];
}
diff --git a/src/interfaces/IProjectsMetadata.ts b/src/interfaces/IProjectsMetadata.ts
new file mode 100644
index 0000000..141ab53
--- /dev/null
+++ b/src/interfaces/IProjectsMetadata.ts
@@ -0,0 +1,13 @@
+
+export interface IValueType {
+ type: "string" | "url" | "date" | "markdown" | "number";
+ isArray: boolean;
+}
+
+export interface IProjectsMetadata {
+ name: IValueType;
+ description: IValueType;
+ link: IValueType;
+ tags: Record;
+ filterTags: string[]
+}
diff --git a/src/services/DataService.ts b/src/services/DataService.ts
index 23d16b7..7845e66 100644
--- a/src/services/DataService.ts
+++ b/src/services/DataService.ts
@@ -1,49 +1,87 @@
import { IProjectInfo } from "../interfaces/IProjectInfo";
+import { ILegacyProjectInfo } from "../interfaces/IProjectInfo";
+import { IProjectsMetadata } from "../interfaces/IProjectsMetadata";
// Despite the data being a static file, we don't pull it in using Webpack so
// we can change to using a restful service in the future.
-function fetchProjectsInternal(): Promise {
- return fetch(`${process.env.PUBLIC_URL}/data/glTF-projects-data.json`)
- .then((r) => r.json())
- .catch((error) => console.error(`Error fetching data. Reason: ${error}`));
+async function fetchProjectsInternal(): Promise {
+ try {
+ const response = await fetch(
+ `${process.env.PUBLIC_URL}/data/glTF-projects-data.json`
+ );
+ return await response.json();
+ } catch (error) {
+ console.error(`Error fetching data. Reason: ${error}`);
+ return [];
+ }
}
/**
- * Converts the given project info into the new structure, where the
- * properties are stored in the `properties` record.
+ * Converts the given legacy project info into the new structure, where the
+ * properties are stored in the `tags` record.
*
- * TODO_GENERALIZATION This is a preliminary state. In the future,
- * the input JSON could be modified, or be represented by a structure
- * like "ILegacyProjectInfo" that is converted into the IProjectInfo
- * here.
- *
- * @param p The project info
- * @returns The given project info
+ * @param p The legacy project info
+ * @returns The new project info
*/
-function migrateProjectInfoForGeneralization(p: IProjectInfo): IProjectInfo {
- p.properties = {};
- p.properties["task"] = p.task ? p.task : [];
- p.properties["license"] = p.license ? p.license : [];
- p.properties["type"] = p.type ? p.type : [];
- p.properties["language"] = p.language ? p.language : [];
- p.properties["inputs"] = p.inputs ? p.inputs : [];
- p.properties["outputs"] = p.outputs ? p.outputs : [];
- p.properties["tags"] = p.tags ? p.tags : [];
- return p;
+function migrateProjectInfoForGeneralization(
+ p: ILegacyProjectInfo
+): IProjectInfo {
+ const result: IProjectInfo = {
+ id: p.id,
+ key: p.key,
+ name: p.name,
+ description: p.description,
+ link: p.link,
+ tags: {},
+ };
+ if (p.task) {
+ result.tags["task"] = p.task;
+ }
+ if (p.license) {
+ result.tags["license"] = p.license;
+ }
+ if (p.type) {
+ result.tags["type"] = p.type;
+ }
+ if (p.language) {
+ result.tags["language"] = p.language;
+ }
+ if (p.inputs) {
+ result.tags["inputs"] = p.inputs;
+ }
+ if (p.outputs) {
+ result.tags["outputs"] = p.outputs;
+ }
+ if (p.tags) {
+ result.tags["tags"] = p.tags;
+ }
+ return result;
}
-export function fetchProjects(): Promise {
- return new Promise(async (resolve) => {
- const projects = await fetchProjectsInternal();
+export async function fetchProjects(): Promise {
+ const legacyProjects = await fetchProjectsInternal();
- // This work gives us a stable key. Eventually when this is database
- // backed the ID will be provided by the DB and this can be removed.
- let id = 0;
- let resultProjects = projects.map((p) => {
- p.id = id++;
- return p;
- });
- resultProjects = resultProjects.map(migrateProjectInfoForGeneralization);
- resolve(resultProjects);
+ // This work gives us a stable key. Eventually when this is database
+ // backed the ID will be provided by the DB and this can be removed.
+ let id = 0;
+ let legacyProjectsWithId = legacyProjects.map((p) => {
+ p.id = id++;
+ return p;
});
+ const resultProjects = legacyProjectsWithId.map(
+ migrateProjectInfoForGeneralization
+ );
+ return resultProjects;
+}
+
+export async function fetchProjectsMetadata(): Promise {
+ try {
+ const response = await fetch(
+ `${process.env.PUBLIC_URL}/data/glTF-projects-metadata.json`
+ );
+ return response.json();
+ } catch (error) {
+ console.error(`Error fetching data. Reason: ${error}`);
+ return undefined;
+ }
}
diff --git a/src/store/Sagas.ts b/src/store/Sagas.ts
index 1a33b99..8c35560 100644
--- a/src/store/Sagas.ts
+++ b/src/store/Sagas.ts
@@ -1,16 +1,18 @@
import { fork, put } from "redux-saga/effects";
-import { updateProjects as watchForProjectUpdates } from "./projects/Sagas";
+import { updateProjects, updateProjectsMetadata } from "./projects/Sagas";
import { ProjectsActionTypes } from "./projects/Types";
import { watchForFilterRecalculate } from "./filters/Sagas";
import { watchForResultUpdates } from "./results/Sagas";
function* startup() {
+ yield put({ type: ProjectsActionTypes.PROJECTS_METADATA_REQUESTED });
yield put({ type: ProjectsActionTypes.PROJECTS_REQUESTED });
}
export default function* root() {
yield fork(startup);
- yield fork(watchForProjectUpdates);
+ yield fork(updateProjectsMetadata);
+ yield fork(updateProjects);
yield fork(watchForFilterRecalculate);
yield fork(watchForResultUpdates);
}
diff --git a/src/store/filters/Interfaces.ts b/src/store/filters/Interfaces.ts
index 6f2d2e3..ca21e9b 100644
--- a/src/store/filters/Interfaces.ts
+++ b/src/store/filters/Interfaces.ts
@@ -11,9 +11,8 @@ export type FiltersActions =
* only called ONCE (?).
*
* The `filterOptions` are in fact the options for the filters. It is a
- * map that maps ProjectProperties keys (i.e. the keys of the
- * IProjectInfo#properties map) to the filters that have been created
- * for the respective property for ALL available projects.
+ * map that maps projectsMetadata tags to the filters that have been
+ * created for the respective tag for ALL available projects.
*
* @property type The type
* @property filterOptions The filter options
diff --git a/src/store/filters/Sagas.ts b/src/store/filters/Sagas.ts
index a7eb455..51f9df4 100644
--- a/src/store/filters/Sagas.ts
+++ b/src/store/filters/Sagas.ts
@@ -1,36 +1,31 @@
import { takeEvery, select, put } from "redux-saga/effects";
import { createNewFilter, IFilter } from "../../interfaces/IFilter";
import { IProjectInfo } from "../../interfaces/IProjectInfo";
-import { ProjectFilterProperties } from "../../interfaces/IProjectInfo";
import { ProjectsActionTypes } from "../projects/Types";
import * as projectSelectors from "../projects/Selectors";
import * as actions from "./Actions";
const DEFAULT_FULL_TEXT_TITLE_VALUE = "";
-function calculateFiltersForProperty(
- projects: IProjectInfo[],
- propertyName: string
-) {
+function calculateFiltersForTag(projects: IProjectInfo[], tagName: string) {
const tasks = [
- ...new Set(
- projects.flatMap((p) => p.properties[propertyName]).filter((x) => x)
- ),
+ ...new Set(projects.flatMap((p) => p.tags[tagName]).filter((x) => x)),
] as string[];
- return tasks.map((t) => createNewFilter(propertyName, t));
+ return tasks.map((t) => createNewFilter(tagName, t));
}
function* calculateFilters(): any {
const projects = yield select(projectSelectors.getProjects);
+ const projectsMetadata = yield select(projectSelectors.getProjectsMetadata);
const filterOptions = new Map();
- const filterProperties = Array.from(ProjectFilterProperties.keys());
- for (const propertyName of filterProperties) {
- const filters = calculateFiltersForProperty(projects, propertyName);
+ const filterTags = projectsMetadata.filterTags;
+ for (const tagName of filterTags) {
+ const filters = calculateFiltersForTag(projects, tagName);
const sortedFilters = filters.sort((f0, f1) =>
f0.value.localeCompare(f1.value)
);
- filterOptions.set(propertyName, sortedFilters);
+ filterOptions.set(tagName, sortedFilters);
}
yield put(
actions.updateFilters(filterOptions, DEFAULT_FULL_TEXT_TITLE_VALUE)
diff --git a/src/store/projects/Actions.ts b/src/store/projects/Actions.ts
index 663f4d7..af65760 100644
--- a/src/store/projects/Actions.ts
+++ b/src/store/projects/Actions.ts
@@ -2,7 +2,8 @@ import { Document } from "flexsearch";
import { IProjectInfo } from "../../interfaces/IProjectInfo";
import { IProjectSearchDoc } from "../../interfaces/IAppState";
import { ProjectsActionTypes } from "./Types";
-import { ISuccessfulProjectsAction, IFailedProjectsAction } from "./Interfaces";
+import { ISuccessfulProjectsAction, IFailedProjectsAction, ISuccessfulProjectsMetadataAction, IFailedProjectsMetadataAction } from "./Interfaces";
+import { IProjectsMetadata } from "../../interfaces/IProjectsMetadata";
export function successfulProjects(
projects: IProjectInfo[],
@@ -21,3 +22,19 @@ export function failedProjects(error: Error): IFailedProjectsAction {
error: error,
};
}
+
+export function successfulProjectsMetadata(
+ projectsMetadata: IProjectsMetadata
+): ISuccessfulProjectsMetadataAction {
+ return {
+ type: ProjectsActionTypes.PROJECTS_METADATA_SUCCESSFUL,
+ projectsMetadata: projectsMetadata,
+ };
+}
+
+export function failedProjectsMetadata(error: Error): IFailedProjectsMetadataAction {
+ return {
+ type: ProjectsActionTypes.PROJECTS_METADATA_FAILED,
+ error: error,
+ };
+}
diff --git a/src/store/projects/Interfaces.ts b/src/store/projects/Interfaces.ts
index 2cd7aaa..91bd826 100644
--- a/src/store/projects/Interfaces.ts
+++ b/src/store/projects/Interfaces.ts
@@ -2,23 +2,42 @@ import { Document } from "flexsearch";
import { IProjectSearchDoc } from "../../interfaces/IAppState";
import { IProjectInfo } from "../../interfaces/IProjectInfo";
import { ProjectsActionTypes } from "./Types";
+import { IProjectsMetadata } from "../../interfaces/IProjectsMetadata";
export type ProjectsActions =
| IRequestProjectsAction
| ISuccessfulProjectsAction
- | IFailedProjectsAction;
+ | IFailedProjectsAction
+ | IRequestProjectsMetadataAction
+ | ISuccessfulProjectsMetadataAction
+ | IFailedProjectsMetadataAction;
-interface IRequestProjectsAction {
- readonly type: ProjectsActionTypes.PROJECTS_REQUESTED;
-}
+ interface IRequestProjectsAction {
+ readonly type: ProjectsActionTypes.PROJECTS_REQUESTED;
+ }
+
+ export interface ISuccessfulProjectsAction {
+ readonly type: ProjectsActionTypes.PROJECTS_SUCCESSFUL;
+ readonly projects: IProjectInfo[];
+ readonly searchIndex: Document;
+ }
+
+ export interface IFailedProjectsAction {
+ readonly type: ProjectsActionTypes.PROJECTS_FAILED;
+ readonly error: Error;
+ }
-export interface ISuccessfulProjectsAction {
- readonly type: ProjectsActionTypes.PROJECTS_SUCCESSFUL;
- readonly projects: IProjectInfo[];
- readonly searchIndex: Document;
-}
-
-export interface IFailedProjectsAction {
- readonly type: ProjectsActionTypes.PROJECTS_FAILED;
- readonly error: Error;
-}
+ interface IRequestProjectsMetadataAction {
+ readonly type: ProjectsActionTypes.PROJECTS_METADATA_REQUESTED;
+ }
+
+ export interface ISuccessfulProjectsMetadataAction {
+ readonly type: ProjectsActionTypes.PROJECTS_METADATA_SUCCESSFUL;
+ readonly projectsMetadata: IProjectsMetadata;
+ }
+
+ export interface IFailedProjectsMetadataAction {
+ readonly type: ProjectsActionTypes.PROJECTS_METADATA_FAILED;
+ readonly error: Error;
+ }
+
\ No newline at end of file
diff --git a/src/store/projects/Reducers.ts b/src/store/projects/Reducers.ts
index 5f594dd..ed731e1 100644
--- a/src/store/projects/Reducers.ts
+++ b/src/store/projects/Reducers.ts
@@ -6,6 +6,7 @@ export function projects(
state: IProjectsState = {
isFetchingProjects: false,
values: [],
+ isFetchingProjectsMetadata: false,
},
action: ProjectsActions
) {
@@ -21,6 +22,17 @@ export function projects(
};
case ProjectsActionTypes.PROJECTS_FAILED:
return { ...state, isFetchingProjects: false };
+ case ProjectsActionTypes.PROJECTS_METADATA_REQUESTED:
+ return { ...state, isFetchingProjectsMetadata: true };
+ case ProjectsActionTypes.PROJECTS_METADATA_SUCCESSFUL:
+ return {
+ ...state,
+ projectsMetadata: action.projectsMetadata,
+ isFetchingProjectsMetadata: false,
+ };
+ case ProjectsActionTypes.PROJECTS_METADATA_FAILED:
+ return { ...state, isFetchingProjectsMetadata: false };
+
default:
return state;
}
diff --git a/src/store/projects/Sagas.ts b/src/store/projects/Sagas.ts
index fd35951..39354cc 100644
--- a/src/store/projects/Sagas.ts
+++ b/src/store/projects/Sagas.ts
@@ -2,9 +2,11 @@ import * as actions from "./Actions";
import { call, put, takeEvery } from "redux-saga/effects";
import { Document } from "flexsearch";
import { fetchProjects } from "../../services/DataService";
+import { fetchProjectsMetadata } from "../../services/DataService";
import { IProjectInfo } from "../../interfaces/IProjectInfo";
import { IProjectSearchDoc } from "../../interfaces/IAppState";
import { ProjectsActionTypes } from "./Types";
+import { IProjectsMetadata } from "../../interfaces/IProjectsMetadata";
function* retrieveProjects() {
try {
@@ -38,3 +40,16 @@ function* retrieveProjects() {
export function* updateProjects() {
yield takeEvery(ProjectsActionTypes.PROJECTS_REQUESTED, retrieveProjects);
}
+
+function* retrieveProjectsMetadata() {
+ try {
+ const projectsMetadata: IProjectsMetadata = yield call(fetchProjectsMetadata);
+ yield put(actions.successfulProjectsMetadata(projectsMetadata));
+ } catch (err) {
+ yield put(actions.failedProjectsMetadata(err as Error));
+ }
+}
+
+export function* updateProjectsMetadata() {
+ yield takeEvery(ProjectsActionTypes.PROJECTS_METADATA_REQUESTED, retrieveProjectsMetadata);
+}
diff --git a/src/store/projects/Selectors.ts b/src/store/projects/Selectors.ts
index c3ccbfc..c930bd3 100644
--- a/src/store/projects/Selectors.ts
+++ b/src/store/projects/Selectors.ts
@@ -1,5 +1,5 @@
import { IAppState } from "../../interfaces/IAppState";
export const getProjects = (state: IAppState) => state.projects.values;
-
export const getSearchIndex = (state: IAppState) => state.projects.searchIndex;
+export const getProjectsMetadata = (state: IAppState) => state.projects.projectsMetadata;
diff --git a/src/store/projects/Types.ts b/src/store/projects/Types.ts
index c86f5f0..c7d3074 100644
--- a/src/store/projects/Types.ts
+++ b/src/store/projects/Types.ts
@@ -2,4 +2,9 @@ export enum ProjectsActionTypes {
PROJECTS_REQUESTED = "PROJECTS_REQUESTED",
PROJECTS_SUCCESSFUL = "PROJECTS_SUCCESSFUL",
PROJECTS_FAILED = "PROJECTS_FAILED",
+
+ PROJECTS_METADATA_REQUESTED = "PROJECTS_METADATA_REQUESTED",
+ PROJECTS_METADATA_SUCCESSFUL = "PROJECTS_METADATA_SUCCESSFUL",
+ PROJECTS_METADATA_FAILED = "PROJECTS_METADATA_FAILED",
+
}
diff --git a/src/store/results/Sagas.ts b/src/store/results/Sagas.ts
index 516e863..f1ca5fa 100644
--- a/src/store/results/Sagas.ts
+++ b/src/store/results/Sagas.ts
@@ -6,12 +6,12 @@ import { Document } from "flexsearch";
import { FilterActionTypes } from "../filters/Types";
import { IFilter } from "../../interfaces/IFilter";
import { IProjectInfo } from "../../interfaces/IProjectInfo";
-import { ProjectFilterProperties } from "../../interfaces/IProjectInfo";
+import { IProjectsMetadata } from "../../interfaces/IProjectsMetadata";
import { IProjectSearchDoc } from "../../interfaces/IAppState";
// Tags in these groups will be pulled to the top of the list.
// Priority is given to tags with lower index values.
-const tagPriority = ["Khronos Official", "Staff Picks"];
+const tagPriority = ["Khronos Official"];
const UNTAGGED_KEY = "UNTAGGED";
type ResultsBuckets = { [key: string]: IProjectInfo[] };
@@ -21,6 +21,7 @@ interface IGroupedFilters {
}
function applyTagFilters(
+ projectsMetadata: IProjectsMetadata,
projects: IProjectInfo[],
selectedFilters: Set
): IProjectInfo[] {
@@ -28,14 +29,14 @@ function applyTagFilters(
return projects;
}
- const filterPropertyNames = Array.from(ProjectFilterProperties.keys());
+ const filterTagNames = projectsMetadata.filterTags;
const groupedFilters = Array.from(selectedFilters).reduce(
(acc, curr) => {
- if (!acc[curr.propertyName]) {
- acc[curr.propertyName] = [];
+ if (!acc[curr.tagName]) {
+ acc[curr.tagName] = [];
}
- acc[curr.propertyName].push(curr);
+ acc[curr.tagName].push(curr);
return acc;
},
@@ -45,15 +46,22 @@ function applyTagFilters(
return projects.filter((project) => {
let match = false;
- for (const propertyName of filterPropertyNames) {
- if (!groupedFilters[propertyName]) continue;
+ for (const tagName of filterTagNames) {
+ if (!groupedFilters[tagName]) continue;
- match = groupedFilters[propertyName].some((filter) => {
- if (project.properties[propertyName]) {
+ match = groupedFilters[tagName].some((filter) => {
+ if (project.tags[tagName]) {
// Within the dimension we do an OR.
- return project.properties[propertyName]!.some(
- (v) => v === filter.value
- );
+
+ // TODO_V2: Here, the type will have to be checked.
+ // Right now, this assumes that the values are
+ // string arrays or single values
+ const tagValue = project.tags[tagName];
+ if (Array.isArray(tagValue)) {
+ return tagValue.some((v: any) => v === filter.value);
+ } else {
+ return tagValue === filter.value;
+ }
}
return false;
@@ -96,12 +104,13 @@ function sortResults(projects: IProjectInfo[]): IProjectInfo[] {
buckets[UNTAGGED_KEY] = [];
for (const project of projects) {
- if (!project.tags) {
+ const tags = project.tags["tags"];
+ if (!tags) {
buckets[UNTAGGED_KEY].push(project);
continue;
}
- for (const tag of project.tags) {
+ for (const tag of tags) {
if (buckets[tag]) {
buckets[tag].push(project);
} else {
@@ -110,25 +119,29 @@ function sortResults(projects: IProjectInfo[]): IProjectInfo[] {
}
}
- return Object.entries(buckets).flatMap(([_, projects]) => {
+ const result = Object.entries(buckets).flatMap(([_, projects]) => {
return projects.sort((a, b) => a.name.localeCompare(b.name));
});
+
+ return result;
}
function* applyFilters() {
- const [projects, searchIndex, selectedFilters, query]: [
+ const [projectsMetadata, projects, searchIndex, selectedFilters, query]: [
+ IProjectsMetadata,
IProjectInfo[],
Document | undefined,
Set,
string
] = yield all([
+ select(projectSelectors.getProjectsMetadata),
select(projectSelectors.getProjects),
select(projectSelectors.getSearchIndex),
select(filterSelectors.getSelectedFilters),
select(filterSelectors.getTitleSubstring),
]);
- const filteredResults = applyTagFilters(projects, selectedFilters);
+ const filteredResults = applyTagFilters(projectsMetadata, projects, selectedFilters);
const searchedResults = applyIndexedSearch(
filteredResults,
searchIndex,
diff --git a/src/utils/FilterHelpers.ts b/src/utils/FilterHelpers.ts
index 96703ba..ed00f22 100644
--- a/src/utils/FilterHelpers.ts
+++ b/src/utils/FilterHelpers.ts
@@ -1,13 +1,13 @@
import { IFilter } from "../interfaces/IFilter";
const NUM_FILTER_COLORS = 8; // As defined in tailwind.config.css
-const filterPropertyNames: string[] = [];
+const filterTagNames: string[] = [];
export const determineClassName = (filter: IFilter) => {
- let index = filterPropertyNames.indexOf(filter.propertyName);
+ let index = filterTagNames.indexOf(filter.tagName);
if (index < 0) {
- index = filterPropertyNames.length;
- filterPropertyNames.push(filter.propertyName);
+ index = filterTagNames.length;
+ filterTagNames.push(filter.tagName);
}
index %= NUM_FILTER_COLORS;
return `filter-${index}`;
diff --git a/yarn.lock b/yarn.lock
index 1fac294..57e0e22 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8628,11 +8628,6 @@ react-error-overlay@^6.0.10:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
-react-github-fork-ribbon@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/react-github-fork-ribbon/-/react-github-fork-ribbon-0.6.0.tgz#8b7454fec5bd5481af7fdf6d0fc8aa7653373301"
- integrity sha512-lmobEGwg1k/JGMJMbiWNh3dd+5HUtePVqUu1LPr5L65MiK6mnn7ZgkGpFYXLijPxO9hD7x+f9qUrEzeazQdx2g==
-
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"