Skip to content

Commit f223642

Browse files
committed
updated readme to add some context and reminiscing
1 parent 5c4db67 commit f223642

File tree

3 files changed

+421
-143
lines changed

3 files changed

+421
-143
lines changed

README.MD

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
# Pareto Operating System (ParetOS)
22

3+
Archive Note: In the days before ChatGPT, the ParetOS was an open-source project from 2019-2021 that worked to create a digital mentorship system for aspiring developers, as they worked through a series of practical, experience-based learning modules part of a curriculum sometimes called the Full-Stack Apprenticeship. Before I continue, I wanted to give a shoutout to the 32 developers who contributed their time and code to the project, especially [Jaye Clark](https://github.com/jayeclark) who spearheaded a large scale redesign of the UI in 2020 & was the finest open-source developer I've had the pleasure of working with.
4+
5+
To run an offline-only, static-data-only 'archive' of the ParetOS, simply clone the repository and run `pnpm install && pnpm start` - thanks to the magic of locked dependencies (no carets in your package.json), as of October 30th 2025, the dependencies still install just like they did 2+ years ago.
6+
7+
The modules of the 'Experience' section were designed to work alongside a set of offline paper products and a workbook I designed for aspiring software developers, as the prototype of an educational business. In addition, we had an online, multi-player habit tracker meant to incentivize aspiring developers to improve their study and life habits to help them increase their chance of breaking into the industry. We also had a 'Library of Context', a collection of links and resources curated to guide developers to the right places, but whose data model was designed by me in 2018 in a terrible, inneficient way abstracting types and ids to a headless CMS, resulting in a lack of filterable search for the filters today without that secondary data schema available to us. A simple search of all 600+ resources is available in the Context page.
8+
9+
Perhaps I will write a blog post about this sometime, but it certainly feels odd looking at this in the age of AI development. With the benefit of hindsight and experience, what took 32 people, could probably be done by one person in less time, with access to the latest in AI tooling. It makes me wonder what the future of software development would be; 'back in the day' you actually had to write the code and develop muscle memory & debugging skills. Now, you just lean back in your chair and guide it to the point you want. What a time to be alive.
10+
11+
## Original README below
12+
313
The ParetOS is a browser-based, high-level operating system designed to maximize human potential. It is designed to give you the 20% of software you need on a day-to-day basis that gives you 80% of the benefits modern tech has to offer.
414

5-
Live at [https://paret0.com](https://paret0.com)
15+
Live at [https://paret0.com](https://paret0.com) **note** as of October 2025 the landing page here works, but the sign-in likely doesn't.
616

717
## The Arena
818

src/context/ContextBuilder.tsx

Lines changed: 207 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState, useEffect, SetStateAction, Dispatch, KeyboardEventHandler, KeyboardEvent } from "react";
1+
import { useState, useEffect, SetStateAction, Dispatch, KeyboardEventHandler, KeyboardEvent, useMemo } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { I18n } from "@aws-amplify/core";
4-
import { AppBar, Tabs, Tab, Box } from "@mui/material";
4+
import { AppBar, Tabs, Tab, Box, TextField, InputAdornment, FormControl, Select, MenuItem, InputLabel, Chip } from "@mui/material";
55
import Tour from "reactour";
66
import imageUrlBuilder from "@sanity/image-url";
77
import classNames from "classnames";
@@ -10,6 +10,7 @@ import ContextObject from "./ContextObject";
1010
import question from "../assets/question.svg";
1111
import sanity from "../libs/sanity";
1212
import TabPanel from "../components/TabPanel";
13+
import { sanityObjects } from "../offline-data/sanity-objects";
1314

1415
const builder = imageUrlBuilder(sanity);
1516

@@ -18,6 +19,52 @@ const builder = imageUrlBuilder(sanity);
1819
*
1920
*/
2021

22+
// Auto-categorize resources based on keywords in their titles
23+
const categorizeResource = (resource: any) => {
24+
const title = resource.title?.toLowerCase() || '';
25+
26+
// Define category patterns
27+
if (title.includes('react') || title.includes('vue') || title.includes('angular') ||
28+
title.includes('javascript') || title.includes('typescript') || title.includes('css') ||
29+
title.includes('html') || title.includes('web')) {
30+
return 'Web Development';
31+
}
32+
if (title.includes('security') || title.includes('csrf') || title.includes('sql injection') ||
33+
title.includes('auth')) {
34+
return 'Security';
35+
}
36+
if (title.includes('design') || title.includes('ui') || title.includes('ux') ||
37+
title.includes('color') || title.includes('font')) {
38+
return 'Design & UX';
39+
}
40+
if (title.includes('job') || title.includes('work') || title.includes('hire') ||
41+
title.includes('career') || title.includes('interview') || title.includes('freelanc')) {
42+
return 'Career & Jobs';
43+
}
44+
if (title.includes('startup') || title.includes('founder') || title.includes('vc') ||
45+
title.includes('incubat') || title.includes('venture') || title.includes('capital')) {
46+
return 'Startups & Funding';
47+
}
48+
if (title.includes('community') || title.includes('meetup') || title.includes('club') ||
49+
title.includes('group')) {
50+
return 'Communities';
51+
}
52+
if (title.includes('learn') || title.includes('tutorial') || title.includes('course') ||
53+
title.includes('academy') || title.includes('training')) {
54+
return 'Learning Resources';
55+
}
56+
if (title.includes('tool') || title.includes('library') || title.includes('framework') ||
57+
title.includes('service') || title.includes('api')) {
58+
return 'Tools & Services';
59+
}
60+
if (title.includes('africa') || title.includes('nigeria') || title.includes('uganda') ||
61+
title.includes('costa rica') || title.includes('seattle')) {
62+
return 'Regional';
63+
}
64+
65+
return 'Other';
66+
};
67+
2168
interface ContextBuilderProps {
2269
navigate: typeof useNavigate;
2370
sanitySchemas: {
@@ -29,68 +76,62 @@ interface ContextBuilderProps {
2976

3077
function ContextBuilder(props: ContextBuilderProps) {
3178
const [isTourOpen, setIsTourOpen] = useState(false);
32-
const [value, setValue] = useState<number>(0);
79+
const [searchTerm, setSearchTerm] = useState("");
80+
const [selectedCategory, setSelectedCategory] = useState("All");
3381
const navigate = props.navigate();
3482

35-
useEffect(() => {
36-
let initialTab = localStorage.getItem("contextTab");
37-
if (initialTab !== null) {
38-
setValue(parseInt(initialTab, 10));
39-
}
83+
// Get unique categories from all resources
84+
const categories = useMemo(() => {
85+
const cats = new Set(['All']);
86+
sanityObjects.forEach(item => {
87+
cats.add(categorizeResource(item));
88+
});
89+
return Array.from(cats).sort();
4090
}, []);
4191

42-
const renderTopicsList = (topics: LibraryEntry[]) => {
43-
const newCardClass = classNames("context-card", "second-step-library");
44-
45-
return (
46-
<div className="context-cards" style={{padding: 120}}>
47-
{topics?.length > 0 &&
48-
topics.map((topic) => {
49-
const link = topic.type === "hub" ? "hubs" : "context";
50-
// Handle cases where mainImage might be null or undefined
51-
let img: any = "na";
52-
if (topic.mainImage?.asset?._ref) {
53-
img = builder.image(topic.mainImage.asset._ref);
54-
}
55-
const handleKeyPress: KeyboardEventHandler<HTMLDivElement> = (e: KeyboardEvent) => {
56-
if (e.key === 'Enter') {
57-
navigate(`/${link}/${topic.slug?.current || topic._id}`)
58-
}
59-
}
92+
// Filter resources based on search and category
93+
const filteredItems = useMemo(() => {
94+
let filtered = sanityObjects || [];
95+
96+
// Apply search filter
97+
if (searchTerm) {
98+
filtered = filtered.filter(item =>
99+
item.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
100+
item.url?.toLowerCase().includes(searchTerm.toLowerCase())
101+
);
102+
}
103+
104+
// Apply category filter
105+
if (selectedCategory && selectedCategory !== 'All') {
106+
filtered = filtered.filter(item =>
107+
categorizeResource(item) === selectedCategory
108+
);
109+
}
110+
111+
return filtered;
112+
}, [searchTerm, selectedCategory]);
60113

61-
return (
62-
<div
63-
role="button"
64-
tabIndex={0}
65-
className={newCardClass}
66-
key={topic._id}
67-
onClick={() => navigate(`/${link}/${topic.slug?.current || topic._id}`)}
68-
onKeyDown={handleKeyPress}
69-
>
70-
<ContextObject item={topic} img={img} />
71-
</div>
72-
);
73-
})}
74-
</div>
75-
);
114+
const handleResourceClick = (item: any) => {
115+
if (item.url) {
116+
window.open(item.url, '_blank', 'noopener,noreferrer');
117+
}
76118
};
77119

78120
const steps = [
79121
{
80-
selector: ".first-step-library",
81-
content: `${I18n.get("libraryFirst")}`,
122+
selector: ".search-resources",
123+
content: "Search for resources by typing keywords",
124+
},
125+
{
126+
selector: ".filter-category",
127+
content: "Filter resources by category",
82128
},
83129
{
84-
selector: ".second-step-library",
85-
content: `${I18n.get("librarySecond")}`,
130+
selector: ".resource-card",
131+
content: "Click on any resource to open it in a new tab",
86132
},
87133
];
88134

89-
const tabPanelSxStyle = {
90-
margin: "-8.5vw",
91-
marginBottom:"6vh",
92-
}
93-
94135
return (
95136
<div>
96137
<h1>
@@ -103,59 +144,134 @@ function ContextBuilder(props: ContextBuilderProps) {
103144
sx={{
104145
opacity: 0.8,
105146
filter: "invert()",
106-
margin: '8px 8px 14px 8px'
147+
margin: '8px 8px 14px 8px',
148+
cursor: 'pointer'
107149
}}
150+
onClick={() => setIsTourOpen(true)}
108151
/>
109152
</h1>
110-
<AppBar
111-
position="static"
112-
sx={{
113-
boxShadow: "none",
114-
backgroundImage: "none",
115-
backgroundColor: "transparent",
116-
}}
117-
>
118-
<TabNavigation value={value} setValue={setValue} />
119-
</AppBar>
120-
<TabPanel value={value} index={0} sx={tabPanelSxStyle}>
121-
{props.sanitySchemas &&
122-
renderTopicsList(props.sanitySchemas.technicalSchemas)}
123-
</TabPanel>
124-
<TabPanel value={value} index={1} sx={tabPanelSxStyle}>
125-
{props.sanitySchemas &&
126-
renderTopicsList(props.sanitySchemas?.economicSchemas)}
127-
</TabPanel>
128-
<TabPanel value={value} index={2} sx={tabPanelSxStyle}>
129-
{props.sanitySchemas &&
130-
renderTopicsList(props.sanitySchemas?.hubSchemas)}
131-
</TabPanel>
153+
154+
{/* Search and Filter Controls */}
155+
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3, flexWrap: 'wrap' }}>
156+
<TextField
157+
className="search-resources"
158+
label="Search Resources"
159+
variant="outlined"
160+
value={searchTerm}
161+
onChange={(e) => setSearchTerm(e.target.value)}
162+
sx={{ minWidth: 300 }}
163+
InputProps={{
164+
startAdornment: (
165+
<InputAdornment position="start">
166+
🔍
167+
</InputAdornment>
168+
),
169+
}}
170+
/>
171+
172+
<FormControl className="filter-category" sx={{ minWidth: 200 }}>
173+
<InputLabel>Category</InputLabel>
174+
<Select
175+
value={selectedCategory}
176+
onChange={(e) => setSelectedCategory(e.target.value)}
177+
label="Category"
178+
>
179+
{categories.map(cat => (
180+
<MenuItem key={cat} value={cat}>{cat}</MenuItem>
181+
))}
182+
</Select>
183+
</FormControl>
184+
</Box>
185+
186+
{/* Results count */}
187+
<Box sx={{ mb: 2 }}>
188+
<Chip
189+
label={`${filteredItems.length} resources found`}
190+
color="primary"
191+
variant="outlined"
192+
/>
193+
{selectedCategory !== 'All' && (
194+
<Chip
195+
label={selectedCategory}
196+
onDelete={() => setSelectedCategory('All')}
197+
sx={{ ml: 1 }}
198+
/>
199+
)}
200+
</Box>
201+
202+
{/* Resource Grid */}
203+
<div className="context-cards" style={{ padding: '20px 0' }}>
204+
{filteredItems.map((item: any) => {
205+
const category = categorizeResource(item);
206+
return (
207+
<div
208+
key={item.id}
209+
className="context-card resource-card"
210+
style={{
211+
cursor: item.url ? 'pointer' : 'default',
212+
padding: '16px',
213+
border: '1px solid rgba(255,255,255,0.2)',
214+
borderRadius: '8px',
215+
marginBottom: '16px',
216+
transition: 'all 0.3s ease',
217+
position: 'relative',
218+
minHeight: '100px'
219+
}}
220+
onClick={() => handleResourceClick(item)}
221+
onMouseEnter={(e) => {
222+
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.5)';
223+
e.currentTarget.style.transform = 'translateY(-2px)';
224+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
225+
}}
226+
onMouseLeave={(e) => {
227+
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.2)';
228+
e.currentTarget.style.transform = 'translateY(0)';
229+
e.currentTarget.style.boxShadow = 'none';
230+
}}
231+
>
232+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
233+
<div style={{ flex: 1 }}>
234+
<h3 style={{ margin: '0 0 8px 0', fontSize: '18px' }}>{item.title}</h3>
235+
<Chip
236+
label={category}
237+
size="small"
238+
sx={{ mb: 1 }}
239+
variant="outlined"
240+
/>
241+
{item.summary && (
242+
<p style={{ fontSize: '14px', opacity: 0.8, marginTop: '8px' }}>{item.summary}</p>
243+
)}
244+
{item.url && (
245+
<p style={{ fontSize: '12px', opacity: 0.6, marginTop: '8px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
246+
{item.url}
247+
</p>
248+
)}
249+
</div>
250+
{item.url && (
251+
<span
252+
style={{
253+
fontSize: '20px',
254+
opacity: 0.6,
255+
marginLeft: '8px'
256+
}}
257+
>
258+
259+
</span>
260+
)}
261+
</div>
262+
</div>
263+
);
264+
})}
265+
</div>
132266

133267
<Tour
134268
steps={steps}
135269
isOpen={isTourOpen}
136270
onRequestClose={() => setIsTourOpen(false)}
137271
showCloseButton
138-
// rewindOnClose={false}
139272
/>
140273
</div>
141274
);
142275
}
143276

144-
function TabNavigation({ value, setValue }: { value: number, setValue: Dispatch<SetStateAction<number>> }) {
145-
return (
146-
<Tabs
147-
value={value}
148-
onChange={(_, newValue) => {
149-
localStorage.setItem("contextTab", newValue.toString());
150-
setValue(newValue);
151-
}}
152-
aria-label="Select the topics you wish to see in this group of tab"
153-
>
154-
<Tab label={I18n.get("fullStackDev")} sx={{ fontSize: 18 }} />
155-
<Tab label={I18n.get("findingWork")} sx={{ fontSize: 18 }} />
156-
<Tab label={I18n.get("cityByCity")} sx={{ fontSize: 18 }} />
157-
</Tabs>
158-
)
159-
}
160-
161277
export default ContextBuilder;

0 commit comments

Comments
 (0)