mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 16:43:24 +03:00
app_store UI: details page
This commit is contained in:
parent
a3168c1274
commit
340d8d1436
@ -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");
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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, '');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user