mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-11-23 03:44:04 +03:00
app_store: widget change and more ui
This commit is contained in:
parent
aa4c114a17
commit
7edcf31ddd
@ -144,7 +144,7 @@ fn make_widget() -> String {
|
||||
if (app.metadata) {
|
||||
const a = document.createElement('a');
|
||||
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.rel = 'noopener noreferrer';
|
||||
const iconLetter = app.metadata_hash.replace('0x', '')[0].toUpperCase();
|
||||
|
@ -2,9 +2,8 @@ import React from "react";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
|
||||
import StorePage from "./pages/StorePage";
|
||||
import MyAppsPage from "./pages/MyAppsPage";
|
||||
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 Header from "./components/Header";
|
||||
|
||||
@ -19,7 +18,6 @@ function App() {
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path={STORE_PATH} element={<StorePage />} />
|
||||
<Route path={MY_APPS_PATH} element={<MyAppsPage />} />
|
||||
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
|
||||
<Route path={PUBLISH_PATH} element={<PublishPage />} />
|
||||
</Routes>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
@ -11,7 +11,6 @@ const Header: React.FC = () => {
|
||||
<div className="header-left">
|
||||
<nav>
|
||||
<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>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
export const MY_APPS_PATH = '/my-apps';
|
||||
export const STORE_PATH = '/';
|
||||
export const PUBLISH_PATH = '/publish';
|
||||
export const APP_DETAILS_PATH = '/app';
|
||||
|
@ -106,85 +106,89 @@
|
||||
color: light-dark(var(--gray), var(--off-white));
|
||||
}
|
||||
|
||||
/* ... (previous styles) ... */
|
||||
|
||||
/* App Page Styles */
|
||||
.app-page {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.app-details-card {
|
||||
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;
|
||||
.app-description {
|
||||
margin-bottom: 1rem;
|
||||
color: light-dark(var(--gray), var(--off-white));
|
||||
}
|
||||
|
||||
.app-info-item {
|
||||
text-align: center;
|
||||
.app-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.app-info-top {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
.app-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-info-middle {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
.app-details-list {
|
||||
list-style-type: none;
|
||||
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;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.app-screenshots {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.app-screenshot {
|
||||
max-width: 200px;
|
||||
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 {
|
||||
padding: 2rem;
|
||||
@ -258,8 +262,6 @@
|
||||
color: light-dark(var(--gray), var(--off-white));
|
||||
}
|
||||
|
||||
/* ... (previous styles) ... */
|
||||
|
||||
/* Publish Page Styles */
|
||||
.publish-page {
|
||||
padding: 2rem;
|
||||
@ -361,8 +363,6 @@
|
||||
background-color: #c62828;
|
||||
}
|
||||
|
||||
/* ... (previous styles) ... */
|
||||
|
||||
/* Store Page Styles */
|
||||
.store-page {
|
||||
padding: 2rem;
|
||||
@ -371,13 +371,15 @@
|
||||
.store-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
margin-bottom: 2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-grow: 1;
|
||||
margin-right: 1rem;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
@ -386,6 +388,27 @@
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--gray);
|
||||
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 {
|
||||
@ -430,7 +453,7 @@
|
||||
}
|
||||
|
||||
.status.installed {
|
||||
background-color: var(--green);
|
||||
background-color: var(--off-black);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
|
@ -1,148 +1,72 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { FaGlobe, FaPeopleGroup, FaCode, FaLink, FaCaretDown } from "react-icons/fa6";
|
||||
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import React from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { PUBLISH_PATH } from "../constants/path";
|
||||
import { FaCheckCircle, FaTimesCircle } from "react-icons/fa";
|
||||
import { appId } from "../utils/app";
|
||||
|
||||
export default function AppPage() {
|
||||
const { getApp, installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate } = useAppsStore();
|
||||
const navigate = useNavigate();
|
||||
const { installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate, apps } = useAppsStore();
|
||||
const { id } = useParams();
|
||||
const [app, setApp] = useState<AppInfo | undefined>(undefined);
|
||||
const app = apps.find(a => appId(a) === id);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getApp(id).then(setApp).catch(console.error);
|
||||
}
|
||||
}, [id, getApp]);
|
||||
if (!app) {
|
||||
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
|
||||
}
|
||||
|
||||
const handleInstall = () => app && installApp(app);
|
||||
const handleUpdate = () => app && updateApp(app);
|
||||
const handleUninstall = () => app && uninstallApp(app);
|
||||
const handleMirror = () => app && setMirroring(app, !app.state?.mirroring);
|
||||
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 (
|
||||
<div className="app-page">
|
||||
<div className="app-details-card">
|
||||
<h2>{app.metadata?.name || app.package}</h2>
|
||||
<p>{app.metadata?.description || "No description available"}</p>
|
||||
<hr className="app-divider" />
|
||||
<div className="app-info-grid">
|
||||
<div className="app-info-item">
|
||||
<FaPeopleGroup size={36} />
|
||||
<span>Developer</span>
|
||||
<span>{app.publisher}</span>
|
||||
</div>
|
||||
<div className="app-info-item">
|
||||
<FaCode size={36} />
|
||||
<span>Version</span>
|
||||
<span>{version}</span>
|
||||
<span className="hash">{`${hash.slice(0, 5)}...${hash.slice(-5)}`}</span>
|
||||
</div>
|
||||
<div className="app-info-item">
|
||||
<FaGlobe size={36} />
|
||||
<span>Mirrors</span>
|
||||
<span>{mirrors.length}</span>
|
||||
<MirrorsDropdown mirrors={mirrors} />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
<section className="app-page">
|
||||
<h2>{app.metadata?.name || app.package}</h2>
|
||||
<p className="app-description">{app.metadata?.description || "No description available"}</p>
|
||||
<div className="app-details">
|
||||
<div className="app-info">
|
||||
<ul className="app-details-list">
|
||||
<li><span>Version:</span> <span>{app.metadata?.properties?.current_version || "Unknown"}</span></li>
|
||||
<li><span>Developer:</span> <span>{app.publisher}</span></li>
|
||||
<li><span>Mirrors:</span> <span>{app.metadata?.properties?.mirrors?.length || 0}</span></li>
|
||||
<li>
|
||||
<span>Installed:</span>
|
||||
{app.installed ? <FaCheck className="status-icon installed" /> : <FaTimes className="status-icon not-installed" />}
|
||||
</li>
|
||||
<li>
|
||||
<span>Mirroring:</span>
|
||||
{app.state?.mirroring ? <FaCheck className="status-icon mirroring" /> : <FaTimes className="status-icon not-mirroring" />}
|
||||
</li>
|
||||
<li>
|
||||
<span>Auto-Update:</span>
|
||||
{app.state?.auto_update ? <FaCheck className="status-icon auto-update" /> : <FaTimes className="status-icon no-auto-update" />}
|
||||
</li>
|
||||
</ul>
|
||||
</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">
|
||||
{app.installed ? (
|
||||
<>
|
||||
<button onClick={handleUpdate}>Update</button>
|
||||
<button onClick={handleUninstall}>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={handleUpdate} className="secondary"><FaSync /> Update</button>
|
||||
<button onClick={handleUninstall} className="secondary"><FaTrash /> Uninstall</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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
{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" />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
}
|
@ -39,7 +39,7 @@ export default function PublishPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setMyPublishedApps(
|
||||
apps.filter((app) => app.owner?.toLowerCase() === address?.toLowerCase())
|
||||
apps.filter((app) => app.publisher?.toLowerCase() === window.our.node?.toLowerCase())
|
||||
);
|
||||
}, [apps, address])
|
||||
|
||||
|
@ -3,20 +3,22 @@ import useAppsStore from "../store";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from '../utils/app'
|
||||
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() {
|
||||
const { apps, getApps, rebuildIndex } = useAppsStore();
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [isRebuildingIndex, setIsRebuildingIndex] = useState(false);
|
||||
const [showMyApps, setShowMyApps] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getApps();
|
||||
}, [getApps]);
|
||||
|
||||
const filteredApps = apps.filter((app) =>
|
||||
app.package.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
(app.package.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase())) &&
|
||||
(!showMyApps || app.installed)
|
||||
);
|
||||
|
||||
const handleRebuildIndex = async () => {
|
||||
@ -44,6 +46,12 @@ export default function StorePage() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={`filter-button ${showMyApps ? 'active' : ''}`}
|
||||
onClick={() => setShowMyApps(!showMyApps)}
|
||||
>
|
||||
<FaFilter /> {showMyApps ? 'All Apps' : 'My Apps'}
|
||||
</button>
|
||||
<button onClick={handleRebuildIndex} disabled={isRebuildingIndex}>
|
||||
{isRebuildingIndex ? "Rebuilding..." : "Rebuild Index"}
|
||||
</button>
|
||||
|
Loading…
Reference in New Issue
Block a user