Skip to content

Commit 9f37989

Browse files
authored
Merge pull request #69 from dailker/main
feat(app-store): enhance UI with search, pagination, and installed fi…
2 parents 6be6838 + c708a6f commit 9f37989

File tree

23 files changed

+1384
-67
lines changed

23 files changed

+1384
-67
lines changed

orbit.schema.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "OrbitOS App Manifest",
4+
"type": "object",
5+
"required": ["id", "name", "version", "permissions", "main"],
6+
"properties": {
7+
"id": {
8+
"type": "string",
9+
"pattern": "^[a-z0-9-]+$",
10+
"description": "Unique app identifier (lowercase, numbers, hyphens only)"
11+
},
12+
"name": {
13+
"type": "string",
14+
"maxLength": 50,
15+
"description": "Display name of the app"
16+
},
17+
"version": {
18+
"type": "string",
19+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
20+
"description": "Semantic version (e.g., 1.0.0)"
21+
},
22+
"description": {
23+
"type": "string",
24+
"maxLength": 200,
25+
"description": "Brief description of the app"
26+
},
27+
"author": {
28+
"type": "string",
29+
"maxLength": 100,
30+
"description": "App author name"
31+
},
32+
"icon": {
33+
"type": "string",
34+
"description": "App icon (emoji or base64 data URL)"
35+
},
36+
"main": {
37+
"type": "string",
38+
"description": "Entry point file (e.g., App.jsx)"
39+
},
40+
"permissions": {
41+
"type": "array",
42+
"items": {
43+
"type": "string",
44+
"enum": ["ui", "storage", "network", "files"]
45+
},
46+
"description": "Required permissions"
47+
},
48+
"size": {
49+
"type": "integer",
50+
"maximum": 1048576,
51+
"description": "App size in bytes (max 1MB)"
52+
},
53+
"category": {
54+
"type": "string",
55+
"enum": [
56+
"productivity",
57+
"utility",
58+
"entertainment",
59+
"development",
60+
"system"
61+
],
62+
"description": "App category"
63+
}
64+
}
65+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "A Web OS-style project for group collaboration",
55
"scripts": {
66
"dev": "next dev",
7+
"dev:test-unapproved": "ORBIT_DEV_MODE=true next dev",
78
"build": "next build",
89
"start": "next start",
910
"dev:all": "npm test && npm run dev",
@@ -26,12 +27,13 @@
2627
"helmet": "^8.1.0",
2728
"idb": "^8.0.3",
2829
"jsonwebtoken": "^9.0.2",
30+
"lucide-react": "^0.545.0",
2931
"mongoose": "^8.18.2",
3032
"multer": "2.0.2",
3133
"next": "^14.0.0",
3234
"react": "^18.0.0",
3335
"react-dom": "^18.0.0",
34-
"react-quill": "^2.0.0"
36+
"react-quill": "^0.0.2"
3537
},
3638
"devDependencies": {
3739
"@commitlint/cli": "^19.8.1",

src/components/Desktop.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// src/components/Desktop.js
22

3-
import { useState } from 'react';
3+
import React, { useState, useEffect } from 'react';
44
import { useApp } from '@/context/AppContext';
55
import { useTheme } from '@/context/ThemeContext';
66
import { useSettings } from '@/context/SettingsContext';
@@ -24,6 +24,7 @@ import SettingsApp from '@/pages/apps/settings';
2424
import MonitorApp from '@/pages/apps/monitor';
2525
import FilemanagerApp from '@/pages/apps/filemanager';
2626
import Calculator from '@/pages/apps/calculator';
27+
import AppStoreApp from '@/pages/apps/appstore';
2728
import TabManager from './TabManager';
2829

2930
const appComponents = {
@@ -33,9 +34,68 @@ const appComponents = {
3334
monitor: MonitorApp,
3435
filemanager: FilemanagerApp,
3536
calculator: Calculator,
37+
appstore: AppStoreApp,
3638
'tab-manager': TabManager,
3739
};
3840

41+
// Dynamic app component for installed apps
42+
const DynamicApp = ({ appId }) => {
43+
const [AppComponent, setAppComponent] = useState(null);
44+
const [loading, setLoading] = useState(true);
45+
const [error, setError] = useState(null);
46+
47+
useEffect(() => {
48+
const loadApp = async () => {
49+
try {
50+
// Get app code from GitHub
51+
const response = await fetch(
52+
`https://raw.githubusercontent.com/codehubbers/OrbitOSPackages/main/${appId}/App.jsx`,
53+
);
54+
const appCode = await response.text();
55+
56+
// Create sandboxed component
57+
const componentFactory = new Function(
58+
'React',
59+
`
60+
${appCode}
61+
return App;
62+
`,
63+
);
64+
65+
const Component = componentFactory(React);
66+
setAppComponent(() => Component);
67+
} catch (err) {
68+
setError(err.message);
69+
} finally {
70+
setLoading(false);
71+
}
72+
};
73+
74+
loadApp();
75+
}, [appId]);
76+
77+
if (loading)
78+
return React.createElement(
79+
'div',
80+
{ style: { padding: '20px' } },
81+
'Loading app...',
82+
);
83+
if (error)
84+
return React.createElement(
85+
'div',
86+
{ style: { padding: '20px', color: 'red' } },
87+
`Error: ${error}`,
88+
);
89+
if (!AppComponent)
90+
return React.createElement(
91+
'div',
92+
{ style: { padding: '20px' } },
93+
'App not found',
94+
);
95+
96+
return React.createElement(AppComponent);
97+
};
98+
3999
export default function Desktop() {
40100
const { state } = useApp();
41101
const { theme } = useTheme();
@@ -135,6 +195,15 @@ export default function Desktop() {
135195
const services = createAppServices(app);
136196
const AppComponent = appComponents[app.component];
137197

198+
// Handle dynamic installed apps
199+
if (!AppComponent && app.component) {
200+
return (
201+
<Window key={app.id} app={app}>
202+
<DynamicApp appId={app.component} />
203+
</Window>
204+
);
205+
}
206+
138207
if (!AppComponent)
139208
return (
140209
<div key={app.id}>App component not found for: {app.component}</div>

src/components/Taskbar.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,17 @@ const StartButtonAndMenu = ({ apps, onAppClick, theme }) => {
116116
const OpenAppsTray = ({ openApps, activeApp, onAppClick, theme, dispatch }) => (
117117
<div className="flex items-center space-x-1">
118118
{openApps.map((app) => {
119-
const isActive = activeApp === app.id && !app.isMinimized; // ✅ Use isMinimized
120-
const isMinimized = app.isMinimized; // ✅ Use isMinimized
119+
const isActive = activeApp === app.id && !app.isMinimized;
120+
const isMinimized = app.isMinimized;
121121
return (
122122
<motion.button
123123
key={app.id}
124124
onClick={() => {
125125
if (app.isMinimized) {
126-
// ✅ Add restore logic
127126
dispatch({ type: 'RESTORE_APP', payload: { appId: app.id } });
127+
} else {
128+
onAppClick(app.id);
128129
}
129-
onAppClick(app.id);
130130
}}
131131
className={`w-12 h-12 flex items-center justify-center ${theme.glass} rounded-xl shadow-lg hover:bg-white/20 transition-all ${isMinimized ? 'opacity-60' : ''}`}
132132
whileHover={{ scale: 1.2, y: -5 }}
@@ -139,7 +139,7 @@ const OpenAppsTray = ({ openApps, activeApp, onAppClick, theme, dispatch }) => (
139139
isActive
140140
? 'bg-white'
141141
: isMinimized
142-
? 'bg-yellow-400' // ✅ Use yellow for minimize indicator
142+
? 'bg-yellow-400'
143143
: 'bg-gray-400'
144144
}`}
145145
/>
@@ -438,7 +438,7 @@ export default function Taskbar({ onAvatarEdit }) {
438438
activeApp={state.activeApp}
439439
onAppClick={toggleApp}
440440
theme={theme}
441-
dispatch={dispatch} // ✅ Add dispatch prop for restore functionality
441+
dispatch={dispatch}
442442
/>
443443
</div>
444444
<div className="flex items-center space-x-4">

src/components/Window.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export default function Window({ app, children }) {
2929
x: 100 + Math.random() * 200,
3030
y: 100 + Math.random() * 100,
3131
};
32-
const initialSize = { width: 600, height: 400 };
32+
const initialSize = {
33+
width: 600,
34+
height: app.id === 'settings' ? 350 : 400,
35+
};
3336

3437
const {
3538
size,
@@ -178,12 +181,11 @@ export default function Window({ app, children }) {
178181
/>
179182

180183
{/* Window Content */}
181-
<div className="flex-1 overflow-hidden">
182-
<div
183-
className={`p-4 h-full overflow-auto ${theme.window.content}`}
184-
>
185-
{children}
186-
</div>
184+
<div
185+
className={`flex-1 ${theme.window.content} ${theme.window.scrollbar}`}
186+
style={{ height: 'calc(100% - 40px)', overflow: 'hidden' }}
187+
>
188+
{children}
187189
</div>
188190

189191
{/* Resize Handles */}

src/context/AppContext.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,16 @@ function appReducer(state, action) {
7474
state.activeApp === action.payload.appId ? null : state.activeApp,
7575
};
7676

77-
case 'RESTORE_APP': // ✅ Add missing action
77+
case 'RESTORE_APP':
7878
return {
7979
...state,
8080
openApps: state.openApps.map((app) =>
8181
app.id === action.payload.appId
82-
? { ...app, isMinimized: false }
82+
? { ...app, isMinimized: false, zIndex: state.nextZIndex }
8383
: app,
8484
),
8585
activeApp: action.payload.appId,
86+
nextZIndex: state.nextZIndex + 1,
8687
};
8788

8889
case 'TOGGLE_APP':
@@ -263,6 +264,9 @@ function appReducer(state, action) {
263264
...state,
264265
theme: action.payload,
265266
};
267+
case 'INSTALL_APP':
268+
// Add installed app to registry (runtime installation)
269+
return state;
266270
default:
267271
return state;
268272
}

src/models/User.js

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,61 +8,71 @@ const userSchema = new mongoose.Schema({
88
unique: true,
99
trim: true,
1010
minlength: 3,
11-
maxlength: 30
11+
maxlength: 30,
1212
},
1313
email: {
1414
type: String,
1515
required: true,
1616
unique: true,
1717
lowercase: true,
18-
trim: true
18+
trim: true,
1919
},
2020
passwordHash: {
2121
type: String,
2222
required: true,
23-
minlength: 6
23+
minlength: 6,
2424
},
2525
displayName: {
2626
type: String,
2727
trim: true,
28-
maxlength: 50
28+
maxlength: 50,
2929
},
3030
avatar: {
3131
type: String,
32-
default: null
32+
default: null,
3333
},
34-
roles: [{
35-
type: String,
36-
enum: ['user', 'admin'],
37-
default: 'user'
38-
}],
34+
roles: [
35+
{
36+
type: String,
37+
enum: ['user', 'admin'],
38+
default: 'user',
39+
},
40+
],
3941
preferences: {
4042
theme: { type: String, default: 'light' },
4143
wallpaper: { type: String, default: '/backgrounds/orbitos-default.jpg' },
42-
notifications: { type: Boolean, default: true }
44+
notifications: { type: Boolean, default: true },
4345
},
46+
installedApps: [
47+
{
48+
appId: { type: String, required: true },
49+
manifest: { type: Object, required: true },
50+
downloadUrl: { type: String, required: true },
51+
installedAt: { type: Date, default: Date.now },
52+
},
53+
],
4454
createdAt: {
4555
type: Date,
46-
default: Date.now
56+
default: Date.now,
4757
},
4858
lastLogin: {
4959
type: Date,
50-
default: Date.now
60+
default: Date.now,
5161
},
5262
isActive: {
5363
type: Boolean,
54-
default: true
55-
}
64+
default: true,
65+
},
5666
});
5767

58-
userSchema.pre('save', async function(next) {
68+
userSchema.pre('save', async function (next) {
5969
if (!this.isModified('passwordHash')) return next();
6070
this.passwordHash = await bcrypt.hash(this.passwordHash, 12);
6171
next();
6272
});
6373

64-
userSchema.methods.comparePassword = async function(password) {
74+
userSchema.methods.comparePassword = async function (password) {
6575
return bcrypt.compare(password, this.passwordHash);
6676
};
6777

68-
export default mongoose.models.User || mongoose.model('User', userSchema);
78+
export default mongoose.models.User || mongoose.model('User', userSchema);

0 commit comments

Comments
 (0)