mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-13 14:48:22 +03:00
Merge pull request #340 from kinode-dao/tm/homepage-appstore-touchup
homepage and appstore touch-up
This commit is contained in:
commit
4f1ecf8422
94
kinode/packages/app_store/pkg/ui/assets/index-BxGs27ah.js
Normal file
94
kinode/packages/app_store/pkg/ui/assets/index-BxGs27ah.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -14,8 +14,8 @@
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-kpw1YN6W.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-i4SytJ9j.css">
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-BxGs27ah.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-umttRNrr.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "kit-ui",
|
||||
"name": "kinode-app-store",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
@ -110,7 +110,7 @@ function App() {
|
||||
const props = { provider, packageAbi };
|
||||
|
||||
return (
|
||||
<div className="flex flex-col c h-screen w-screen">
|
||||
<div className="flex flex-col c h-screen w-screen max-h-screen max-w-screen overflow-x-hidden special-appstore-background">
|
||||
<Web3ReactProvider connectors={connectors}>
|
||||
<Router basename={BASE_URL}>
|
||||
<Routes>
|
||||
|
@ -1,27 +1,19 @@
|
||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import Modal from "./Modal";
|
||||
import { getAppName } from "../utils/app";
|
||||
import Loader from "./Loader";
|
||||
import classNames from "classnames";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import UpdateButton from "./UpdateButton";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
import InstallButton from "./InstallButton";
|
||||
import LaunchButton from "./LaunchButton";
|
||||
import { FaCheck } from "react-icons/fa6";
|
||||
|
||||
interface ActionButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
isIcon?: boolean;
|
||||
}
|
||||
|
||||
export default function ActionButton({ app, ...props }: ActionButtonProps) {
|
||||
const { updateApp, downloadApp, installApp, getCaps, getMyApp } =
|
||||
useAppsStore();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [mirror, setMirror] = useState(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
const [customMirror, setCustomMirror] = useState("");
|
||||
const [caps, setCaps] = useState<string[]>([]);
|
||||
const [launchPath, setLaunchPath] = useState('');
|
||||
const [loading, setLoading] = useState("");
|
||||
|
||||
const { clean, installed, downloaded, updatable } = useMemo(() => {
|
||||
export default function ActionButton({ app, isIcon = false, ...props }: ActionButtonProps) {
|
||||
const [incrementNumber, setIncrementNumber] = useState(0);
|
||||
const { installed, downloaded, updatable } = useMemo(() => {
|
||||
const versions = Object.entries(app?.metadata?.properties?.code_hashes || {});
|
||||
const latestHash = (versions.find(([v]) => v === app.metadata?.properties?.current_version) || [])[1];
|
||||
|
||||
@ -31,217 +23,46 @@ export default function ActionButton({ app, ...props }: ActionButtonProps) {
|
||||
const updatable =
|
||||
Boolean(app.state?.our_version && latestHash) &&
|
||||
app.state?.our_version !== latestHash &&
|
||||
app.publisher !== window.our.node;
|
||||
app.publisher !== (window as any).our.node;
|
||||
return {
|
||||
clean: !installed && !downloaded && !updatable,
|
||||
installed,
|
||||
downloaded,
|
||||
updatable,
|
||||
};
|
||||
}, [app]);
|
||||
}, [app, incrementNumber]);
|
||||
|
||||
|
||||
const [launchPath, setLaunchPath] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setMirror(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
}, [app.metadata?.properties?.mirrors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (installed) {
|
||||
fetch('/apps').then(data => data.json())
|
||||
.then((data: Array<{ package_name: string, path: string }>) => {
|
||||
// console.log(data)
|
||||
if (Array.isArray(data)) {
|
||||
// console.log('is array')
|
||||
const homepageAppData = data.find(otherApp => app.package === otherApp.package_name)
|
||||
if (homepageAppData) {
|
||||
// console.log('found the good appness', homepageAppData.package_name, homepageAppData.path);
|
||||
setLaunchPath(homepageAppData.path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [installed])
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (installed && !updatable && launchPath) {
|
||||
window.location.href = `/${launchPath.replace('/', '')}`
|
||||
return;
|
||||
} else {
|
||||
if (downloaded) {
|
||||
getCaps(app).then((manifest) => {
|
||||
setCaps(manifest.request_capabilities);
|
||||
});
|
||||
}
|
||||
setShowModal(true);
|
||||
}
|
||||
}, [app, installed, downloaded, updatable, setShowModal, getCaps, launchPath]);
|
||||
|
||||
const download = useCallback(async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const targetMirror = mirror === "Other" ? customMirror : mirror;
|
||||
|
||||
if (!targetMirror) {
|
||||
window.alert("Please select a mirror");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(`Downloading ${getAppName(app)}...`);
|
||||
await downloadApp(app, targetMirror);
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then(() => {
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(
|
||||
`Failed to download app from ${targetMirror}, please try a different mirror.`
|
||||
);
|
||||
setLoading("");
|
||||
}
|
||||
}, [mirror, customMirror, app, downloadApp, getMyApp]);
|
||||
|
||||
const install = useCallback(async () => {
|
||||
try {
|
||||
setLoading(`Installing ${getAppName(app)}...`);
|
||||
await installApp(app);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then((app) => {
|
||||
if (!app.installed) return;
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to install, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, installApp, getMyApp]);
|
||||
|
||||
const update = useCallback(async () => {
|
||||
try {
|
||||
setLoading(`Updating ${getAppName(app)}...`);
|
||||
await updateApp(app);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then((app) => {
|
||||
if (!app.installed) return;
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to update, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, updateApp, getMyApp]);
|
||||
|
||||
const appName = getAppName(app);
|
||||
}, [app, incrementNumber])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames("text-sm min-w-[100px] px-2 py-1 self-start", props.className, {
|
||||
'bg-orange': installed,
|
||||
'hidden': installed && !updatable && !launchPath
|
||||
})}
|
||||
onClick={onClick}
|
||||
{(installed && launchPath)
|
||||
? <LaunchButton app={app} {...props} isIcon={isIcon} launchPath={launchPath} />
|
||||
: (installed && updatable)
|
||||
? <UpdateButton app={app} {...props} isIcon={isIcon} callback={() => setIncrementNumber(incrementNumber + 1)} />
|
||||
: !downloaded
|
||||
? <DownloadButton app={app} {...props} isIcon={isIcon} callback={() => setIncrementNumber(incrementNumber + 1)} />
|
||||
: !installed
|
||||
? <InstallButton app={app} {...props} isIcon={isIcon} callback={() => setIncrementNumber(incrementNumber + 1)} />
|
||||
: isIcon
|
||||
? <button
|
||||
className="pointer-events none icon clear absolute top-0 right-0"
|
||||
>
|
||||
{installed && updatable
|
||||
? "Update"
|
||||
: installed && launchPath
|
||||
? "Launch"
|
||||
: installed
|
||||
? "Installed"
|
||||
: downloaded
|
||||
? "Install"
|
||||
: "Download"}
|
||||
<FaCheck />
|
||||
</button>
|
||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
||||
{loading ? (
|
||||
<Loader msg={loading} />
|
||||
) : clean ? (
|
||||
<form className="flex flex-col items-center gap-2" onSubmit={download}>
|
||||
<h4>Download '{appName}'</h4>
|
||||
<h5 style={{ margin: 0 }}>Select Mirror</h5>
|
||||
<select value={mirror} onChange={(e) => setMirror(e.target.value)}>
|
||||
{((app.metadata?.properties?.mirrors || []).concat(["Other"])).map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{mirror === "Other" && (
|
||||
<input
|
||||
type="text"
|
||||
value={customMirror}
|
||||
onChange={(e) => setCustomMirror(e.target.value)}
|
||||
placeholder="Mirror, i.e. 'template.os'"
|
||||
className="p-1 max-w-[240px] w-full"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<button type="submit">
|
||||
Download
|
||||
</button>
|
||||
</form>
|
||||
) : downloaded ? (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 className="m-0">
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
<ul className="flex flex-col items-start">
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button type="button" onClick={install}>
|
||||
Approve & Install
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 className="m-0">
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
{/* <h5>Send Messages:</h5> */}
|
||||
<br />
|
||||
<ul className="flex flex-col items-start">
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* <h5>Receive Messages:</h5>
|
||||
<ul>
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul> */}
|
||||
<button type="button" onClick={update}>
|
||||
Approve & Update
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
: <div>Installed</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -4,25 +4,40 @@ import AppHeader from "./AppHeader";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import MoreActions from "./MoreActions";
|
||||
import { isMobileCheck } from "../utils/dimensions";
|
||||
import classNames from "classnames";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { APP_DETAILS_PATH } from "../constants/path";
|
||||
|
||||
interface AppEntryProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
size?: "small" | "medium" | "large";
|
||||
overrideImageSize?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
export default function AppEntry({ app, ...props }: AppEntryProps) {
|
||||
export default function AppEntry({ app, size = "medium", overrideImageSize, ...props }: AppEntryProps) {
|
||||
const isMobile = isMobileCheck()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div {...props} key={appId(app)} className={classNames("flex justify-between w-full rounded hover:bg-white/10 card", {
|
||||
'flex-wrap gap-2': isMobile
|
||||
})}>
|
||||
<AppHeader app={app} size="small" />
|
||||
<div className="flex mr-1 items-start">
|
||||
<ActionButton app={app} className="mr-2" />
|
||||
<MoreActions app={app} />
|
||||
</div>
|
||||
<div
|
||||
{...props}
|
||||
key={appId(app)}
|
||||
className={classNames("flex justify-between rounded-lg hover:bg-white/10 card cursor-pointer", props.className, {
|
||||
'flex-wrap gap-2': isMobile,
|
||||
'flex-col relative': size !== 'large'
|
||||
})}
|
||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
||||
>
|
||||
<AppHeader app={app} size={size} overrideImageSize={overrideImageSize} />
|
||||
<ActionButton
|
||||
app={app}
|
||||
isIcon={size !== 'large'}
|
||||
className={classNames({
|
||||
'absolute top-0 right-0': size !== 'large',
|
||||
'bg-orange text-lg min-w-1/5': size === 'large',
|
||||
'ml-auto': size === 'large' && isMobile
|
||||
})} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,54 +1,84 @@
|
||||
import React from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { APP_DETAILS_PATH } from "../constants/path";
|
||||
import ColorDot from "./ColorDot";
|
||||
import { isMobileCheck } from "../utils/dimensions";
|
||||
|
||||
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
size?: "small" | "medium" | "large";
|
||||
overrideImageSize?: "small" | "medium" | "large"
|
||||
}
|
||||
|
||||
export default function AppHeader({
|
||||
app,
|
||||
size = "medium",
|
||||
overrideImageSize,
|
||||
...props
|
||||
}: AppHeaderProps) {
|
||||
const navigate = useNavigate()
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={classNames('flex w-full justify-content-start', size, props.className, { 'cursor-pointer': size !== 'large' })}
|
||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
||||
const appName = <div
|
||||
className={classNames({
|
||||
'text-3xl font-[OpenSans]': !isMobile && size === 'large',
|
||||
'text-xl': !isMobile && size !== 'large',
|
||||
'text-lg': isMobile
|
||||
})}
|
||||
>
|
||||
{app.metadata?.name || appId(app)}
|
||||
</div>
|
||||
|
||||
const imageSize = overrideImageSize || size
|
||||
|
||||
return <div
|
||||
{...props}
|
||||
className={classNames('flex w-full justify-content-start', size, props.className, {
|
||||
'flex-col': size === 'small',
|
||||
'gap-2': isMobile,
|
||||
'gap-4': !isMobile,
|
||||
'gap-6': !isMobile && size === 'large'
|
||||
})}
|
||||
>
|
||||
{size === 'small' && appName}
|
||||
{app.metadata?.image
|
||||
? <img
|
||||
src={app.metadata.image}
|
||||
alt="app icon"
|
||||
className={classNames('mr-2', { 'h-32 rounded-md': size === 'large', 'h-12 rounded': size !== 'large' })}
|
||||
className={classNames('object-cover', {
|
||||
'rounded': !imageSize,
|
||||
'rounded-lg': imageSize === 'small',
|
||||
'rounded-xl': imageSize === 'medium',
|
||||
'rounded-2xl': imageSize === 'large',
|
||||
'h-32': imageSize === 'large' || imageSize === 'small',
|
||||
'h-20': imageSize === 'medium',
|
||||
})}
|
||||
/>
|
||||
: <ColorDot
|
||||
num={app.metadata_hash}
|
||||
dotSize={size}
|
||||
className={classNames('mr-2')}
|
||||
dotSize={imageSize}
|
||||
/>}
|
||||
<div className={classNames("flex flex-col", { 'gap-2 max-w-3/4': isMobile })}>
|
||||
<div className={classNames("flex flex-col", {
|
||||
'gap-2': isMobile,
|
||||
'gap-4 max-w-3/4': isMobile && size !== 'small'
|
||||
})}>
|
||||
{size !== 'small' && appName}
|
||||
{app.metadata?.description && (
|
||||
<div
|
||||
className={classNames("whitespace-nowrap overflow-hidden text-ellipsis", { 'text-3xl': size === 'large', })}
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
className={classNames({
|
||||
'text-2xl': size === 'large'
|
||||
})}
|
||||
>
|
||||
{app.metadata?.name || appId(app)}
|
||||
</div>
|
||||
{app.metadata?.description && size !== "large" && (
|
||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{app.metadata?.description?.slice(0, 100)}
|
||||
{app.metadata.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -33,8 +33,8 @@ const ColorDot: React.FC<ColorDotProps> = ({
|
||||
<div {...props} className={classNames('flex', props.className)}>
|
||||
<div
|
||||
className={classNames('m-0 align-self-center border rounded-full outline-black', {
|
||||
'h-20 w-20': !isMobile && dotSize === 'large',
|
||||
'h-16 w-16': !isMobile && dotSize === 'medium',
|
||||
'h-32 w-32': !isMobile && dotSize === 'large',
|
||||
'h-18 w-18': !isMobile && dotSize === 'medium',
|
||||
'h-12 w-12': isMobile || dotSize === 'small',
|
||||
'border-4': !isMobile,
|
||||
'border-2': isMobile,
|
||||
|
111
kinode/packages/app_store/ui/src/components/DownloadButton.tsx
Normal file
111
kinode/packages/app_store/ui/src/components/DownloadButton.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import Modal from "./Modal";
|
||||
import { getAppName } from "../utils/app";
|
||||
import Loader from "./Loader";
|
||||
import classNames from "classnames";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FaDownload } from "react-icons/fa6";
|
||||
|
||||
interface DownloadButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
isIcon?: boolean;
|
||||
callback?: () => void;
|
||||
}
|
||||
|
||||
export default function DownloadButton({ app, isIcon = false, callback, ...props }: DownloadButtonProps) {
|
||||
const { downloadApp, getCaps, getMyApp } =
|
||||
useAppsStore();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [mirror, setMirror] = useState(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
const [customMirror, setCustomMirror] = useState("");
|
||||
const [loading, setLoading] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setMirror(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
}, [app.metadata?.properties?.mirrors]);
|
||||
|
||||
const onClick = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
setShowModal(true);
|
||||
}, [app, setShowModal, getCaps]);
|
||||
|
||||
const download = useCallback(async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const targetMirror = mirror === "Other" ? customMirror : mirror;
|
||||
|
||||
if (!targetMirror) {
|
||||
window.alert("Please select a mirror");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(`Downloading ${getAppName(app)}...`);
|
||||
await downloadApp(app, targetMirror);
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then(() => {
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
callback && callback();
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(
|
||||
`Failed to download app from ${targetMirror}, please try a different mirror.`
|
||||
);
|
||||
setLoading("");
|
||||
}
|
||||
}, [mirror, customMirror, app, downloadApp, getMyApp]);
|
||||
|
||||
const appName = getAppName(app);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames("text-sm self-start", props.className, { 'icon clear': isIcon })}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isIcon ? <FaDownload /> : 'Download'}
|
||||
</button>
|
||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
||||
{loading ? (
|
||||
<Loader msg={loading} />
|
||||
) : (
|
||||
<form className="flex flex-col items-center gap-2" onSubmit={download}>
|
||||
<h4>Download '{appName}'</h4>
|
||||
<h5>Select Mirror</h5>
|
||||
<select value={mirror} onChange={(e) => setMirror(e.target.value)}>
|
||||
{((app.metadata?.properties?.mirrors || []).concat(["Other"])).map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{mirror === "Other" && (
|
||||
<input
|
||||
type="text"
|
||||
value={customMirror}
|
||||
onChange={(e) => setCustomMirror(e.target.value)}
|
||||
placeholder="Mirror, i.e. 'template.os'"
|
||||
className="p-1 max-w-[240px] w-full"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<button type="submit">
|
||||
Download
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
19
kinode/packages/app_store/ui/src/components/HomeButton.tsx
Normal file
19
kinode/packages/app_store/ui/src/components/HomeButton.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react"
|
||||
import { FaHome } from "react-icons/fa"
|
||||
import { isMobileCheck } from "../utils/dimensions";
|
||||
|
||||
const HomeButton: React.FC = () => {
|
||||
const isMobile = isMobileCheck()
|
||||
return <button
|
||||
className={classNames("clear absolute p-2", {
|
||||
'top-2 left-2': isMobile,
|
||||
'top-8 left-8': !isMobile
|
||||
})}
|
||||
onClick={() => window.location.href = '/'}
|
||||
>
|
||||
<FaHome size={24} />
|
||||
</button>
|
||||
}
|
||||
|
||||
export default HomeButton;
|
@ -0,0 +1,88 @@
|
||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import Modal from "./Modal";
|
||||
import { getAppName } from "../utils/app";
|
||||
import Loader from "./Loader";
|
||||
import classNames from "classnames";
|
||||
import { FaI } from "react-icons/fa6";
|
||||
|
||||
interface InstallButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
isIcon?: boolean;
|
||||
callback?: () => void;
|
||||
}
|
||||
|
||||
export default function InstallButton({ app, isIcon = false, callback, ...props }: InstallButtonProps) {
|
||||
const { installApp, getCaps, getMyApp } =
|
||||
useAppsStore();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [caps, setCaps] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState("");
|
||||
|
||||
const onClick = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
getCaps(app).then((manifest) => {
|
||||
setCaps(manifest.request_capabilities);
|
||||
});
|
||||
setShowModal(true);
|
||||
}, [app, setShowModal, getCaps]);
|
||||
|
||||
const install = useCallback(async () => {
|
||||
try {
|
||||
setLoading(`Installing ${getAppName(app)}...`);
|
||||
await installApp(app);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then((app) => {
|
||||
if (!app.installed) return;
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
callback && callback();
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to install, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, installApp, getMyApp]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames("text-sm self-start", props.className, {
|
||||
'icon clear': isIcon
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isIcon ? <FaI /> : "Install"}
|
||||
</button>
|
||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
||||
{loading ? (
|
||||
<Loader msg={loading} />
|
||||
) : (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 className="m-0">
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
<ul className="flex flex-col items-start">
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button type="button" onClick={install}>
|
||||
Approve & Install
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
33
kinode/packages/app_store/ui/src/components/LaunchButton.tsx
Normal file
33
kinode/packages/app_store/ui/src/components/LaunchButton.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import classNames from "classnames";
|
||||
import { FaPlay } from "react-icons/fa6";
|
||||
|
||||
interface LaunchButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
launchPath: string;
|
||||
isIcon?: boolean;
|
||||
}
|
||||
|
||||
export default function LaunchButton({ app, launchPath, isIcon = false, ...props }: LaunchButtonProps) {
|
||||
const onLaunch = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `/${launchPath.replace('/', '')}`
|
||||
return;
|
||||
}, [app, launchPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames("text-sm self-start", props.className, {
|
||||
'icon clear': isIcon
|
||||
})}
|
||||
onClick={onLaunch}
|
||||
>
|
||||
{isIcon ? <FaPlay /> : "Launch"}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
@ -27,7 +27,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(`bg-black/25 fixed top-0 bottom-0 left-0 right-0 flex flex-col c z-30 min-h-[10em] min-w-[30em]`,
|
||||
className={classNames(`bg-black/25 backdrop-blur-lg fixed top-0 bottom-0 left-0 right-0 flex flex-col c z-30 min-h-[10em] min-w-[30em]`,
|
||||
{ show }
|
||||
)}
|
||||
onClick={hide}
|
||||
|
@ -5,11 +5,12 @@ import {
|
||||
FaDownload,
|
||||
FaMagnifyingGlass,
|
||||
FaUpload,
|
||||
FaX,
|
||||
} from "react-icons/fa6";
|
||||
|
||||
import { MY_APPS_PATH, PUBLISH_PATH } from "../constants/path";
|
||||
import classNames from "classnames";
|
||||
import { isMobileCheck } from "../utils/dimensions";
|
||||
import HomeButton from "./HomeButton";
|
||||
import { FaHome } from "react-icons/fa";
|
||||
|
||||
interface SearchHeaderProps {
|
||||
@ -34,67 +35,73 @@ export default function SearchHeader({
|
||||
|
||||
const canGoBack = location.key !== "default";
|
||||
const isMyAppsPage = location.pathname === MY_APPS_PATH;
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
{location.pathname !== '/' ? (
|
||||
<button className="flex flex-col c mr-2 icon" onClick={() => {
|
||||
<div className={classNames("flex justify-between", {
|
||||
"gap-4": isMobile,
|
||||
"gap-8": !isMobile
|
||||
})}>
|
||||
{location.pathname !== '/'
|
||||
? <button
|
||||
className="flex flex-col c icon icon-orange"
|
||||
onClick={() => {
|
||||
if (onBack) {
|
||||
onBack()
|
||||
} else {
|
||||
canGoBack ? navigate(-1) : navigate('/')
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<FaArrowLeft />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="flex flex-col c mr-2 icon"
|
||||
: isMobile
|
||||
? <button
|
||||
className={classNames("icon icon-orange", {
|
||||
})}
|
||||
onClick={() => window.location.href = '/'}
|
||||
>
|
||||
<FaHome />
|
||||
</button>
|
||||
)}
|
||||
: <></>}
|
||||
{!hidePublish && <button
|
||||
className="flex flex-col c mr-2 icon"
|
||||
className="flex flex-col c icon icon-orange"
|
||||
onClick={() => navigate(PUBLISH_PATH)}
|
||||
>
|
||||
<FaUpload />
|
||||
</button>}
|
||||
{!hideSearch && (
|
||||
<div className="flex mr-2 flex-1 rounded-md">
|
||||
<button
|
||||
className="icon mr-2"
|
||||
type="button"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
<FaMagnifyingGlass />
|
||||
</button>
|
||||
<div className="flex flex-1 rounded-md relative">
|
||||
<input
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={value}
|
||||
placeholder="Search for apps..."
|
||||
className="w-full mr-2"
|
||||
className="w-full self-stretch grow"
|
||||
/>
|
||||
{value.length > 0 && <button
|
||||
className="icon"
|
||||
onClick={() => onChange("")}
|
||||
>
|
||||
<FaX />
|
||||
</button>}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<button
|
||||
className={classNames("flex alt")}
|
||||
onClick={() => (isMyAppsPage ? navigate(-1) : navigate(MY_APPS_PATH))}
|
||||
className={classNames("icon border-0 absolute top-1/2 -translate-y-1/2", {
|
||||
'right-2': isMobile,
|
||||
'right-4': !isMobile
|
||||
})}
|
||||
type="button"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
<FaDownload className="mr-2" />
|
||||
<span>My Apps</span>
|
||||
<FaMagnifyingGlass />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className={classNames("flex c", {
|
||||
"gap-4": isMobile,
|
||||
"gap-8 basis-1/5": !isMobile
|
||||
})}
|
||||
onClick={() => (isMyAppsPage ? navigate(-1) : navigate(MY_APPS_PATH))}
|
||||
>
|
||||
{!isMobile && <span>My Apps</span>}
|
||||
<FaDownload />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
94
kinode/packages/app_store/ui/src/components/UpdateButton.tsx
Normal file
94
kinode/packages/app_store/ui/src/components/UpdateButton.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import Modal from "./Modal";
|
||||
import { getAppName } from "../utils/app";
|
||||
import Loader from "./Loader";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface UpdateButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
isIcon?: boolean;
|
||||
callback?: () => void
|
||||
}
|
||||
|
||||
export default function UpdateButton({ app, callback, isIcon = false, ...props }: UpdateButtonProps) {
|
||||
const { updateApp, getCaps, getMyApp } =
|
||||
useAppsStore();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [caps, setCaps] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState("");
|
||||
|
||||
|
||||
const onClick = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
getCaps(app).then((manifest) => {
|
||||
setCaps(manifest.request_capabilities);
|
||||
});
|
||||
setShowModal(true);
|
||||
}, [app, setShowModal, getCaps]);
|
||||
|
||||
const update = useCallback(async () => {
|
||||
try {
|
||||
setLoading(`Updating ${getAppName(app)}...`);
|
||||
await updateApp(app);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then((app) => {
|
||||
if (!app.installed) return;
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
callback && callback();
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to update, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, updateApp, getMyApp]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames("text-sm self-start", props.className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
||||
{loading ? (
|
||||
<Loader msg={loading} />
|
||||
) : (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 className="m-0">
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
{/* <h5>Send Messages:</h5> */}
|
||||
<br />
|
||||
<ul className="flex flex-col items-start">
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* <h5>Receive Messages:</h5>
|
||||
<ul>
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul> */}
|
||||
<button type="button" onClick={update}>
|
||||
Approve & Update
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -60,7 +60,7 @@ h6 {
|
||||
button,
|
||||
button[type="submit"],
|
||||
.button {
|
||||
@apply flex m-0 py-2 px-6 rounded border-orange bg-orange border-2 cursor-pointer place-items-center place-content-center text-center rounded-lg heading transition ease-in-out duration-100 hover:bg-black text-white font-[OpenSans];
|
||||
@apply flex m-0 py-2 px-6 rounded border-orange bg-orange border cursor-pointer place-items-center place-content-center text-center rounded-lg heading transition ease-in-out duration-100 hover:bg-black text-white font-[OpenSans];
|
||||
}
|
||||
|
||||
.clear {
|
||||
@ -80,7 +80,15 @@ button[type="submit"],
|
||||
}
|
||||
|
||||
.icon.alt {
|
||||
@apply border-black/25 hover:border-white/25
|
||||
@apply border-black/25 hover:border-white/25;
|
||||
}
|
||||
|
||||
.icon.icon-orange {
|
||||
@apply border-orange bg-orange/25;
|
||||
}
|
||||
|
||||
.icon.clear {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -98,7 +106,7 @@ textarea,
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="checkbox"] {
|
||||
@apply px-4 py-2 rounded-lg bg-orange bg-opacity-25 text-white border border-orange border-2;
|
||||
@apply px-4 py-2 rounded-lg bg-orange bg-opacity-25 text-white border border-solid border-orange;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@ -133,3 +141,8 @@ button:disabled {
|
||||
.c {
|
||||
@apply place-items-center place-content-center;
|
||||
}
|
||||
|
||||
.special-appstore-background {
|
||||
background-color: #22211f;
|
||||
background-image: radial-gradient(circle at -20% -68%, #f75a2991 36%, transparent 56.05%), linear-gradient(37deg, #86000185 19%, transparent 45.05%), linear-gradient(-36deg, #8600016e 26%, transparent 50.05%);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import React, { useState, useEffect, useMemo, useCallback, ReactElement } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { AppInfo } from "../types/Apps";
|
||||
@ -9,6 +9,10 @@ import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { appId } from "../utils/app";
|
||||
import { PUBLISH_PATH } from "../constants/path";
|
||||
import HomeButton from "../components/HomeButton";
|
||||
import classNames from "classnames";
|
||||
import { isMobileCheck } from "../utils/dimensions";
|
||||
import { FaGlobe, FaPeopleGroup, FaStar } from "react-icons/fa6";
|
||||
|
||||
interface AppPageProps extends PageProps { }
|
||||
|
||||
@ -48,80 +52,104 @@ export default function AppPage() {
|
||||
app?.state?.our_version ||
|
||||
(versions[(versions.length || 1) - 1] || ["", ""])[1];
|
||||
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
const appDetails: Array<{ top: ReactElement, middle: ReactElement, bottom: ReactElement }> = [
|
||||
{
|
||||
top: <div className={classNames({ 'text-sm': isMobile })}>0 ratings</div>,
|
||||
middle: <span className="text-2xl">5.0</span>,
|
||||
bottom: <div className={classNames("flex-center gap-1", {
|
||||
'text-sm': isMobile
|
||||
})}>
|
||||
<FaStar />
|
||||
<FaStar />
|
||||
<FaStar />
|
||||
<FaStar />
|
||||
<FaStar />
|
||||
</div>
|
||||
},
|
||||
{
|
||||
top: <div className={classNames({ 'text-sm': isMobile })}>Developer</div>,
|
||||
middle: <FaPeopleGroup size={36} />,
|
||||
bottom: <div className={classNames({ 'text-sm': isMobile })}>
|
||||
{app?.publisher}
|
||||
</div>
|
||||
},
|
||||
{
|
||||
top: <div className={classNames({ 'text-sm': isMobile })}>Version</div>,
|
||||
middle: <span className="text-2xl">{version}</span>,
|
||||
bottom: <div className={classNames({ 'text-xs': isMobile })}>
|
||||
{hash.slice(0, 5)}...{hash.slice(-5)}
|
||||
</div>
|
||||
},
|
||||
{
|
||||
top: <div className={classNames({ 'text-sm': isMobile })}>Mirrors</div>,
|
||||
middle: <FaGlobe size={36} />,
|
||||
bottom: <div className={classNames({ 'text-sm': isMobile })}>
|
||||
{app?.metadata?.properties?.mirrors?.length || 0}
|
||||
</div>
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full max-w-[900px]">
|
||||
<SearchHeader value="" onChange={() => null} hideSearch />
|
||||
<div className="card mt1">
|
||||
{app ? (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<AppHeader app={app} size="large" />
|
||||
<ActionButton app={app} className="mr-1" />
|
||||
<div className={classNames("flex flex-col w-full h-screen",
|
||||
{
|
||||
'gap-4 p-2 max-w-screen': isMobile,
|
||||
'gap-8 max-w-[900px]': !isMobile,
|
||||
})}
|
||||
>
|
||||
{!isMobile && <HomeButton />}
|
||||
<SearchHeader
|
||||
value=""
|
||||
onChange={() => null}
|
||||
hideSearch
|
||||
hidePublish
|
||||
/>
|
||||
<div className={classNames("flex-col-center card !rounded-3xl", {
|
||||
'p-12 gap-4 grow overflow-y-auto': isMobile,
|
||||
'p-24 gap-8': !isMobile,
|
||||
})}>
|
||||
{app ? <>
|
||||
<AppHeader app={app} size={isMobile ? "medium" : "large"} />
|
||||
<div className="w-5/6 h-0 border border-orange" />
|
||||
<div className={classNames("flex items-start text-xl", {
|
||||
'gap-4 flex-wrap': isMobile,
|
||||
'gap-8': !isMobile,
|
||||
})}>
|
||||
{appDetails.map((detail, index) => <>
|
||||
<div
|
||||
className={classNames("flex-col-center gap-2 justify-between self-stretch", {
|
||||
'rounded-lg bg-white/10 p-1 min-w-1/4 grow': isMobile,
|
||||
'opacity-50': !isMobile,
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{detail.top}
|
||||
{detail.middle}
|
||||
{detail.bottom}
|
||||
</div>
|
||||
<div className="flex flex-col mt-2">
|
||||
<div className="flex mt-1 items-start">
|
||||
<div className="w-1/4">Description</div>
|
||||
<div className="mb-1 w-3/4">
|
||||
{(app.metadata?.description || "No description given").slice(
|
||||
0,
|
||||
2000
|
||||
)}
|
||||
{!isMobile && index !== appDetails.length - 1 && <div className="h-3/4 w-0 border border-orange self-center" />}
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex mt-1 items-start">
|
||||
<div className="w-1/4">Publisher</div>
|
||||
<div className="mb-1 w-3/4">{app.publisher}</div>
|
||||
</div>
|
||||
<div className="flex mt-1 items-start">
|
||||
<div className="w-1/4">Version</div>
|
||||
<div className="mb-1 w-3/4">{version}</div>
|
||||
</div>
|
||||
<div className="flex mt-1 items-start">
|
||||
<div className="w-1/4">Mirrors</div>
|
||||
<div className="w-3/4 flex flex-col">
|
||||
{(app.metadata?.properties?.mirrors || []).map(
|
||||
(mirror, index) => (
|
||||
<div key={index + mirror} className="mb-1">
|
||||
{mirror}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex mt-1 items-start">
|
||||
<div className="w-1/4">Permissions</div>
|
||||
<div className="w-3/4 flex flex-col">
|
||||
{app.permissions?.map((permission, index) => (
|
||||
<div key={index + permission} className="mb-1">{permission}</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="flex mt-1 items-start">
|
||||
<div className="w-1/4">Hash</div>
|
||||
<div className="w-3/4 break-all">
|
||||
{hash}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-screenshots flex mt-2 overflow-x-auto max-w-full">
|
||||
{(app.metadata?.properties?.screenshots || []).map(
|
||||
{Array.isArray(app.metadata?.properties?.screenshots)
|
||||
&& app.metadata?.properties.screenshots.length > 0
|
||||
&& <div className="flex flex-wrap overflow-x-auto max-w-full">
|
||||
{app.metadata.properties.screenshots.map(
|
||||
(screenshot, index) => (
|
||||
<img key={index + screenshot} src={screenshot} className="mr-2 max-h-20 max-w-full rounded border border-black" />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{app.installed && (
|
||||
</div>}
|
||||
<ActionButton app={app} className={classNames("self-center bg-orange text-lg px-12")} />
|
||||
{app.installed && app.state?.mirroring && (
|
||||
<button type="button" onClick={goToPublish}>
|
||||
Publish
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
</> : <>
|
||||
<h4>App details not found for </h4>
|
||||
<h4>{params.id}</h4>
|
||||
</>
|
||||
)}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import { PageProps } from "../types/Page";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { appId } from "../utils/app";
|
||||
import { PUBLISH_PATH } from "../constants/path";
|
||||
import HomeButton from "../components/HomeButton";
|
||||
|
||||
export default function MyAppsPage() { // eslint-disable-line
|
||||
const { myApps, getMyApps } = useAppsStore()
|
||||
@ -53,6 +54,7 @@ export default function MyAppsPage() { // eslint-disable-line
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full max-w-[900px]">
|
||||
<HomeButton />
|
||||
<SearchHeader value={searchQuery} onChange={searchMyApps} />
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<h3>My Packages</h3>
|
||||
|
@ -16,6 +16,9 @@ import { AppInfo } from "../types/Apps";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import Jazzicon from "../components/Jazzicon";
|
||||
import { Tooltip } from "../components/Tooltip";
|
||||
import HomeButton from "../components/HomeButton";
|
||||
import classNames from "classnames";
|
||||
import { isMobileCheck } from "../utils/dimensions";
|
||||
|
||||
const { useIsActivating } = hooks;
|
||||
|
||||
@ -207,16 +210,22 @@ export default function PublishPage({
|
||||
}
|
||||
}, [listedApps, packageName, publisherId, isUpdate, setIsUpdate]);
|
||||
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return (
|
||||
<div className="max-w-[900px] w-full">
|
||||
<div className={classNames("w-full flex flex-col gap-2", {
|
||||
'max-w-[900px]': !isMobile,
|
||||
'p-2 h-screen w-screen': isMobile
|
||||
})}>
|
||||
{!isMobile && <HomeButton />}
|
||||
<SearchHeader
|
||||
hideSearch
|
||||
hidePublish
|
||||
onBack={showMetadataForm ? () => setShowMetadataForm(false) : undefined}
|
||||
/>
|
||||
<div className="flex justify-between items-center my-2">
|
||||
<div className="flex-center justify-between">
|
||||
<h4>Publish Package</h4>
|
||||
{Boolean(account) && <div className="card flex items-center">
|
||||
{Boolean(account) && <div className="card flex-center">
|
||||
<span>Publishing as:</span>
|
||||
<Jazzicon address={account!} className="mx-2" />
|
||||
<span className="font-mono">{account?.slice(0, 4)}...{account?.slice(-4)}</span>
|
||||
@ -224,20 +233,20 @@ export default function PublishPage({
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex-col-center">
|
||||
<Loader msg={loading} />
|
||||
</div>
|
||||
) : publishSuccess ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<h4 className="mb-2">Package Published!</h4>
|
||||
<div className="mb-2">
|
||||
<div className="flex-col-center gap-2">
|
||||
<h4>Package Published!</h4>
|
||||
<div>
|
||||
<strong>Package Name:</strong> {publishSuccess.packageName}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div>
|
||||
<strong>Publisher ID:</strong> {publishSuccess.publisherId}
|
||||
</div>
|
||||
<button
|
||||
className={`flex ml-2 mt-2`}
|
||||
className={`flex ml-2`}
|
||||
onClick={() => setPublishSuccess(undefined)}
|
||||
>
|
||||
Publish Another Package
|
||||
@ -247,7 +256,7 @@ export default function PublishPage({
|
||||
<MetadataForm {...{ packageName, publisherId, app: state?.app }} goBack={() => setShowMetadataForm(false)} />
|
||||
) : !account || !isActive ? (
|
||||
<>
|
||||
<h4 style={{}}>Please connect your wallet to publish a package</h4>
|
||||
<h4>Please connect your wallet {isMobile && <br />} to publish a package</h4>
|
||||
<button className={`connect-wallet row`} onClick={connectWallet}>
|
||||
Connect Wallet
|
||||
</button>
|
||||
@ -256,7 +265,7 @@ export default function PublishPage({
|
||||
<Loader msg="Approve connection in your wallet" />
|
||||
) : (
|
||||
<form
|
||||
className="flex flex-col flex-1 overflow-y-auto"
|
||||
className="flex flex-col flex-1 overflow-y-auto gap-2"
|
||||
onSubmit={publishPackage}
|
||||
>
|
||||
<div
|
||||
@ -270,7 +279,7 @@ export default function PublishPage({
|
||||
Update existing package
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-col mb-2">
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="package-name">Package Name</label>
|
||||
<input
|
||||
id="package-name"
|
||||
@ -282,7 +291,7 @@ export default function PublishPage({
|
||||
onBlur={checkIfUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col mb-2">
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="publisher-id">Publisher ID</label>
|
||||
<input
|
||||
id="publisher-id"
|
||||
@ -293,7 +302,7 @@ export default function PublishPage({
|
||||
onBlur={checkIfUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col mb-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="metadata-url">
|
||||
Metadata URL
|
||||
</label>
|
||||
@ -306,7 +315,7 @@ export default function PublishPage({
|
||||
onBlur={calculateMetadataHash}
|
||||
placeholder="https://github/my-org/my-repo/metadata.json"
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<div>
|
||||
Metadata is a JSON file that describes your package.
|
||||
<br /> You can{" "}
|
||||
<a onClick={() => setShowMetadataForm(true)}
|
||||
@ -317,7 +326,7 @@ export default function PublishPage({
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col mb-2">
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="metadata-hash">Metadata Hash</label>
|
||||
<input
|
||||
readOnly
|
||||
@ -334,7 +343,7 @@ export default function PublishPage({
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col my-2 mt-4">
|
||||
<div className="flex flex-col">
|
||||
<h4>Packages You Own</h4>
|
||||
{myPublishedApps.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
@ -344,11 +353,9 @@ export default function PublishPage({
|
||||
<Jazzicon address={app.publisher} className="mr-2" />
|
||||
<span>{app.package}</span>
|
||||
</div>
|
||||
{/* <Tooltip content="View Package"> */}
|
||||
<button className="flex items-center" onClick={() => unpublishPackage(app.package, app.publisher)}>
|
||||
<span>Unpublish</span>
|
||||
</button>
|
||||
{/* </Tooltip> */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -9,6 +9,8 @@ import { PageProps } from "../types/Page";
|
||||
import { appId } from "../utils/app";
|
||||
import classNames from 'classnames';
|
||||
import { FaArrowRotateRight } from "react-icons/fa6";
|
||||
import { isMobileCheck } from "../utils/dimensions";
|
||||
import HomeButton from "../components/HomeButton";
|
||||
|
||||
interface StorePageProps extends PageProps { }
|
||||
|
||||
@ -21,6 +23,7 @@ export default function StorePage() {
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [displayedApps, setDisplayedApps] = useState<AppInfo[]>(listedApps);
|
||||
const [page, setPage] = useState(1);
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
|
||||
const pages = useMemo(
|
||||
() =>
|
||||
@ -42,6 +45,14 @@ export default function StorePage() {
|
||||
getListedApps()
|
||||
.then((apps) => {
|
||||
setDisplayedApps(Object.values(apps));
|
||||
let _tags: string[] = [];
|
||||
for (const app of Object.values(apps)) {
|
||||
_tags = _tags.concat((app.metadata as any || {}).tags || [])
|
||||
}
|
||||
if (_tags.length === 0) {
|
||||
_tags = ['App', 'Tags', 'Coming', 'Soon', 'tm'];
|
||||
}
|
||||
setTags(Array.from(new Set(_tags)))
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
}, []); // eslint-disable-line
|
||||
@ -112,25 +123,48 @@ export default function StorePage() {
|
||||
}
|
||||
}, [rebuildIndex]);
|
||||
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return (
|
||||
<div className="max-w-[900px] w-full">
|
||||
<div className={classNames("flex flex-col w-full max-h-screen", {
|
||||
'gap-4 max-w-screen p-2': isMobile,
|
||||
'gap-6 max-w-[900px]': !isMobile
|
||||
})}>
|
||||
{!isMobile && <HomeButton />}
|
||||
<SearchHeader value={searchQuery} onChange={searchApps} />
|
||||
<div className="flex justify-between items-center my-2 mx-0">
|
||||
<h4>New</h4>
|
||||
<div className={classNames("flex items-center self-stretch justify-between", {
|
||||
'gap-4 flex-wrap': isMobile,
|
||||
'gap-8 grow': !isMobile
|
||||
})}>
|
||||
<button
|
||||
className="flex flex-col c mr-auto ml-1 icon"
|
||||
className="flex flex-col c icon icon-orange"
|
||||
onClick={tryRebuildIndex}
|
||||
title="Rebuild index"
|
||||
>
|
||||
<FaArrowRotateRight />
|
||||
</button>
|
||||
|
||||
{tags.slice(0, isMobile ? 3 : 6).map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
className="clear flex c rounded-full !bg-white/10 !hover:bg-white/25"
|
||||
onClick={() => {
|
||||
console.log('clicked tag', tag)
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<select
|
||||
value={resultsSort}
|
||||
onChange={(e) => {
|
||||
setResultsSort(e.target.value);
|
||||
sortApps(e.target.value);
|
||||
}}
|
||||
className={classNames({
|
||||
'basis-1/5': !isMobile
|
||||
})}
|
||||
>
|
||||
<option>Recently published</option>
|
||||
<option>Most popular</option>
|
||||
@ -138,13 +172,54 @@ export default function StorePage() {
|
||||
<option>Recently updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 overflow-y-auto gap-2 max-h-[80vh]">
|
||||
{displayedApps.map((app) => (
|
||||
{!searchQuery ? <div className={classNames("flex flex-col", {
|
||||
'grow overflow-y-auto gap-4 items-center px-2': isMobile
|
||||
})}>
|
||||
<h2>Top apps this week...</h2>
|
||||
<div className={classNames("flex gap-2", {
|
||||
'flex-col': isMobile
|
||||
})}>
|
||||
{displayedApps.slice(0, 4).map((app) => (
|
||||
<AppEntry
|
||||
key={appId(app) + (app.state?.our_version || "")}
|
||||
size={'medium'}
|
||||
app={app}
|
||||
className={classNames("grow", {
|
||||
'w-1/4': !isMobile,
|
||||
'w-full': isMobile
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<h2>Must-have apps!</h2>
|
||||
<div className={classNames("flex gap-2", {
|
||||
'flex-col': isMobile
|
||||
})}>
|
||||
{displayedApps.slice(0, 6).map((app) => (
|
||||
<AppEntry
|
||||
key={appId(app) + (app.state?.our_version || "")}
|
||||
size={isMobile ? 'medium' : 'small'}
|
||||
app={app}
|
||||
overrideImageSize={isMobile ? 'medium' : 'large'}
|
||||
className={classNames("grow", {
|
||||
'w-1/6': !isMobile,
|
||||
'w-full': isMobile
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div> : <div className={classNames("flex-col-center grow", {
|
||||
'gap-2': isMobile,
|
||||
'gap-4': !isMobile,
|
||||
})}>
|
||||
{displayedApps.map(app => <AppEntry
|
||||
size='large'
|
||||
app={app}
|
||||
className="self-stretch items-center"
|
||||
overrideImageSize="medium"
|
||||
/>)}
|
||||
</div>}
|
||||
<div className="flex flex-col gap-2 overflow-y-auto">
|
||||
{pages.length > 1 && (
|
||||
<div className="flex self-center">
|
||||
{page !== pages[0] && (
|
||||
|
@ -14,7 +14,7 @@ export enum AppType {
|
||||
export const getAppType = (app: AppInfo) => {
|
||||
if (app.publisher === 'sys') {
|
||||
return AppType.System
|
||||
} else if (app.state?.our_version && !app.state?.capsApproved) {
|
||||
} else if (app.state?.our_version && !app.state?.caps_approved) {
|
||||
return AppType.Downloaded
|
||||
} else if (!app.metadata) {
|
||||
return AppType.Local
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -9,8 +9,8 @@
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<script type="module" crossorigin src="/assets/index-CiPfZ2kc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C9o9YkgK.css">
|
||||
<script type="module" crossorigin src="/assets/index-BrbxaEm2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-iMQiSiXv.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
4
kinode/packages/homepage/ui/dist/index.html
vendored
4
kinode/packages/homepage/ui/dist/index.html
vendored
@ -9,8 +9,8 @@
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<script type="module" crossorigin src="/assets/index-CiPfZ2kc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C9o9YkgK.css">
|
||||
<script type="module" crossorigin src="/assets/index-BrbxaEm2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-iMQiSiXv.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"name": "kinode-homepage-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
@ -7,10 +7,11 @@ const AllApps: React.FC<{ expanded: boolean }> = ({ expanded }) => {
|
||||
const { apps } = useHomepageStore()
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return <div className={classNames('flex-center flex-wrap gap-4 overflow-y-auto self-stretch relative', {
|
||||
'max-h-0': !expanded,
|
||||
'p-8 max-h-[1000px]': expanded,
|
||||
'placeholder': isMobile
|
||||
return <div className={classNames('flex-center flex-wrap overflow-y-auto fixed h-screen w-screen backdrop-blur-md transition transition-all ease-in-out duration-500', {
|
||||
'top-[100vh]': !expanded,
|
||||
'top-0': expanded,
|
||||
'gap-4 p-8': isMobile,
|
||||
'gap-8 p-16': !isMobile,
|
||||
})}>
|
||||
{apps.length === 0
|
||||
? <div>Loading apps...</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import KinodeText from '../components/KinodeText'
|
||||
import KinodeBird from '../components/KinodeBird'
|
||||
import useHomepageStore from '../store/homepageStore'
|
||||
import useHomepageStore, { HomepageApp } from '../store/homepageStore'
|
||||
import { FaChevronDown, FaChevronUp, FaScrewdriverWrench, FaV } from 'react-icons/fa6'
|
||||
import AppsDock from '../components/AppsDock'
|
||||
import AllApps from '../components/AllApps'
|
||||
@ -20,37 +20,56 @@ interface AppStoreApp {
|
||||
function Homepage() {
|
||||
const [our, setOur] = useState('')
|
||||
const [allAppsExpanded, setAllAppsExpanded] = useState(false)
|
||||
const { apps, setApps, isHosted, fetchHostedStatus, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
|
||||
const { setApps, isHosted, fetchHostedStatus, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/apps')
|
||||
.then(res => res.json())
|
||||
.then(data => setApps(data))
|
||||
.then(() => {
|
||||
fetch('/main:app_store:sys/apps')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const appz = [...apps]
|
||||
data.forEach((app: AppStoreApp) => {
|
||||
if (!appz.find(a => a.package_name === app.package)) {
|
||||
const getAppPathsAndIcons = () => {
|
||||
Promise.all([
|
||||
fetch('/apps').then(res => res.json() as any as HomepageApp[]),
|
||||
fetch('/main:app_store:sys/apps').then(res => res.json())
|
||||
]).then(([appsData, appStoreData]) => {
|
||||
console.log({ appsData, appStoreData })
|
||||
const appz = appsData.map(app => ({
|
||||
...app,
|
||||
is_favorite: false, // Assuming initial state for all apps
|
||||
}));
|
||||
|
||||
appStoreData.forEach((appStoreApp: AppStoreApp) => {
|
||||
const existingAppIndex = appz.findIndex(a => a.package_name === appStoreApp.package);
|
||||
if (existingAppIndex === -1) {
|
||||
appz.push({
|
||||
package_name: app.package,
|
||||
package_name: appStoreApp.package,
|
||||
path: '',
|
||||
label: app.package,
|
||||
state: app.state,
|
||||
label: appStoreApp.package,
|
||||
state: appStoreApp.state,
|
||||
is_favorite: false
|
||||
})
|
||||
});
|
||||
} else {
|
||||
const i = appz.findIndex(a => a.package_name === app.package)
|
||||
if (i !== -1) {
|
||||
appz[i] = { ...appz[i], state: app.state }
|
||||
appz[existingAppIndex] = {
|
||||
...appz[existingAppIndex],
|
||||
state: appStoreApp.state
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
setApps(appz);
|
||||
|
||||
// TODO: be less dumb about this edge case!
|
||||
for (
|
||||
let i = 0;
|
||||
i < 5 && appz.find(a => a.package_name === 'app_store' && !a.base64_icon);
|
||||
i++
|
||||
) {
|
||||
getAppPathsAndIcons();
|
||||
}
|
||||
})
|
||||
setApps(appz)
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAppPathsAndIcons();
|
||||
}, [our]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/our')
|
||||
.then(res => res.text())
|
||||
.then(data => {
|
||||
@ -95,13 +114,14 @@ function Homepage() {
|
||||
<AppsDock />
|
||||
<Widgets />
|
||||
<button
|
||||
className={classNames("clear flex-center self-center", {
|
||||
'-mb-1': !allAppsExpanded,
|
||||
className={classNames("fixed alt clear flex-center self-center z-20", {
|
||||
'bottom-2 right-2': isMobile,
|
||||
'bottom-8 right-8': !isMobile,
|
||||
})}
|
||||
onClick={() => setAllAppsExpanded(!allAppsExpanded)}
|
||||
>
|
||||
{allAppsExpanded ? <FaChevronDown /> : <FaChevronUp />}
|
||||
<span className="ml-2">{allAppsExpanded ? 'Collapse' : 'All installed apps'}</span>
|
||||
<span className="ml-2">{allAppsExpanded ? 'Collapse' : 'All apps'}</span>
|
||||
</button>
|
||||
<AllApps expanded={allAppsExpanded} />
|
||||
{showWidgetsSettings && <WidgetsSettingsModal />}
|
||||
|
@ -1141,88 +1141,18 @@ Constrain images and videos to the parent width and preserve their intrinsic asp
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body><noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<body
|
||||
style="background-color: #22211f;
|
||||
background-image: linear-gradient(-55deg, #f75a2977 0%, transparent 72.05%),linear-gradient(-11deg, #86000172 3%, transparent 57.05%);">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<svg width="1440" height="1024" viewBox="0 0 1440 1024" fill="none" xmlns="http://www.w3.org/2000/svg" style="position: fixed;
|
||||
top: -300px;
|
||||
left: -350px;
|
||||
z-index: -1;
|
||||
filter: blur(100px);
|
||||
transform: scale(3);">
|
||||
<g clip-path="url(#clip0_152_8506)">
|
||||
<rect width="1440" height="2777" transform="translate(0 -5)" fill="#22211F" />
|
||||
<path
|
||||
d="M2122.93 951.101C1925.12 1458.86 1658.87 2101.89 1376.85 1992.03C601.547 2045.26 -152.056 1158.27 434.949 891.737C1021.95 625.205 1605.89 -327.704 1970.46 -167.208C2361.3 4.85831 2320.74 443.343 2122.93 951.101Z"
|
||||
fill="url(#paint0_radial_152_8506)" />
|
||||
<path
|
||||
d="M274.686 1388.42C15.0266 1578.85 -501.874 1262.71 -695.499 998.693C-889.123 734.676 -935.603 448.185 -675.943 257.757C-416.284 67.3282 500.306 639.9 693.93 903.918C887.555 1167.94 534.346 1197.99 274.686 1388.42Z"
|
||||
fill="url(#paint1_linear_152_8506)" />
|
||||
<path
|
||||
d="M673.921 -604.363C829.107 -41.0363 308.158 505 -81.9719 505C-472.102 505 -788.365 72.0217 -788.365 -462.084C-788.365 -996.189 -1002.49 -1551 -612.361 -1551C-222.231 -1551 673.921 -1138.47 673.921 -604.363Z"
|
||||
fill="#F75A29" fill-opacity="0.7" />
|
||||
<g filter="url(#filter0_b_152_8506)">
|
||||
<rect width="1464" height="2823" transform="translate(0 -5)" fill="#22211F" fill-opacity="0.01" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_b_152_8506" x="-314" y="-319" width="2092" height="3451" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="157" />
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_152_8506" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_152_8506" result="shape" />
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_152_8506" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(1444 1488.65) rotate(-86.9561) scale(699.105 590.175)">
|
||||
<stop offset="0.635" stop-color="#F35422" stop-opacity="0.7" />
|
||||
<stop offset="1" stop-color="#E60D16" stop-opacity="0.35" />
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_152_8506" x1="-389.52" y1="620.673" x2="747.127" y2="934.639"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F35422" />
|
||||
<stop offset="1" stop-color="#E60D16" stop-opacity="0.1" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_152_8506">
|
||||
<rect width="1440" height="2777" fill="white" transform="translate(0 -5)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div id="signup-page" class="flex flex-col place-items-center place-content-center h-screen font-sans">
|
||||
<div id="home-form-header" class="flex flex-col place-content-center place-items-center"
|
||||
style="gap: 24px; margin: 32px 0 56px;">
|
||||
<h3 style="text-align: center;">Login to</h3>
|
||||
<svg width="289.5" height="36" viewBox="0 0 580 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_641)">
|
||||
<path d="M0.824922 1.07031L0.794922 70.0703H14.7949L14.8049 1.07031H0.824922Z" fill="#FFF5D9"></path>
|
||||
<path d="M16.5947 36.8803L41.2547 1.07031H58.2447L33.1647 36.8803L61.2447 70.0703H42.9947L16.5947 36.8803Z"
|
||||
fill="#FFF5D9"></path>
|
||||
<path d="M119.885 1.07031H105.765V70.0703H119.885V1.07031Z" fill="#FFF5D9"></path>
|
||||
<path
|
||||
d="M173.185 1.07031V70.0703H186.775V26.8303L224.045 70.0703H234.825V1.07031H221.325V45.6803L183.445 1.07031H173.185Z"
|
||||
fill="#FFF5D9"></path>
|
||||
<path
|
||||
d="M342.465 8.86C333.025 0.15 321.645 0 318.535 0C315.475 0 303.575 0.22 294.005 9.52C283.845 19.4 283.805 32.24 283.795 35.66C283.785 39.3 283.895 49.03 290.805 57.99C300.855 71.02 316.695 71.31 318.535 71.32C321.375 71.32 334.185 71 343.965 60.66C353.065 51.04 353.265 39.4 353.275 35.66C353.275 32.49 353.305 18.86 342.455 8.86H342.465ZM318.435 58.01C307.095 58.01 297.895 47.95 297.895 35.54C297.895 23.13 307.085 13.07 318.435 13.07C329.785 13.07 338.975 23.13 338.975 35.54C338.975 47.95 329.785 58.01 318.435 58.01Z"
|
||||
fill="#FFF5D9"></path>
|
||||
<path
|
||||
d="M450.495 12.0802C444.975 5.46023 437.135 0.990234 427.955 0.990234C417.555 0.990234 405.295 1.07023 402.295 1.07023V69.9802C405.285 69.9802 417.555 70.0602 427.955 70.0602C445.525 70.0602 458.445 53.4102 459.065 36.8602C459.395 28.0102 456.185 18.9002 450.495 12.0802ZM440.085 49.9502C436.895 53.8702 432.705 56.6902 427.665 57.5602C424.025 58.1902 420.095 57.8302 416.405 57.8302C416.405 50.4002 416.405 42.9802 416.405 35.5502V13.2202C423.795 13.2202 430.525 12.7002 436.605 17.6002C440.275 20.5602 442.925 24.7102 444.165 29.2402C444.525 30.5402 444.765 31.8802 444.875 33.2302C445.395 39.3702 443.995 45.1402 440.085 49.9502Z"
|
||||
fill="#FFF5D9"></path>
|
||||
<path
|
||||
d="M508.135 0.990234V70.0602H552.715V57.9302H522.035V40.4202H547.125V28.0702H521.995V13.3202H552.715V0.990234H508.135Z"
|
||||
fill="#FFF5D9"></path>
|
||||
<path
|
||||
d="M574.835 66.0398H572.745L571.015 63.0698H569.845V66.0398H567.805V57.5498H571.765C572.845 57.5498 573.865 57.9298 574.425 58.9398C575.205 60.3698 574.665 62.3798 573.105 63.0298C573.725 64.1198 574.225 64.9498 574.845 66.0398H574.835ZM570.375 61.0798H570.845C571.335 61.0798 572.365 61.0798 572.365 60.2898C572.365 59.5598 571.335 59.5598 570.845 59.5598H570.375V61.0798Z"
|
||||
fill="#FFF5D9"></path>
|
||||
<path
|
||||
d="M570.964 69.0002C574.913 69.0002 578.114 65.799 578.114 61.8502C578.114 57.9014 574.913 54.7002 570.964 54.7002C567.016 54.7002 563.814 57.9014 563.814 61.8502C563.814 65.799 567.016 69.0002 570.964 69.0002Z"
|
||||
stroke="#FFF5D9" stroke-width="2.2" stroke-miterlimit="10"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6_641">
|
||||
<rect width="578.41" height="71.32" fill="white" transform="translate(0.794922)"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<h1 style="font-family: 'Futura', sans-serif; letter-spacing: 0.4em; font-size: xxx-large; margin-left: 1em;">
|
||||
KINODE<span style="font-size: small">®</span>
|
||||
</h1>
|
||||
<svg style="margin-top: 0.5em;" width="67.5" height="48" viewBox="0 0 122 81" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_651)">
|
||||
@ -1240,7 +1170,7 @@ Constrain images and videos to the parent width and preserve their intrinsic asp
|
||||
<form id="signup-form" class="flex flex-col">
|
||||
<h3 class="flex flex-col ml-2 text-lg"> ${node} </h3>
|
||||
<div class="flex mt-2 mb-2"> Enter Password </div>
|
||||
<input type="password" id="password" required="" minlength="6" name="password" placeholder="Password"
|
||||
<input autofocus type="password" id="password" required="" minlength="6" name="password" placeholder="Password"
|
||||
oninput="document.getElementById('password-err').style.display = 'none';" value="" class="self-stretch mb-2">
|
||||
<div id="password-err" class="login-row flex mb-2" style="display: none;"> Incorrect Password </div>
|
||||
<div class="flex flex-col leading-6 self-stretch mb-2">
|
||||
@ -1260,7 +1190,7 @@ Constrain images and videos to the parent width and preserve their intrinsic asp
|
||||
if ('${fake}' === 'true') {
|
||||
document.getElementById("fake-or-not").innerHTML = "Fake node -- any password will work!";
|
||||
} else {
|
||||
document.getElementById("fake-or-not").innerHTML = "Restart node to change networking info";
|
||||
document.getElementById("fake-or-not").innerHTML = "To change your networking info, please restart your node.";
|
||||
}
|
||||
|
||||
async function login(password) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "register",
|
||||
"name": "kinode-register-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:8080",
|
||||
|
Loading…
Reference in New Issue
Block a user