@eventvisor/catalog 0.0.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/LICENSE +21 -0
  3. package/components.json +22 -0
  4. package/dist/assets/index-B8NMB5dQ.js +24 -0
  5. package/dist/assets/index-DdncAyM-.css +1 -0
  6. package/dist/catalog.json +297 -0
  7. package/dist/img/logo-text.png +0 -0
  8. package/dist/img/logo.png +0 -0
  9. package/dist/index.html +13 -0
  10. package/index.html +12 -0
  11. package/package.json +37 -16
  12. package/public/catalog.json +297 -0
  13. package/public/img/logo-text.png +0 -0
  14. package/public/img/logo.png +0 -0
  15. package/src/blocks/alert.tsx +30 -0
  16. package/src/blocks/content.tsx +43 -0
  17. package/src/blocks/last-modified.tsx +26 -0
  18. package/src/blocks/links.tsx +42 -0
  19. package/src/blocks/markdown.tsx +10 -0
  20. package/src/blocks/navbar.tsx +46 -0
  21. package/src/blocks/page-attributes-view.tsx +127 -0
  22. package/src/blocks/page-attributes.tsx +80 -0
  23. package/src/blocks/page-destinations-view.tsx +134 -0
  24. package/src/blocks/page-destinations.tsx +80 -0
  25. package/src/blocks/page-effects-view.tsx +139 -0
  26. package/src/blocks/page-effects.tsx +80 -0
  27. package/src/blocks/page-events-view.tsx +147 -0
  28. package/src/blocks/page-events.tsx +80 -0
  29. package/src/blocks/pretty-date.tsx +52 -0
  30. package/src/blocks/properties.tsx +170 -0
  31. package/src/blocks/root.tsx +116 -0
  32. package/src/blocks/search-input.tsx +32 -0
  33. package/src/blocks/tabs.tsx +47 -0
  34. package/src/blocks/tag.tsx +13 -0
  35. package/src/contexts/index.ts +4 -0
  36. package/src/hooks/index.ts +98 -0
  37. package/src/main.tsx +13 -0
  38. package/src/styles.css +126 -0
  39. package/src/utils/index.ts +171 -0
  40. package/tsconfig.json +30 -0
  41. package/tsconfig.node.json +11 -0
  42. package/vite.config.ts +15 -0
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+
3
+ import { TagIcon } from "@heroicons/react/20/solid";
4
+
5
+ import { Content } from "./content";
6
+ import { useEntitiesAsList } from "../hooks";
7
+ import { LinkEvent } from "./links";
8
+ import { Alert } from "./alert";
9
+ import { LastModified } from "./last-modified";
10
+ import { Tag } from "./tag";
11
+ import { SearchInput } from "./search-input";
12
+ import { useSearch } from "../hooks";
13
+ import { getEventsByQuery } from "../utils";
14
+
15
+ export const PageEvents: React.FC = () => {
16
+ const entities = useEntitiesAsList("events");
17
+ const { searchQuery } = useSearch();
18
+
19
+ const filteredEntities = getEventsByQuery(searchQuery, entities);
20
+
21
+ return (
22
+ <Content title="Events">
23
+ <SearchInput />
24
+
25
+ {filteredEntities.length === 0 && <Alert type="warning">No results found</Alert>}
26
+
27
+ {filteredEntities.length > 0 && (
28
+ <div>
29
+ <ul className="diving-gray-200 divide-y">
30
+ {filteredEntities.map((entity: any) => (
31
+ <li key={entity.name}>
32
+ <LinkEvent name={entity.name} className="block hover:bg-gray-50">
33
+ <div className="px-6 py-4">
34
+ <div className="flex items-center justify-between">
35
+ <p className="text-md relative font-bold text-zinc-600">
36
+ {entity.name}{" "}
37
+ {entity.archived && (
38
+ <span className="ml-1 rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-800">
39
+ archived
40
+ </span>
41
+ )}
42
+ </p>
43
+
44
+ <div className="ml-2 flex flex-shrink-0 text-xs text-gray-500">
45
+ <div>
46
+ <TagIcon className="inline-block h-6 w-6 pr-1 text-xs text-gray-400" />
47
+ {entity.tags.map((tag: string) => (
48
+ <Tag tag={tag} key={tag} />
49
+ ))}
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div className="mt-2 flex justify-between">
55
+ <div className="flex">
56
+ <p className="line-clamp-3 max-w-md items-center text-sm text-gray-500">
57
+ {entity.description && entity.description.trim().length > 0
58
+ ? entity.description
59
+ : "n/a"}
60
+ </p>
61
+ </div>
62
+
63
+ <div className="items-top mt-2 flex text-xs text-gray-500 sm:mt-0">
64
+ <LastModified lastModified={entity.lastModified} />
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </LinkEvent>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </div>
73
+ )}
74
+
75
+ <p className="mt-6 text-center text-sm text-gray-500">
76
+ A total of <span className="font-bold">{filteredEntities.length}</span> results found.
77
+ </p>
78
+ </Content>
79
+ );
80
+ };
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+
3
+ function getPrettyDate(props: any) {
4
+ const { showTime } = props;
5
+ const date = new Date(props.date);
6
+
7
+ const day = date.getDate();
8
+ const month = new Intl.DateTimeFormat("en-US", { month: "short" }).format(date);
9
+ const year = date.getFullYear();
10
+
11
+ const ordinalSuffix = (day: number) => {
12
+ switch (day % 10) {
13
+ case 1:
14
+ return "st";
15
+ case 2:
16
+ return "nd";
17
+ case 3:
18
+ return "rd";
19
+ default:
20
+ return "th";
21
+ }
22
+ };
23
+
24
+ const prettyDate = `${day}${ordinalSuffix(day)} ${month} ${year}`;
25
+
26
+ if (showTime) {
27
+ const hours = date.getHours();
28
+ const minutes = date.getMinutes();
29
+ const ampm = hours >= 12 ? "PM" : "AM";
30
+ const hour12 = hours % 12 || 12;
31
+ const prettyTime = `${hour12}:${minutes.toString().padStart(2, "0")}${ampm}`;
32
+
33
+ return (
34
+ <>
35
+ <span className="font-normal text-gray-500">{prettyDate}</span>{" "}
36
+ <span className="font-normal text-gray-500">at</span>{" "}
37
+ <span className="font-normal text-gray-500">{prettyTime}</span>
38
+ </>
39
+ );
40
+ }
41
+
42
+ return prettyDate;
43
+ }
44
+
45
+ interface PrettyDateProps {
46
+ date: string;
47
+ showTime?: boolean;
48
+ }
49
+
50
+ export function PrettyDate(props: PrettyDateProps) {
51
+ return <>{getPrettyDate(props)}</>;
52
+ }
@@ -0,0 +1,170 @@
1
+ import React from "react";
2
+
3
+ type PropertyInfo = {
4
+ path: string;
5
+ type: string;
6
+ required: boolean;
7
+ description?: string;
8
+ default?: any;
9
+ };
10
+
11
+ function flattenSchemaProperties(schema: any, basePath: string = ""): PropertyInfo[] {
12
+ let results: PropertyInfo[] = [];
13
+ if (!schema || typeof schema !== "object") return results;
14
+
15
+ if (schema.type === "object" && schema.properties && typeof schema.properties === "object") {
16
+ const keys = Object.keys(schema.properties);
17
+ for (const key of keys) {
18
+ const propSchema = schema.properties[key];
19
+ // A property is required if it's listed in the current schema's required array
20
+ const isRequired = (schema.required || []).includes(key);
21
+ const fullPath = basePath ? `${basePath}.${key}` : key;
22
+
23
+ if (
24
+ propSchema.type === "object" &&
25
+ propSchema.properties &&
26
+ typeof propSchema.properties === "object"
27
+ ) {
28
+ // Add the intermediate object path if it has properties
29
+ results.push({
30
+ path: fullPath,
31
+ type: "object",
32
+ required: isRequired,
33
+ description: propSchema.description,
34
+ default: propSchema.default,
35
+ });
36
+
37
+ // Recursively flatten nested objects
38
+ results = results.concat(flattenSchemaProperties(propSchema, fullPath));
39
+ } else if (propSchema.type === "array" && propSchema.items) {
40
+ // Show the type as array of type if it's an array
41
+ let itemType = "";
42
+ if (propSchema.items.type) {
43
+ itemType = propSchema.items.type;
44
+ } else if (propSchema.items.$ref) {
45
+ itemType = propSchema.items.$ref;
46
+ } else if (propSchema.items) {
47
+ itemType = "object";
48
+ }
49
+ results.push({
50
+ path: fullPath,
51
+ type: `array of ${itemType}`,
52
+ required: isRequired,
53
+ description: propSchema.description,
54
+ default: propSchema.default,
55
+ });
56
+
57
+ // If array items are objects with properties, also flatten them:
58
+ if (propSchema.items && propSchema.items.type === "object" && propSchema.items.properties) {
59
+ results = results.concat(flattenSchemaProperties(propSchema.items, `${fullPath}[]`));
60
+ }
61
+ } else {
62
+ // Leaf property
63
+ results.push({
64
+ path: fullPath,
65
+ type: propSchema.type || "any",
66
+ required: isRequired,
67
+ description: propSchema.description,
68
+ default: propSchema.default,
69
+ });
70
+ }
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+
76
+ export const PropertiesTable: React.FC<{ schema: any }> = ({ schema }) => {
77
+ const properties = flattenSchemaProperties(schema);
78
+
79
+ if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) {
80
+ return (
81
+ <div className="flex items-center justify-center p-8 bg-gray-50 rounded-xl border-2 border-dashed border-gray-200">
82
+ <div className="text-center">
83
+ <div className="text-gray-400 text-4xl mb-2">📋</div>
84
+ <p className="text-gray-500 text-sm font-medium">No properties defined</p>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ return (
91
+ <div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
92
+ {/* Table Header */}
93
+ <div className="bg-gray-50 px-4 py-2 border-b border-gray-200">
94
+ <h3 className="text-sm font-semibold text-gray-700">Properties</h3>
95
+ </div>
96
+
97
+ {/* Table */}
98
+ <div className="overflow-x-auto">
99
+ <table className="w-full">
100
+ <thead className="bg-gray-50 border-b border-gray-200">
101
+ <tr>
102
+ <th className="text-left px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wider">
103
+ Path
104
+ </th>
105
+ <th className="text-left px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wider">
106
+ Type
107
+ </th>
108
+ <th className="text-left px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wider">
109
+ Required
110
+ </th>
111
+ <th className="text-left px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wider">
112
+ Description
113
+ </th>
114
+ <th className="text-left px-4 py-3 text-xs font-semibold text-gray-700 uppercase tracking-wider">
115
+ Default
116
+ </th>
117
+ </tr>
118
+ </thead>
119
+ <tbody className="divide-y divide-gray-200">
120
+ {properties.map((prop) => {
121
+ return (
122
+ <tr key={prop.path} className="hover:bg-gray-50 transition-colors">
123
+ <td className="px-4 py-2">
124
+ <code className="text-xs font-mono text-gray-800">{prop.path}</code>
125
+ </td>
126
+ <td className="px-4 py-2">
127
+ <span className="text-xs text-gray-700">{prop.type}</span>
128
+ </td>
129
+ <td className="px-4 py-2">
130
+ {prop.required ? (
131
+ <span className="text-xs text-green-700 font-medium">Yes</span>
132
+ ) : (
133
+ <span className="text-xs text-gray-400">No</span>
134
+ )}
135
+ </td>
136
+ <td className="px-4 py-2">
137
+ <div className="text-xs text-gray-600">
138
+ {prop.description ? (
139
+ <span>{prop.description}</span>
140
+ ) : (
141
+ <span className="text-gray-300 italic">n/a</span>
142
+ )}
143
+ </div>
144
+ </td>
145
+ <td className="px-4 py-2">
146
+ <div className="text-xs text-gray-600">
147
+ {prop.default !== undefined ? (
148
+ <code className="bg-gray-100 px-1 py-0.5 rounded text-xs">
149
+ {typeof prop.default === "string"
150
+ ? `"${prop.default}"`
151
+ : String(prop.default)}
152
+ </code>
153
+ ) : (
154
+ <span className="text-gray-300 italic">n/a</span>
155
+ )}
156
+ </div>
157
+ </td>
158
+ </tr>
159
+ );
160
+ })}
161
+ </tbody>
162
+ </table>
163
+ </div>
164
+ </div>
165
+ );
166
+ };
167
+
168
+ export function Properties({ schema }: { schema: any }) {
169
+ return <PropertiesTable schema={schema} />;
170
+ }
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+ import { HashRouter, Routes, Route, useNavigate } from "react-router";
3
+ import { Catalog } from "@eventvisor/types";
4
+
5
+ import { Navbar } from "./navbar";
6
+ import { Content } from "./content";
7
+ import { PageEvents } from "./page-events";
8
+ import { PageEventsView, DisplayEventOverview } from "./page-events-view";
9
+ import { PageAttributes } from "./page-attributes";
10
+ import { PageAttributesView, DisplayAttributeOverview } from "./page-attributes-view";
11
+ import { PageDestinations } from "./page-destinations";
12
+ import { PageDestinationsView, DisplayDestinationOverview } from "./page-destinations-view";
13
+ import { PageEffects } from "./page-effects";
14
+ import { PageEffectsView, DisplayEffectOverview } from "./page-effects-view";
15
+ import { CatalogContext } from "../contexts";
16
+
17
+ import { Alert } from "./alert";
18
+
19
+ const HomePage: React.FC = () => {
20
+ const navigate = useNavigate();
21
+
22
+ React.useEffect(() => {
23
+ navigate("/events");
24
+ }, []);
25
+
26
+ return (
27
+ <Content title="Home">
28
+ <div className="mt-6">
29
+ <Alert type="warning">Loading...</Alert>
30
+ </div>
31
+ </Content>
32
+ );
33
+ };
34
+
35
+ function Wrapper({ children }: { children: React.ReactNode }) {
36
+ return (
37
+ <div className="max-w-3xl pb-4 mx-auto mt-10 bg-white min-h-60 rounded-md shadow">
38
+ {children}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export const Root: React.FC = () => {
44
+ const [catalog, setCatalog] = React.useState<Catalog | null>(null);
45
+
46
+ React.useEffect(() => {
47
+ fetch("/catalog.json")
48
+ .then((response) => response.json())
49
+ .then((data) => setCatalog(data as Catalog));
50
+ }, []);
51
+
52
+ if (!catalog) {
53
+ return (
54
+ <HashRouter>
55
+ <Navbar />
56
+
57
+ <Wrapper>
58
+ <Content title="Home">
59
+ <div className="mt-6">
60
+ <Alert type="warning">Loading...</Alert>
61
+ </div>
62
+ </Content>
63
+ </Wrapper>
64
+ </HashRouter>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <HashRouter>
70
+ <Navbar />
71
+
72
+ <Wrapper>
73
+ <CatalogContext.Provider value={catalog}>
74
+ <Routes>
75
+ <Route path="/" element={<HomePage />} />
76
+
77
+ <Route path="attributes">
78
+ <Route index element={<PageAttributes />} />
79
+ <Route path=":name" element={<PageAttributesView />}>
80
+ <Route index element={<DisplayAttributeOverview />} />
81
+ </Route>
82
+ </Route>
83
+
84
+ <Route path="events">
85
+ <Route index element={<PageEvents />} />
86
+ <Route path=":name" element={<PageEventsView />}>
87
+ <Route index element={<DisplayEventOverview />} />
88
+ </Route>
89
+ </Route>
90
+
91
+ <Route path="destinations">
92
+ <Route index element={<PageDestinations />} />
93
+ <Route path=":name" element={<PageDestinationsView />}>
94
+ <Route index element={<DisplayDestinationOverview />} />
95
+ </Route>
96
+ </Route>
97
+
98
+ <Route path="effects">
99
+ <Route index element={<PageEffects />} />
100
+ <Route path=":name" element={<PageEffectsView />}>
101
+ <Route index element={<DisplayEffectOverview />} />
102
+ </Route>
103
+ </Route>
104
+ </Routes>
105
+ </CatalogContext.Provider>
106
+ </Wrapper>
107
+
108
+ <div className="text-xs text-zinc-500 mt-10 text-center pb-10">
109
+ Built with{" "}
110
+ <a target="_blank" href="https://eventvisor.org">
111
+ Eventvisor
112
+ </a>
113
+ </div>
114
+ </HashRouter>
115
+ );
116
+ };
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ import { useSearch } from "../hooks";
3
+
4
+ export function SearchInput() {
5
+ const { searchQuery, setSearchQuery } = useSearch();
6
+
7
+ return (
8
+ <div className="relative px-4 pt-3.5">
9
+ <div className="pointer-events-none absolute">
10
+ <svg
11
+ className="absolute ml-3 mt-5 h-6 w-6 text-zinc-400"
12
+ viewBox="0 0 20 20"
13
+ fill="currentColor"
14
+ >
15
+ <path
16
+ fillRule="evenodd"
17
+ d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
18
+ clipRule="evenodd"
19
+ />
20
+ </svg>
21
+ </div>
22
+ <input
23
+ type="text"
24
+ value={searchQuery}
25
+ onChange={(e) => setSearchQuery(e.target.value)}
26
+ placeholder="Type to search..."
27
+ autoComplete="off"
28
+ className="mb-4 mt-2 p-2 w-full rounded-full border border-zinc-300 indent-8 text-xl text-zinc-500 placeholder:text-zinc-400"
29
+ />
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import { NavLink } from "react-router";
3
+
4
+ interface Tab {
5
+ title: string;
6
+ to: string;
7
+ end?: boolean;
8
+ }
9
+
10
+ interface TabsProps {
11
+ tabs: Tab[];
12
+ }
13
+
14
+ export function Tabs(props: TabsProps) {
15
+ const { tabs } = props;
16
+
17
+ return (
18
+ <div className="border-b border-gray-200">
19
+ <div className="flex">
20
+ {tabs.map((tab) => (
21
+ <NavLink
22
+ key={tab.title}
23
+ to={tab.to}
24
+ end={Boolean(tab.end)}
25
+ className={({ isActive }) =>
26
+ [
27
+ "w-1/4",
28
+ "border-b-2",
29
+ "pt-2",
30
+ "pb-4",
31
+ "px-1",
32
+ "text-center",
33
+ "text-sm",
34
+ "font-medium",
35
+ isActive
36
+ ? "border-slate-500 text-slate-600"
37
+ : "border-transparent text-slate-500 hover:border-slate-300 hover:text-slate-700",
38
+ ].join(" ")
39
+ }
40
+ >
41
+ {tab.title}
42
+ </NavLink>
43
+ ))}
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+
3
+ interface TagProps {
4
+ tag: string;
5
+ }
6
+
7
+ export function Tag(props: TagProps) {
8
+ return (
9
+ <span className="mr-1 rounded-full border-zinc-400 bg-zinc-300 px-2 py-1 text-xs text-gray-600">
10
+ {props.tag}
11
+ </span>
12
+ );
13
+ }
@@ -0,0 +1,4 @@
1
+ import * as React from "react";
2
+ import { Catalog } from "@eventvisor/types";
3
+
4
+ export const CatalogContext = React.createContext<Catalog | undefined>(undefined);
@@ -0,0 +1,98 @@
1
+ import React from "react";
2
+ import { useSearchParams } from "react-router";
3
+
4
+ import { CatalogContext } from "../contexts";
5
+
6
+ export function useCatalog() {
7
+ return React.useContext(CatalogContext);
8
+ }
9
+
10
+ export type EntitiesType = "attributes" | "events" | "destinations" | "effects";
11
+
12
+ export function useEntities(type: EntitiesType) {
13
+ const catalog = useCatalog();
14
+
15
+ if (!catalog) {
16
+ return [];
17
+ }
18
+
19
+ return catalog.entities[type];
20
+ }
21
+
22
+ export function useEntityEditLink(entityType: string, entityName: string) {
23
+ const catalog = useCatalog();
24
+
25
+ if (!catalog) {
26
+ return;
27
+ }
28
+
29
+ const link = catalog.links?.[entityType as keyof typeof catalog.links];
30
+
31
+ if (!link) {
32
+ return;
33
+ }
34
+
35
+ return link.replace("{{name}}", entityName);
36
+ }
37
+
38
+ export function useEntitiesAsList(type: EntitiesType) {
39
+ const entities = useEntities(type);
40
+
41
+ const list = React.useMemo(() => {
42
+ const arr = [];
43
+
44
+ for (const [key, value] of Object.entries(entities)) {
45
+ arr.push({
46
+ name: key,
47
+ ...value,
48
+ });
49
+ }
50
+
51
+ return arr;
52
+ }, [entities]);
53
+
54
+ return list;
55
+ }
56
+
57
+ export function useEntityNames(type: EntitiesType) {
58
+ const catalog = useCatalog();
59
+
60
+ if (!catalog) {
61
+ return [];
62
+ }
63
+
64
+ return Object.keys(catalog.entities[type]);
65
+ }
66
+
67
+ export function useEntity(type: EntitiesType, name: string) {
68
+ const catalog = useCatalog();
69
+
70
+ if (!catalog) {
71
+ return undefined;
72
+ }
73
+
74
+ return catalog.entities[type][name];
75
+ }
76
+
77
+ export const SEARCH_QUERY_KEY = "q";
78
+
79
+ export function useSearch() {
80
+ const [searchParams, setSearchParams] = useSearchParams();
81
+ const searchQuery = String(searchParams.get(SEARCH_QUERY_KEY) || "");
82
+
83
+ const setSearchQuery = React.useCallback((value: string) => {
84
+ const newSearchParams = new URLSearchParams(searchParams.toString());
85
+
86
+ if (value) {
87
+ newSearchParams.set(SEARCH_QUERY_KEY, String(value));
88
+ } else {
89
+ newSearchParams.delete(SEARCH_QUERY_KEY);
90
+ }
91
+ setSearchParams(newSearchParams);
92
+ }, []);
93
+
94
+ return {
95
+ searchQuery,
96
+ setSearchQuery,
97
+ } as const;
98
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ import { Root } from "./blocks/root";
5
+
6
+ import "./styles.css";
7
+
8
+ const rootElement = document.getElementById("root");
9
+ if (!rootElement) {
10
+ throw new Error("Root element not found");
11
+ }
12
+
13
+ createRoot(rootElement).render(<Root />);