app_store: widget change and more ui

This commit is contained in:
bitful-pannul 2024-07-29 14:02:13 +03:00
parent aa4c114a17
commit 7edcf31ddd
8 changed files with 144 additions and 193 deletions

View File

@ -144,7 +144,7 @@ fn make_widget() -> String {
if (app.metadata) { if (app.metadata) {
const a = document.createElement('a'); const a = document.createElement('a');
a.className = 'app'; a.className = 'app';
a.href = `/main:app_store:sys/apps/${app.package}:${app.publisher}` a.href = `/main:app_store:sys/app/${app.package}:${app.publisher}`
a.target = '_blank'; a.target = '_blank';
a.rel = 'noopener noreferrer'; a.rel = 'noopener noreferrer';
const iconLetter = app.metadata_hash.replace('0x', '')[0].toUpperCase(); const iconLetter = app.metadata_hash.replace('0x', '')[0].toUpperCase();

View File

@ -2,9 +2,8 @@ import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import StorePage from "./pages/StorePage"; import StorePage from "./pages/StorePage";
import MyAppsPage from "./pages/MyAppsPage";
import AppPage from "./pages/AppPage"; import AppPage from "./pages/AppPage";
import { APP_DETAILS_PATH, MY_APPS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path"; import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
import PublishPage from "./pages/PublishPage"; import PublishPage from "./pages/PublishPage";
import Header from "./components/Header"; import Header from "./components/Header";
@ -19,7 +18,6 @@ function App() {
<Header /> <Header />
<Routes> <Routes>
<Route path={STORE_PATH} element={<StorePage />} /> <Route path={STORE_PATH} element={<StorePage />} />
<Route path={MY_APPS_PATH} element={<MyAppsPage />} />
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} /> <Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
<Route path={PUBLISH_PATH} element={<PublishPage />} /> <Route path={PUBLISH_PATH} element={<PublishPage />} />
</Routes> </Routes>

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { STORE_PATH, MY_APPS_PATH, PUBLISH_PATH } from '../constants/path'; import { STORE_PATH, PUBLISH_PATH } from '../constants/path';
import { ConnectButton } from '@rainbow-me/rainbowkit'; import { ConnectButton } from '@rainbow-me/rainbowkit';
const Header: React.FC = () => { const Header: React.FC = () => {
@ -11,7 +11,6 @@ const Header: React.FC = () => {
<div className="header-left"> <div className="header-left">
<nav> <nav>
<Link to={STORE_PATH} className={location.pathname === STORE_PATH ? 'active' : ''}>Home</Link> <Link to={STORE_PATH} className={location.pathname === STORE_PATH ? 'active' : ''}>Home</Link>
<Link to={MY_APPS_PATH} className={location.pathname === MY_APPS_PATH ? 'active' : ''}>My Apps</Link>
<Link to={PUBLISH_PATH} className={location.pathname === PUBLISH_PATH ? 'active' : ''}>Publish</Link> <Link to={PUBLISH_PATH} className={location.pathname === PUBLISH_PATH ? 'active' : ''}>Publish</Link>
</nav> </nav>
</div> </div>

View File

@ -1,4 +1,3 @@
export const MY_APPS_PATH = '/my-apps';
export const STORE_PATH = '/'; export const STORE_PATH = '/';
export const PUBLISH_PATH = '/publish'; export const PUBLISH_PATH = '/publish';
export const APP_DETAILS_PATH = '/app'; export const APP_DETAILS_PATH = '/app';

View File

@ -106,85 +106,89 @@
color: light-dark(var(--gray), var(--off-white)); color: light-dark(var(--gray), var(--off-white));
} }
/* ... (previous styles) ... */
/* App Page Styles */ /* App Page Styles */
.app-page { .app-page {
padding: 2rem; max-width: 800px;
margin: 0 auto;
} }
.app-details-card { .app-description {
background-color: light-dark(var(--white), var(--off-black));
border-radius: 8px;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.app-divider {
border: none;
border-top: 1px solid var(--gray);
margin: 1rem 0;
}
.app-info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
color: light-dark(var(--gray), var(--off-white));
} }
.app-info-item { .app-details {
text-align: center; display: flex;
justify-content: space-between;
gap: 2rem;
} }
.app-info-top { .app-info {
font-weight: bold; flex: 1;
margin-bottom: 0.5rem;
} }
.app-info-middle { .app-details-list {
font-size: 1.5rem; list-style-type: none;
margin-bottom: 0.5rem; padding: 0;
} }
.app-info-bottom { .app-details-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid light-dark(var(--gray), var(--maroon));
}
.app-details-list li:last-child {
border-bottom: none;
}
.status-icon {
font-size: 1rem;
}
.status-icon.installed,
.status-icon.mirroring,
.status-icon.auto-update {
color: var(--orange);
}
.status-icon.not-installed,
.status-icon.not-mirroring,
.status-icon.no-auto-update {
color: var(--ansi-red);
}
.app-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.app-actions button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--gray);
} }
.app-screenshots { .app-screenshots {
display: flex; display: flex;
overflow-x: auto; overflow-x: auto;
gap: 1rem; gap: 1rem;
margin-bottom: 1rem; padding: 1rem 0;
margin-top: 1rem;
} }
.app-screenshot { .app-screenshot {
max-width: 200px; max-width: 200px;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.app-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.app-actions button,
.app-actions .button {
flex: 1;
min-width: 120px;
}
.version-number {
font-family: monospace;
background-color: light-dark(var(--tan), var(--maroon));
padding: 0.2rem 0.5rem;
border-radius: 4px;
}
/* ... (previous styles) ... */
/* My Apps Page Styles */ /* My Apps Page Styles */
.my-apps-page { .my-apps-page {
padding: 2rem; padding: 2rem;
@ -258,8 +262,6 @@
color: light-dark(var(--gray), var(--off-white)); color: light-dark(var(--gray), var(--off-white));
} }
/* ... (previous styles) ... */
/* Publish Page Styles */ /* Publish Page Styles */
.publish-page { .publish-page {
padding: 2rem; padding: 2rem;
@ -361,8 +363,6 @@
background-color: #c62828; background-color: #c62828;
} }
/* ... (previous styles) ... */
/* Store Page Styles */ /* Store Page Styles */
.store-page { .store-page {
padding: 2rem; padding: 2rem;
@ -371,13 +371,15 @@
.store-header { .store-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: stretch;
margin-bottom: 2rem; margin-bottom: 2rem;
gap: 1rem;
} }
.search-bar { .search-bar {
flex-grow: 1; flex-grow: 1;
margin-right: 1rem; display: flex;
align-items: stretch;
} }
.search-bar input { .search-bar input {
@ -386,6 +388,27 @@
font-size: 1rem; font-size: 1rem;
border: 1px solid var(--gray); border: 1px solid var(--gray);
border-radius: 4px; border-radius: 4px;
height: 38px;
}
.filter-button,
.store-header button {
height: 38px;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
align-self: stretch;
}
/* Add these new styles */
.store-header>* {
margin: 0;
}
.store-header button {
flex-shrink: 0;
} }
.app-list table { .app-list table {
@ -430,7 +453,7 @@
} }
.status.installed { .status.installed {
background-color: var(--green); background-color: var(--off-black);
color: var(--white); color: var(--white);
} }

View File

@ -1,148 +1,72 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { FaGlobe, FaPeopleGroup, FaCode, FaLink, FaCaretDown } from "react-icons/fa6"; import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes } from "react-icons/fa";
import { AppInfo } from "../types/Apps";
import useAppsStore from "../store"; import useAppsStore from "../store";
import { PUBLISH_PATH } from "../constants/path"; import { appId } from "../utils/app";
import { FaCheckCircle, FaTimesCircle } from "react-icons/fa";
export default function AppPage() { export default function AppPage() {
const { getApp, installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate } = useAppsStore(); const { installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate, apps } = useAppsStore();
const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const [app, setApp] = useState<AppInfo | undefined>(undefined); const app = apps.find(a => appId(a) === id);
useEffect(() => { if (!app) {
if (id) { return <div className="app-page"><h4>App details not found for {id}</h4></div>;
getApp(id).then(setApp).catch(console.error); }
}
}, [id, getApp]);
const handleInstall = () => app && installApp(app); const handleInstall = () => app && installApp(app);
const handleUpdate = () => app && updateApp(app); const handleUpdate = () => app && updateApp(app);
const handleUninstall = () => app && uninstallApp(app); const handleUninstall = () => app && uninstallApp(app);
const handleMirror = () => app && setMirroring(app, !app.state?.mirroring); const handleMirror = () => app && setMirroring(app, !app.state?.mirroring);
const handleAutoUpdate = () => app && setAutoUpdate(app, !app.state?.auto_update); const handleAutoUpdate = () => app && setAutoUpdate(app, !app.state?.auto_update);
const handlePublish = () => navigate(PUBLISH_PATH, { state: { app } });
if (!app) {
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
}
const version = app.metadata?.properties?.current_version || "Unknown";
const versions = Object.entries(app.metadata?.properties?.code_hashes || {});
const hash = app.state?.our_version || (versions[(versions.length || 1) - 1] || ["", ""])[1];
const mirrors = app.metadata?.properties?.mirrors || [];
const tbaLink = `https://optimistic.etherscan.io/address/${app.tba}`;
return ( return (
<div className="app-page"> <section className="app-page">
<div className="app-details-card"> <h2>{app.metadata?.name || app.package}</h2>
<h2>{app.metadata?.name || app.package}</h2> <p className="app-description">{app.metadata?.description || "No description available"}</p>
<p>{app.metadata?.description || "No description available"}</p> <div className="app-details">
<hr className="app-divider" /> <div className="app-info">
<div className="app-info-grid"> <ul className="app-details-list">
<div className="app-info-item"> <li><span>Version:</span> <span>{app.metadata?.properties?.current_version || "Unknown"}</span></li>
<FaPeopleGroup size={36} /> <li><span>Developer:</span> <span>{app.publisher}</span></li>
<span>Developer</span> <li><span>Mirrors:</span> <span>{app.metadata?.properties?.mirrors?.length || 0}</span></li>
<span>{app.publisher}</span> <li>
</div> <span>Installed:</span>
<div className="app-info-item"> {app.installed ? <FaCheck className="status-icon installed" /> : <FaTimes className="status-icon not-installed" />}
<FaCode size={36} /> </li>
<span>Version</span> <li>
<span>{version}</span> <span>Mirroring:</span>
<span className="hash">{`${hash.slice(0, 5)}...${hash.slice(-5)}`}</span> {app.state?.mirroring ? <FaCheck className="status-icon mirroring" /> : <FaTimes className="status-icon not-mirroring" />}
</div> </li>
<div className="app-info-item"> <li>
<FaGlobe size={36} /> <span>Auto-Update:</span>
<span>Mirrors</span> {app.state?.auto_update ? <FaCheck className="status-icon auto-update" /> : <FaTimes className="status-icon no-auto-update" />}
<span>{mirrors.length}</span> </li>
<MirrorsDropdown mirrors={mirrors} /> </ul>
</div>
<div className="app-info-item">
<FaLink size={36} />
<span>TBA</span>
<a href={tbaLink} target="_blank" rel="noopener noreferrer">
{`${app.package}.${app.publisher}`}
</a>
</div>
<div className="app-info-item">
<span>Installed</span>
{app.installed ? <FaCheckCircle color="green" /> : <FaTimesCircle color="red" />}
</div>
{app.state && (
<>
<div className="app-info-item">
<span>Mirrored From</span>
<span>{app.state.mirrored_from || "N/A"}</span>
</div>
<div className="app-info-item">
<span>Capabilities Approved</span>
{app.state.caps_approved ? <FaCheckCircle color="green" /> : <FaTimesCircle color="red" />}
</div>
<div className="app-info-item">
<span>Mirroring</span>
{app.state.mirroring ? <FaCheckCircle color="green" /> : <FaTimesCircle color="red" />}
</div>
<div className="app-info-item">
<span>Auto Update</span>
{app.state.auto_update ? <FaCheckCircle color="green" /> : <FaTimesCircle color="red" />}
</div>
<div className="app-info-item">
<span>Verified</span>
{app.state.verified ? <FaCheckCircle color="green" /> : <FaTimesCircle color="red" />}
</div>
</>
)}
</div> </div>
{app.metadata?.properties?.screenshots && (
<div className="app-screenshots">
{app.metadata.properties.screenshots.map((screenshot, index) => (
<img key={index} src={screenshot} alt={`Screenshot ${index + 1}`} className="app-screenshot" />
))}
</div>
)}
<div className="app-actions"> <div className="app-actions">
{app.installed ? ( {app.installed ? (
<> <>
<button onClick={handleUpdate}>Update</button> <button onClick={handleUpdate} className="secondary"><FaSync /> Update</button>
<button onClick={handleUninstall}>Uninstall</button> <button onClick={handleUninstall} className="secondary"><FaTrash /> Uninstall</button>
<button onClick={handleMirror}>
{app.state?.mirroring ? "Stop Mirroring" : "Start Mirroring"}
</button>
<button onClick={handleAutoUpdate}>
{app.state?.auto_update ? "Disable Auto-Update" : "Enable Auto-Update"}
</button>
{app.state?.mirroring && (
<button onClick={handlePublish}>Publish</button>
)}
</> </>
) : ( ) : (
<button onClick={handleInstall}>Install</button> <button onClick={handleInstall}><FaDownload /> Install</button>
)} )}
<button onClick={handleMirror} className="secondary">
<FaMagnet /> {app.state?.mirroring ? "Stop Mirroring" : "Start Mirroring"}
</button>
<button onClick={handleAutoUpdate} className="secondary">
<FaCog /> {app.state?.auto_update ? "Disable Auto-Update" : "Enable Auto-Update"}
</button>
</div> </div>
</div> </div>
</div> {app.metadata?.properties?.screenshots && (
); <div className="app-screenshots">
} {app.metadata.properties.screenshots.map((screenshot, index) => (
<img key={index} src={screenshot} alt={`Screenshot ${index + 1}`} className="app-screenshot" />
const MirrorsDropdown = ({ mirrors }: { mirrors: string[] }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="mirrors-dropdown">
<button onClick={() => setIsOpen(!isOpen)} className="mirrors-dropdown-toggle">
Mirrors <FaCaretDown />
</button>
{isOpen && (
<ul className="mirrors-list">
{mirrors.map((mirror, index) => (
<li key={index}>{mirror}</li>
))} ))}
</ul> </div>
)} )}
</div> </section>
); );
}; }

View File

@ -39,7 +39,7 @@ export default function PublishPage() {
useEffect(() => { useEffect(() => {
setMyPublishedApps( setMyPublishedApps(
apps.filter((app) => app.owner?.toLowerCase() === address?.toLowerCase()) apps.filter((app) => app.publisher?.toLowerCase() === window.our.node?.toLowerCase())
); );
}, [apps, address]) }, [apps, address])

View File

@ -3,20 +3,22 @@ import useAppsStore from "../store";
import { AppInfo } from "../types/Apps"; import { AppInfo } from "../types/Apps";
import { appId } from '../utils/app' import { appId } from '../utils/app'
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { FaGlobe, FaPeopleGroup, FaCode } from "react-icons/fa6"; import { FaGlobe, FaPeopleGroup, FaCode, FaFilter } from "react-icons/fa6";
export default function StorePage() { export default function StorePage() {
const { apps, getApps, rebuildIndex } = useAppsStore(); const { apps, getApps, rebuildIndex } = useAppsStore();
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
const [isRebuildingIndex, setIsRebuildingIndex] = useState(false); const [isRebuildingIndex, setIsRebuildingIndex] = useState(false);
const [showMyApps, setShowMyApps] = useState(false);
useEffect(() => { useEffect(() => {
getApps(); getApps();
}, [getApps]); }, [getApps]);
const filteredApps = apps.filter((app) => const filteredApps = apps.filter((app) =>
app.package.toLowerCase().includes(searchQuery.toLowerCase()) || (app.package.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase()) app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase())) &&
(!showMyApps || app.installed)
); );
const handleRebuildIndex = async () => { const handleRebuildIndex = async () => {
@ -44,6 +46,12 @@ export default function StorePage() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
<button
className={`filter-button ${showMyApps ? 'active' : ''}`}
onClick={() => setShowMyApps(!showMyApps)}
>
<FaFilter /> {showMyApps ? 'All Apps' : 'My Apps'}
</button>
<button onClick={handleRebuildIndex} disabled={isRebuildingIndex}> <button onClick={handleRebuildIndex} disabled={isRebuildingIndex}>
{isRebuildingIndex ? "Rebuilding..." : "Rebuild Index"} {isRebuildingIndex ? "Rebuilding..." : "Rebuild Index"}
</button> </button>