mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 14:27:20 +00:00
486 lines
16 KiB
TypeScript
486 lines
16 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useLocation } from "wouter";
|
|
import {
|
|
Store,
|
|
Search,
|
|
Download,
|
|
Star,
|
|
ArrowLeft,
|
|
Play,
|
|
Check,
|
|
Loader2,
|
|
Sparkles,
|
|
TrendingUp,
|
|
Clock,
|
|
Zap,
|
|
} from "lucide-react";
|
|
import { haptics } from "@/lib/haptics";
|
|
import { PullToRefresh } from "@/components/mobile/PullToRefresh";
|
|
|
|
interface AethexApp {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
category: string;
|
|
icon_url?: string;
|
|
install_count: number;
|
|
rating: number;
|
|
rating_count: number;
|
|
is_featured: boolean;
|
|
tags: string[];
|
|
owner_id: string;
|
|
source_code: string;
|
|
compiled_js: string;
|
|
}
|
|
|
|
interface Installation {
|
|
id: string;
|
|
app_id: string;
|
|
installed_at: string;
|
|
last_used_at?: string;
|
|
app: AethexApp;
|
|
}
|
|
|
|
export default function MobileAethexAppStore() {
|
|
const [, navigate] = useLocation();
|
|
const [apps, setApps] = useState<AethexApp[]>([]);
|
|
const [installedApps, setInstalledApps] = useState<Installation[]>([]);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
const [installing, setInstalling] = useState<string | null>(null);
|
|
const [running, setRunning] = useState<string | null>(null);
|
|
const [activeTab, setActiveTab] = useState<"browse" | "installed">("browse");
|
|
|
|
useEffect(() => {
|
|
fetchApps();
|
|
fetchInstalledApps();
|
|
}, []);
|
|
|
|
const fetchApps = async () => {
|
|
try {
|
|
const response = await fetch("/api/aethex/apps", {
|
|
credentials: "include",
|
|
});
|
|
const data = await response.json();
|
|
setApps(data.apps || []);
|
|
} catch (error) {
|
|
console.error("Failed to fetch apps:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchInstalledApps = async () => {
|
|
try {
|
|
const response = await fetch("/api/aethex/apps/installed/my", {
|
|
credentials: "include",
|
|
});
|
|
const data = await response.json();
|
|
setInstalledApps(data.installations || []);
|
|
} catch (error) {
|
|
console.error("Failed to fetch installed apps:", error);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = async () => {
|
|
haptics.light();
|
|
await Promise.all([fetchApps(), fetchInstalledApps()]);
|
|
haptics.success();
|
|
};
|
|
|
|
const handleInstall = async (appId: string, appName: string) => {
|
|
haptics.medium();
|
|
setInstalling(appId);
|
|
try {
|
|
const response = await fetch(`/api/aethex/apps/${appId}/install`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
haptics.success();
|
|
alert(`${appName} installed!`);
|
|
fetchInstalledApps();
|
|
} else {
|
|
haptics.error();
|
|
alert(data.error || "Failed to install");
|
|
}
|
|
} catch (error) {
|
|
console.error("Installation error:", error);
|
|
haptics.error();
|
|
alert("Failed to install");
|
|
} finally {
|
|
setInstalling(null);
|
|
}
|
|
};
|
|
|
|
const handleRun = async (appId: string, appName: string) => {
|
|
haptics.medium();
|
|
setRunning(appId);
|
|
try {
|
|
const response = await fetch(`/api/aethex/apps/${appId}/run`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.compiled_code) {
|
|
try {
|
|
const sandbox = {
|
|
console: {
|
|
log: (...args: any[]) => {
|
|
const output = args.map((a) => String(a)).join(" ");
|
|
alert(`${appName}: ${output}`);
|
|
},
|
|
},
|
|
alert: (msg: string) => alert(`${appName}: ${msg}`),
|
|
};
|
|
|
|
new Function("console", "alert", data.compiled_code)(sandbox.console, sandbox.alert);
|
|
haptics.success();
|
|
} catch (execError) {
|
|
console.error("App execution error:", execError);
|
|
alert(`Runtime error: ${execError}`);
|
|
haptics.error();
|
|
}
|
|
} else {
|
|
alert(data.error || "Failed to run app");
|
|
haptics.error();
|
|
}
|
|
} catch (error) {
|
|
console.error("Run error:", error);
|
|
alert("Failed to run app");
|
|
haptics.error();
|
|
} finally {
|
|
setRunning(null);
|
|
}
|
|
};
|
|
|
|
const isInstalled = (appId: string) => {
|
|
return installedApps.some((i) => i.app_id === appId);
|
|
};
|
|
|
|
const filteredApps = apps.filter(
|
|
(app) =>
|
|
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
app.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
const featuredApps = filteredApps.filter((app) => app.is_featured);
|
|
const regularApps = filteredApps.filter((app) => !app.is_featured);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-black text-white pb-20">
|
|
{/* Animated background */}
|
|
<div className="fixed inset-0 opacity-20 pointer-events-none">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/10 via-transparent to-cyan-600/10" />
|
|
<div className="absolute top-0 left-1/4 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl" />
|
|
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-cyan-500/5 rounded-full blur-3xl" />
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<div className="sticky top-0 z-50 bg-black/90 backdrop-blur-xl border-b border-cyan-500/20">
|
|
<div className="flex items-center justify-between px-4 py-4 safe-area-inset-top">
|
|
<button
|
|
onClick={() => {
|
|
haptics.light();
|
|
navigate("/");
|
|
}}
|
|
className="p-2 rounded-lg bg-cyan-500/10 border border-cyan-500/30 active:scale-95 transition-transform"
|
|
>
|
|
<ArrowLeft className="w-5 h-5 text-cyan-300" />
|
|
</button>
|
|
|
|
<div className="flex-1 text-center">
|
|
<div className="flex items-center justify-center gap-2">
|
|
<Store className="w-5 h-5 text-cyan-400" />
|
|
<h1 className="text-lg font-bold text-white uppercase tracking-wider">App Store</h1>
|
|
</div>
|
|
<p className="text-xs text-cyan-300 font-mono">{apps.length} apps available</p>
|
|
</div>
|
|
|
|
<div className="w-10" />
|
|
</div>
|
|
|
|
{/* Search Bar */}
|
|
<div className="px-4 pb-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-cyan-400" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Search apps..."
|
|
className="w-full bg-cyan-500/10 border border-cyan-500/30 rounded-lg pl-10 pr-4 py-3 text-white placeholder-cyan-300/50 focus:outline-none focus:border-cyan-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Navigation */}
|
|
<div className="flex border-t border-cyan-500/20">
|
|
<button
|
|
onClick={() => {
|
|
haptics.light();
|
|
setActiveTab("browse");
|
|
}}
|
|
className={`flex-1 py-3 text-sm font-semibold uppercase tracking-wider transition-colors ${
|
|
activeTab === "browse"
|
|
? "text-cyan-300 bg-cyan-500/10 border-b-2 border-cyan-400"
|
|
: "text-gray-400"
|
|
}`}
|
|
>
|
|
<Store className="w-4 h-4 mx-auto mb-1" />
|
|
Browse
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
haptics.light();
|
|
setActiveTab("installed");
|
|
}}
|
|
className={`flex-1 py-3 text-sm font-semibold uppercase tracking-wider transition-colors ${
|
|
activeTab === "installed"
|
|
? "text-cyan-300 bg-cyan-500/10 border-b-2 border-cyan-400"
|
|
: "text-gray-400"
|
|
}`}
|
|
>
|
|
<Download className="w-4 h-4 mx-auto mb-1" />
|
|
Installed ({installedApps.length})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<PullToRefresh onRefresh={handleRefresh}>
|
|
<div className="relative p-4 space-y-6">
|
|
{activeTab === "browse" && (
|
|
<>
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 text-cyan-400 animate-spin" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Featured Apps */}
|
|
{featuredApps.length > 0 && (
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Sparkles className="w-5 h-5 text-yellow-400" />
|
|
<h2 className="text-lg font-bold text-white uppercase">Featured</h2>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{featuredApps.map((app) => (
|
|
<AppCard
|
|
key={app.id}
|
|
app={app}
|
|
isInstalled={isInstalled(app.id)}
|
|
installing={installing === app.id}
|
|
running={running === app.id}
|
|
onInstall={handleInstall}
|
|
onRun={handleRun}
|
|
featured
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* All Apps */}
|
|
{regularApps.length > 0 && (
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<TrendingUp className="w-5 h-5 text-cyan-400" />
|
|
<h2 className="text-lg font-bold text-white uppercase">All Apps</h2>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{regularApps.map((app) => (
|
|
<AppCard
|
|
key={app.id}
|
|
app={app}
|
|
isInstalled={isInstalled(app.id)}
|
|
installing={installing === app.id}
|
|
running={running === app.id}
|
|
onInstall={handleInstall}
|
|
onRun={handleRun}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{filteredApps.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<Store className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
|
<p className="text-gray-400">No apps found</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{activeTab === "installed" && (
|
|
<>
|
|
{installedApps.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<Download className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
|
<p className="text-gray-400 mb-1">No installed apps</p>
|
|
<p className="text-sm text-gray-500">Browse apps to get started</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{installedApps.map((installation) => (
|
|
<AppCard
|
|
key={installation.id}
|
|
app={installation.app}
|
|
isInstalled={true}
|
|
installing={false}
|
|
running={running === installation.app_id}
|
|
onInstall={handleInstall}
|
|
onRun={handleRun}
|
|
showLastUsed
|
|
lastUsedAt={installation.last_used_at}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</PullToRefresh>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface AppCardProps {
|
|
app: AethexApp;
|
|
isInstalled: boolean;
|
|
installing: boolean;
|
|
running: boolean;
|
|
onInstall: (appId: string, appName: string) => void;
|
|
onRun: (appId: string, appName: string) => void;
|
|
featured?: boolean;
|
|
showLastUsed?: boolean;
|
|
lastUsedAt?: string;
|
|
}
|
|
|
|
function AppCard({
|
|
app,
|
|
isInstalled,
|
|
installing,
|
|
running,
|
|
onInstall,
|
|
onRun,
|
|
featured,
|
|
showLastUsed,
|
|
lastUsedAt,
|
|
}: AppCardProps) {
|
|
const getCategoryColor = (category: string) => {
|
|
const colors: Record<string, string> = {
|
|
game: "from-pink-500/20 to-purple-500/20 border-pink-500/30",
|
|
utility: "from-blue-500/20 to-cyan-500/20 border-blue-500/30",
|
|
social: "from-green-500/20 to-emerald-500/20 border-green-500/30",
|
|
education: "from-yellow-500/20 to-orange-500/20 border-yellow-500/30",
|
|
};
|
|
return colors[category] || "from-gray-500/20 to-slate-500/20 border-gray-500/30";
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`bg-gradient-to-br ${getCategoryColor(app.category)} border rounded-xl p-4 active:scale-98 transition-transform`}
|
|
>
|
|
<div className="flex items-start gap-3 mb-3">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-xl flex items-center justify-center flex-shrink-0 shadow-lg">
|
|
<Zap className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<h3 className="font-bold text-white text-lg truncate">{app.name}</h3>
|
|
{featured && <Sparkles className="w-4 h-4 text-yellow-400 flex-shrink-0" />}
|
|
</div>
|
|
<p className="text-xs text-gray-300 line-clamp-2 mt-1">{app.description}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="flex items-center gap-4 mb-3 text-xs">
|
|
<div className="flex items-center gap-1 text-yellow-400">
|
|
<Star className="w-3 h-3 fill-yellow-400" />
|
|
<span className="font-mono">{app.rating.toFixed(1)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 text-cyan-300">
|
|
<Download className="w-3 h-3" />
|
|
<span className="font-mono">{app.install_count}</span>
|
|
</div>
|
|
<div className="text-gray-400 uppercase font-mono">{app.category}</div>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
{app.tags && app.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-1 mb-3">
|
|
{app.tags.slice(0, 3).map((tag) => (
|
|
<span key={tag} className="px-2 py-1 text-[10px] font-mono bg-black/30 border border-cyan-500/20 rounded text-cyan-300 uppercase">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Last Used */}
|
|
{showLastUsed && lastUsedAt && (
|
|
<div className="flex items-center gap-1 text-xs text-gray-400 mb-3">
|
|
<Clock className="w-3 h-3" />
|
|
<span>
|
|
Last used: {new Date(lastUsedAt).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-2">
|
|
{isInstalled ? (
|
|
<>
|
|
<button
|
|
onClick={() => onRun(app.id, app.name)}
|
|
disabled={running}
|
|
className="flex-1 bg-gradient-to-r from-green-600 to-emerald-600 text-white font-bold py-3 rounded-lg active:scale-95 transition-transform disabled:opacity-50 flex items-center justify-center gap-2 text-sm"
|
|
>
|
|
{running ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Running...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play className="w-4 h-4" />
|
|
Run
|
|
</>
|
|
)}
|
|
</button>
|
|
<div className="px-4 flex items-center justify-center bg-green-500/10 border border-green-500/30 rounded-lg">
|
|
<Check className="w-4 h-4 text-green-400" />
|
|
</div>
|
|
</>
|
|
) : (
|
|
<button
|
|
onClick={() => onInstall(app.id, app.name)}
|
|
disabled={installing}
|
|
className="flex-1 bg-gradient-to-r from-cyan-600 to-blue-600 text-white font-bold py-3 rounded-lg active:scale-95 transition-transform disabled:opacity-50 flex items-center justify-center gap-2 text-sm"
|
|
>
|
|
{installing ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Installing...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Download className="w-4 h-4" />
|
|
Install
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|