app_store UI: details page

This commit is contained in:
bitful-pannul 2024-07-26 16:39:27 +03:00
parent a3168c1274
commit 340d8d1436
8 changed files with 146 additions and 34 deletions

View File

@ -30,7 +30,7 @@ pub fn init_frontend(our: &Address) {
"ui", "ui",
true, true,
false, false,
vec!["/", "/my-apps", "/apps/:id", "/publish"], vec!["/", "/my-apps", "/app/:id", "/publish"],
) )
.expect("failed to serve static UI"); .expect("failed to serve static UI");

View File

@ -1,4 +1,4 @@
export const MY_APPS_PATH = '/my-apps'; 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 = '/apps'; export const APP_DETAILS_PATH = '/app';

View File

@ -438,3 +438,73 @@
background-color: var(--gray); background-color: var(--gray);
color: var(--white); color: var(--white);
} }
.app-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.app-info-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.app-info-item>span:first-child {
font-weight: bold;
margin-bottom: 0.5rem;
}
.app-info-item a {
color: var(--blue);
text-decoration: none;
}
.app-info-item a:hover {
text-decoration: underline;
}
.mirrors-dropdown {
position: relative;
}
.mirrors-dropdown-toggle {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
color: var(--off-black);
}
.mirrors-list {
position: absolute;
top: 100%;
left: 0;
background-color: white;
border: 1px solid var(--gray-light);
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
list-style-type: none;
padding: 0;
margin: 0;
z-index: 10;
max-height: 200px;
overflow-y: auto;
}
.mirrors-list li {
padding: 8px 12px;
border-bottom: 1px solid var(--gray-light);
}
.mirrors-list li:last-child {
border-bottom: none;
}
.mirrors-list li:hover {
background-color: var(--gray-lighter);
}

View File

@ -1,17 +1,17 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { FaGlobe, FaPeopleGroup, FaCode } from "react-icons/fa6"; import { FaGlobe, FaPeopleGroup, FaCode, FaLink, FaCaretDown } from "react-icons/fa6";
import { AppInfo } from "../types/Apps"; import { AppInfo } from "../types/Apps";
import useAppsStore from "../store"; import useAppsStore from "../store";
import { PUBLISH_PATH } from "../constants/path"; import { PUBLISH_PATH } from "../constants/path";
import { FaCheckCircle, FaTimesCircle } from "react-icons/fa";
export default function AppPage() { export default function AppPage() {
const { getApp, installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate } = useAppsStore(); const { getApp, installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate } = useAppsStore();
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const [app, setApp] = useState<AppInfo | undefined>(undefined); const [app, setApp] = useState<AppInfo | undefined>(undefined);
const [launchPath, setLaunchPath] = useState('');
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@ -19,18 +19,6 @@ export default function AppPage() {
} }
}, [id, getApp]); }, [id, getApp]);
useEffect(() => {
fetch('/apps').then(data => data.json())
.then((data: Array<{ package_name: string, path: string }>) => {
if (Array.isArray(data)) {
const homepageAppData = data.find(otherApp => app?.package === otherApp.package_name)
if (homepageAppData) {
setLaunchPath(homepageAppData.path)
}
}
})
}, [app])
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);
@ -45,7 +33,9 @@ export default function AppPage() {
const version = app.metadata?.properties?.current_version || "Unknown"; const version = app.metadata?.properties?.current_version || "Unknown";
const versions = Object.entries(app.metadata?.properties?.code_hashes || {}); const versions = Object.entries(app.metadata?.properties?.code_hashes || {});
const hash = app.state?.our_version || (versions[(versions.length || 1) - 1] || ["", ""])[1]; const hash = app.state?.our_version || (versions[(versions.length || 1) - 1] || ["", ""])[1];
const mirrors = app.metadata?.properties?.mirrors?.length || 0; const mirrors = app.metadata?.properties?.mirrors || [];
const tbaLink = `https://optimistic.etherscan.io/address/${app.tba}`;
return ( return (
<div className="app-page"> <div className="app-page">
@ -68,8 +58,44 @@ export default function AppPage() {
<div className="app-info-item"> <div className="app-info-item">
<FaGlobe size={36} /> <FaGlobe size={36} />
<span>Mirrors</span> <span>Mirrors</span>
<span>{mirrors}</span> <span>{mirrors.length}</span>
<MirrorsDropdown mirrors={mirrors} />
</div> </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 && ( {app.metadata?.properties?.screenshots && (
<div className="app-screenshots"> <div className="app-screenshots">
@ -96,11 +122,27 @@ export default function AppPage() {
) : ( ) : (
<button onClick={handleInstall}>Install</button> <button onClick={handleInstall}>Install</button>
)} )}
{launchPath && (
<a href={launchPath} className="button">Launch</a>
)}
</div> </div>
</div> </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>
))}
</ul>
)}
</div>
);
};

View File

@ -5,6 +5,7 @@ import { useNavigate, Link } from "react-router-dom";
import { AppInfo } from "../types/Apps"; import { AppInfo } from "../types/Apps";
import useAppsStore from "../store"; import useAppsStore from "../store";
import { PUBLISH_PATH } from "../constants/path"; import { PUBLISH_PATH } from "../constants/path";
import { appId } from "../utils/app";
export default function MyAppsPage() { export default function MyAppsPage() {
const { apps, getApps } = useAppsStore(); const { apps, getApps } = useAppsStore();
@ -67,7 +68,7 @@ interface AppEntryProps {
const AppEntry: React.FC<AppEntryProps> = ({ app }) => { const AppEntry: React.FC<AppEntryProps> = ({ app }) => {
return ( return (
<Link to={`/apps/${app.package}`} className="app-entry"> <Link to={`/app/${appId(app)}`} className="app-entry">
<h3>{app.metadata?.name || app.package}</h3> <h3>{app.metadata?.name || app.package}</h3>
<p>{app.metadata?.description || "No description available"}</p> <p>{app.metadata?.description || "No description available"}</p>
</Link> </Link>

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import useAppsStore from "../store"; import useAppsStore from "../store";
import { AppInfo } from "../types/Apps"; import { AppInfo } from "../types/Apps";
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 } from "react-icons/fa6";
@ -61,7 +62,7 @@ export default function StorePage() {
</thead> </thead>
<tbody> <tbody>
{filteredApps.map((app) => ( {filteredApps.map((app) => (
<AppRow key={app.package} app={app} /> <AppRow key={appId(app)} app={app} />
))} ))}
</tbody> </tbody>
</table> </table>
@ -81,7 +82,7 @@ const AppRow: React.FC<AppRowProps> = ({ app }) => {
return ( return (
<tr className="app-row"> <tr className="app-row">
<td> <td>
<Link to={`/app-details/${app.package}`} className="app-name"> <Link to={`/app/${appId(app)}`} className="app-name">
{app.metadata?.name || app.package} {app.metadata?.name || app.package}
</Link> </Link>
</td> </td>

View File

@ -6,7 +6,7 @@ export interface MyApps {
} }
export interface AppListing { export interface AppListing {
owner?: string tba: string
package: string package: string
publisher: string publisher: string
metadata_hash: string metadata_hash: string

View File

@ -39,30 +39,28 @@ export default defineConfig({
server: { server: {
open: true, open: true,
proxy: { proxy: {
'^/our\\.js': { [`^${BASE_URL}/our.js`]: {
target: PROXY_URL, target: PROXY_URL,
changeOrigin: true, changeOrigin: true,
rewrite: (path) => { rewrite: (path) => {
console.log('Rewriting path for our.js:', path); console.log('Proxying jsrequest:', path);
return '/our.js'; return '/our.js';
}, },
}, },
'^/kinode\\.css': { [`^${BASE_URL}/kinode.css`]: {
target: PROXY_URL, target: PROXY_URL,
changeOrigin: true, changeOrigin: true,
rewrite: (path) => { rewrite: (path) => {
console.log('Rewriting path for kinode.css:', path); console.log('Proxying csrequest:', path);
return '/kinode.css'; return '/kinode.css';
}, },
}, },
// This route will match all other HTTP requests to the backend
[`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|$))`]: { [`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|$))`]: {
target: PROXY_URL, target: PROXY_URL,
changeOrigin: true, changeOrigin: true,
rewrite: (path) => {
console.log('Rewriting path for other requests:', path);
return path.replace(BASE_URL, '');
},
}, },
}, },