mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 08:32:23 +03:00
show all installed packages on homepage
This commit is contained in:
parent
078a898053
commit
e1bc001d1b
File diff suppressed because one or more lines are too long
@ -14,8 +14,7 @@
|
||||
<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" />
|
||||
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-mfHVbCij.js"></script>
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-BW3CPqe4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-McZbj3o1.css">
|
||||
</head>
|
||||
|
||||
|
@ -14,7 +14,6 @@
|
||||
<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" />
|
||||
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import { ethers } from "ethers";
|
||||
import { Web3ReactProvider, Web3ReactHooks } from '@web3-react/core';
|
||||
import type { MetaMask } from '@web3-react/metamask'
|
||||
|
||||
@ -8,7 +7,7 @@ import { PackageStore, PackageStore__factory } from "./abis/types";
|
||||
import StorePage from "./pages/StorePage";
|
||||
import MyAppsPage from "./pages/MyAppsPage";
|
||||
import AppPage from "./pages/AppPage";
|
||||
import { MY_APPS_PATH } from "./constants/path";
|
||||
import { APP_DETAILS_PATH, MY_APPS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
|
||||
import { ChainId, PACKAGE_STORE_ADDRESSES } from "./constants/chain";
|
||||
import PublishPage from "./pages/PublishPage";
|
||||
import { hooks as metaMaskHooks, metaMask } from './utils/metamask'
|
||||
@ -115,10 +114,10 @@ function App() {
|
||||
<Web3ReactProvider connectors={connectors}>
|
||||
<Router basename={BASE_URL}>
|
||||
<Routes>
|
||||
<Route path="/" element={<StorePage />} />
|
||||
<Route path={STORE_PATH} element={<StorePage />} />
|
||||
<Route path={MY_APPS_PATH} element={<MyAppsPage />} />
|
||||
<Route path="/app-details/:id" element={<AppPage />} />
|
||||
<Route path="/publish" element={<PublishPage {...props} />} />
|
||||
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
|
||||
<Route path={PUBLISH_PATH} element={<PublishPage {...props} />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Web3ReactProvider>
|
||||
|
@ -3,7 +3,7 @@ import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { FaCircleQuestion } from "react-icons/fa6";
|
||||
import { APP_DETAILS_PATH } from "../constants/path";
|
||||
|
||||
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
@ -21,7 +21,7 @@ export default function AppHeader({
|
||||
<div
|
||||
{...props}
|
||||
className={classNames('flex w-full justify-content-start', size, props.className, { 'cursor-pointer': size !== 'large' })}
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
||||
>
|
||||
{app.metadata?.image && <img
|
||||
src={app.metadata.image}
|
||||
|
@ -5,6 +5,7 @@ import Dropdown from "./Dropdown";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import { APP_DETAILS_PATH } from "../constants/path";
|
||||
|
||||
interface MoreActionsProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
@ -26,7 +27,7 @@ export default function MoreActions({ app, className }: MoreActionsProps) {
|
||||
{app.metadata?.description && (
|
||||
<button
|
||||
className="my-1 whitespace-nowrap clear"
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
@ -50,7 +51,7 @@ export default function MoreActions({ app, className }: MoreActionsProps) {
|
||||
<div className="flex flex-col bg-black/50 p-2 rounded-lg">
|
||||
<button
|
||||
className="my-1 whitespace-nowrap clear"
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
FaX,
|
||||
} from "react-icons/fa6";
|
||||
|
||||
import { MY_APPS_PATH } from "../constants/path";
|
||||
import { MY_APPS_PATH, PUBLISH_PATH } from "../constants/path";
|
||||
import classNames from "classnames";
|
||||
import { FaHome } from "react-icons/fa";
|
||||
|
||||
@ -57,7 +57,7 @@ export default function SearchHeader({
|
||||
)}
|
||||
{!hidePublish && <button
|
||||
className="flex flex-col c mr-2 icon"
|
||||
onClick={() => navigate("/publish")}
|
||||
onClick={() => navigate(PUBLISH_PATH)}
|
||||
>
|
||||
<FaUpload />
|
||||
</button>}
|
||||
|
@ -1 +1,4 @@
|
||||
export const MY_APPS_PATH = '/my-apps';
|
||||
export const MY_APPS_PATH = '/';
|
||||
export const STORE_PATH = '/store';
|
||||
export const PUBLISH_PATH = '/publish';
|
||||
export const APP_DETAILS_PATH = '/app-details';
|
||||
|
@ -8,6 +8,7 @@ import AppHeader from "../components/AppHeader";
|
||||
import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { appId } from "../utils/app";
|
||||
import { PUBLISH_PATH } from "../constants/path";
|
||||
|
||||
interface AppPageProps extends PageProps { }
|
||||
|
||||
@ -35,7 +36,7 @@ export default function AppPage() {
|
||||
}, [params.id]);
|
||||
|
||||
const goToPublish = useCallback(() => {
|
||||
navigate("/publish", { state: { app } });
|
||||
navigate(PUBLISH_PATH, { state: { app } });
|
||||
}, [app, navigate]);
|
||||
|
||||
const version = useMemo(
|
||||
|
@ -8,8 +8,7 @@ import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { appId } from "../utils/app";
|
||||
|
||||
interface MyAppsPageProps extends PageProps { }
|
||||
import { PUBLISH_PATH } from "../constants/path";
|
||||
|
||||
export default function MyAppsPage() { // eslint-disable-line
|
||||
const { myApps, getMyApps } = useAppsStore()
|
||||
@ -57,7 +56,7 @@ export default function MyAppsPage() { // eslint-disable-line
|
||||
<SearchHeader value={searchQuery} onChange={searchMyApps} />
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<h4 className="mb-2">My Packages</h4>
|
||||
<button onClick={() => navigate('/publish')}>
|
||||
<button onClick={() => navigate(PUBLISH_PATH)}>
|
||||
<FaUpload className="mr-2" />
|
||||
Publish Package
|
||||
</button>
|
||||
|
@ -80,13 +80,6 @@ export default function StorePage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// const viewDetails = useCallback(
|
||||
// (app: AppInfo) => () => {
|
||||
// navigate(`/app-details/${appId(app)}`);
|
||||
// },
|
||||
// [navigate]
|
||||
// );
|
||||
|
||||
const searchApps = useCallback(
|
||||
(query: string) => {
|
||||
setSearchQuery(query);
|
||||
|
@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// "strict": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"../src",
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -5,10 +5,10 @@ use kinode_process_lib::{
|
||||
bind_http_path, bind_http_static_path, send_response, serve_ui, HttpServerError,
|
||||
HttpServerRequest, StatusCode,
|
||||
},
|
||||
println, Address, Message,
|
||||
println, Address, Message, ProcessId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
/// The request format to add or remove an app from the homepage. You must have messaging
|
||||
/// access to `homepage:homepage:sys` in order to perform this. Serialize using serde_json.
|
||||
@ -21,6 +21,7 @@ enum HomepageRequest {
|
||||
label: String,
|
||||
icon: String,
|
||||
path: String,
|
||||
widget: Option<String>,
|
||||
},
|
||||
Remove,
|
||||
}
|
||||
@ -31,6 +32,17 @@ struct HomepageApp {
|
||||
path: String,
|
||||
label: String,
|
||||
base64_icon: String,
|
||||
is_favorite: bool,
|
||||
widget: Option<String>,
|
||||
}
|
||||
|
||||
fn get_package_id(url_params: &HashMap<String, String>) -> anyhow::Result<ProcessId> {
|
||||
let Some(package_id) = url_params.get("id") else {
|
||||
return Err(anyhow::anyhow!("Missing id"));
|
||||
};
|
||||
|
||||
let id = package_id.parse::<ProcessId>()?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
wit_bindgen::generate!({
|
||||
@ -40,7 +52,7 @@ wit_bindgen::generate!({
|
||||
|
||||
call_init!(init);
|
||||
fn init(our: Address) {
|
||||
let mut app_data: BTreeMap<ProcessId, HomepageApp> = BTreeMap::new();
|
||||
let mut app_data: BTreeMap<String, HomepageApp> = BTreeMap::new();
|
||||
|
||||
serve_ui(&our, "ui", true, false, vec!["/"]).expect("failed to serve ui");
|
||||
|
||||
@ -65,6 +77,7 @@ fn init(our: Address) {
|
||||
.expect("failed to bind to /our.js");
|
||||
|
||||
bind_http_path("/apps", true, false).expect("failed to bind /apps");
|
||||
bind_http_path("/apps/:id/favorite", true, false).expect("failed to bind /apps/:id/favorite");
|
||||
|
||||
loop {
|
||||
let Ok(ref message) = await_message() else {
|
||||
@ -84,7 +97,12 @@ fn init(our: Address) {
|
||||
// they must have messaging access to us in order to perform this.
|
||||
if let Ok(request) = serde_json::from_slice::<HomepageRequest>(message.body()) {
|
||||
match request {
|
||||
HomepageRequest::Add { label, icon, path } => {
|
||||
HomepageRequest::Add {
|
||||
label,
|
||||
icon,
|
||||
path,
|
||||
widget,
|
||||
} => {
|
||||
app_data.insert(
|
||||
message.source().process.to_string(),
|
||||
HomepageApp {
|
||||
@ -98,22 +116,24 @@ fn init(our: Address) {
|
||||
),
|
||||
label: label.clone(),
|
||||
base64_icon: icon.clone(),
|
||||
is_favorite: false,
|
||||
widget: widget.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
HomepageRequest::Remove => {
|
||||
app_data.remove(&message.source().process);
|
||||
app_data.remove(&message.source().process.to_string());
|
||||
}
|
||||
}
|
||||
} else if let Ok(request) = serde_json::from_slice::<HttpServerRequest>(message.body())
|
||||
{
|
||||
match request {
|
||||
} else if let Ok(req) = serde_json::from_slice::<HttpServerRequest>(message.body()) {
|
||||
match req {
|
||||
HttpServerRequest::Http(incoming) => {
|
||||
let path = incoming.bound_path(None);
|
||||
if path == "/apps" {
|
||||
match path {
|
||||
"/apps" => {
|
||||
send_response(
|
||||
StatusCode::OK,
|
||||
Some(std::collections::HashMap::from([(
|
||||
Some(HashMap::from([(
|
||||
"Content-Type".to_string(),
|
||||
"application/json".to_string(),
|
||||
)])),
|
||||
@ -128,7 +148,32 @@ fn init(our: Address) {
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
} else {
|
||||
}
|
||||
"/apps/:id/favorite" => {
|
||||
let url_params = incoming.url_params();
|
||||
let Ok(app_id) = get_package_id(url_params) else {
|
||||
send_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
None,
|
||||
format!("Missing id").into_bytes(),
|
||||
);
|
||||
println!("failed to parse app id");
|
||||
for key in url_params {
|
||||
println!("{} {}", key.0, key.1);
|
||||
}
|
||||
println!("All application IDs currently stored:");
|
||||
for key in app_data.keys() {
|
||||
println!("{}", key);
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let app = app_data.get_mut(&app_id.to_string());
|
||||
if let Some(app) = app {
|
||||
app.is_favorite = !app.is_favorite;
|
||||
}
|
||||
send_response(StatusCode::CREATED, Some(HashMap::new()), vec![]);
|
||||
}
|
||||
_ => {
|
||||
send_response(
|
||||
StatusCode::OK,
|
||||
Some(HashMap::new()),
|
||||
@ -136,6 +181,7 @@ fn init(our: Address) {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
87
kinode/packages/homepage/pkg/ui/assets/index-BSV0lkgx.js
Normal file
87
kinode/packages/homepage/pkg/ui/assets/index-BSV0lkgx.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
@ -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-zsBPcPK6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-a1_1vktn.css">
|
||||
<script type="module" crossorigin src="/assets/index-BSV0lkgx.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-v9ePqnS0.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-zsBPcPK6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-a1_1vktn.css">
|
||||
<script type="module" crossorigin src="/assets/index-BSV0lkgx.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-v9ePqnS0.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,61 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import KinodeText from './components/KinodeText'
|
||||
import KinodeBird from './components/KinodeBird'
|
||||
import useHomepageStore from './store/homepageStore'
|
||||
import { FaV } from 'react-icons/fa6'
|
||||
|
||||
interface HomepageApp {
|
||||
package_name: string,
|
||||
path: string
|
||||
label: string,
|
||||
base64_icon: string,
|
||||
}
|
||||
function App() {
|
||||
const [our, setOur] = useState('')
|
||||
const [apps, setApps] = useState<HomepageApp[]>([])
|
||||
const { isHosted, fetchHostedStatus } = useHomepageStore()
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/apps')
|
||||
.then(res => res.json())
|
||||
.then(data => setApps(data))
|
||||
fetch('/our')
|
||||
.then(res => res.text())
|
||||
.then(data => setOur(data))
|
||||
.then(() => fetchHostedStatus(our))
|
||||
}, [our])
|
||||
|
||||
return (
|
||||
<div className="flex-col-center relative h-screen w-screen overflow-hidden special-bg-homepage">
|
||||
<h5 className='absolute top-8 left-8'>
|
||||
{isHosted && <a
|
||||
href={`https://${our.replace('.os', '')}.hosting.kinode.net/`}
|
||||
className='button icon'
|
||||
>
|
||||
<FaV />
|
||||
</a>}
|
||||
{our}
|
||||
</h5>
|
||||
<div className="flex-col-center gap-6 mt-8 mx-0 mb-16">
|
||||
<h3 className='text-center'>Welcome to</h3>
|
||||
<KinodeText />
|
||||
<KinodeBird />
|
||||
</div>
|
||||
<div className='flex-center flex-wrap gap-8'>
|
||||
{apps.length === 0 ? <div>Loading apps...</div> : apps.map(app => <a
|
||||
className="flex-col-center mb-8 cursor-pointer gap-2 hover:opacity-90"
|
||||
id={app.package_name}
|
||||
href={app.path}
|
||||
>
|
||||
<img
|
||||
src={app.base64_icon}
|
||||
className='h-32 w-32'
|
||||
/>
|
||||
<h6>{app.label}</h6>
|
||||
</a>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
14
kinode/packages/homepage/ui/src/components/AllApps.tsx
Normal file
14
kinode/packages/homepage/ui/src/components/AllApps.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { HomepageApp } from "../store/homepageStore"
|
||||
import AppDisplay from "./AppDisplay"
|
||||
|
||||
interface AllAppsProps {
|
||||
apps: HomepageApp[]
|
||||
}
|
||||
|
||||
const AllApps: React.FC<AllAppsProps> = ({ apps }) => {
|
||||
return <div className='flex-center flex-wrap gap-8 overflow-y-auto flex-grow self-stretch px-16'>
|
||||
{apps.length === 0 ? <div>Loading apps...</div> : apps.map(app => <AppDisplay app={app} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default AllApps
|
49
kinode/packages/homepage/ui/src/components/AppDisplay.tsx
Normal file
49
kinode/packages/homepage/ui/src/components/AppDisplay.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import classNames from "classnames"
|
||||
import ColorDot from "./HexNum"
|
||||
import useHomepageStore, { HomepageApp } from "../store/homepageStore"
|
||||
import { FaHeart, FaRegHeart } from "react-icons/fa6"
|
||||
import { useState } from "react"
|
||||
|
||||
interface AppDisplayProps {
|
||||
app: HomepageApp
|
||||
}
|
||||
|
||||
const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
|
||||
const { favoriteApp, apps, setApps } = useHomepageStore()
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const onAppFavorited = async (appId: string) => {
|
||||
const res = await favoriteApp(appId.replace('/', ''))
|
||||
if (res.status === 201) {
|
||||
setApps(apps.map(a => a.package_name === app.package_name ? { ...app, is_favorite: !app.is_favorite } : a))
|
||||
} else {
|
||||
alert('Something went wrong. Please try again.')
|
||||
}
|
||||
}
|
||||
return <a
|
||||
className={classNames("flex-col-center gap-2 relative hover:opacity-90", { 'cursor-pointer': app.path })}
|
||||
id={app.package_name}
|
||||
href={app.path}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{app.base64_icon
|
||||
? <img
|
||||
src={app.base64_icon}
|
||||
className='h-32 w-32 rounded'
|
||||
/>
|
||||
: <ColorDot num={app.state?.our_version || '0'} />}
|
||||
<h6>{app.label}</h6>
|
||||
{app.path && isHovered && <button
|
||||
className="absolute p-2 -top-2 -right-2 clear text-sm saturate-50"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onAppFavorited(app.path)
|
||||
}}
|
||||
>
|
||||
{app.is_favorite ? <FaHeart /> : <FaRegHeart />}
|
||||
</button>}
|
||||
</a>
|
||||
}
|
||||
|
||||
export default AppDisplay
|
||||
|
14
kinode/packages/homepage/ui/src/components/AppsDock.tsx
Normal file
14
kinode/packages/homepage/ui/src/components/AppsDock.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { HomepageApp } from "../store/homepageStore"
|
||||
import AppDisplay from "./AppDisplay"
|
||||
|
||||
interface AppsDockProps {
|
||||
apps: HomepageApp[]
|
||||
}
|
||||
|
||||
const AppsDock: React.FC<AppsDockProps> = ({ apps }) => {
|
||||
return <div className='flex-center flex-wrap obox border border-orange gap-8'>
|
||||
{apps.length === 0 ? <div>Favorite an app to pin it to your dock.</div> : apps.map(app => <AppDisplay app={app} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default AppsDock
|
44
kinode/packages/homepage/ui/src/components/HexNum.tsx
Normal file
44
kinode/packages/homepage/ui/src/components/HexNum.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../utils/colors'
|
||||
|
||||
interface ColorDotProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
num: string,
|
||||
}
|
||||
|
||||
const ColorDot: React.FC<ColorDotProps> = ({
|
||||
num,
|
||||
...props
|
||||
}) => {
|
||||
num = (num || '').replace(/(0x|\.)/g, '')
|
||||
|
||||
while (num.length < 6) {
|
||||
num = '0' + num
|
||||
}
|
||||
|
||||
const leftHsl = rgbToHsl(hexToRgb(num.slice(0, 6)))
|
||||
const rightHsl = rgbToHsl(hexToRgb(num.length > 6 ? num.slice(num.length - 6) : num))
|
||||
leftHsl.s = rightHsl.s = 1
|
||||
const leftColor = rgbToHex(hslToRgb(leftHsl))
|
||||
const rightColor = rgbToHex(hslToRgb(rightHsl))
|
||||
|
||||
const angle = (parseInt(num, 16) % 360) || -45
|
||||
|
||||
return (
|
||||
<div {...props} className={classNames('flex', props.className)}>
|
||||
<div
|
||||
className='m-0 align-self-center h-32 w-32 border border-8 rounded-full outline-black'
|
||||
style={{
|
||||
borderTopColor: leftColor,
|
||||
borderRightColor: rightColor,
|
||||
borderBottomColor: rightColor,
|
||||
borderLeftColor: leftColor,
|
||||
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
|
||||
filter: 'saturate(0.25)'
|
||||
}} />
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColorDot
|
@ -1,12 +1,19 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import Homepage from './pages/Homepage.tsx'
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import 'uno.css'
|
||||
import './index.css'
|
||||
import { Settings } from './pages/Settings.tsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path='/' element={<Homepage />} />
|
||||
<Route path='/settings' element={<Settings />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
86
kinode/packages/homepage/ui/src/pages/Homepage.tsx
Normal file
86
kinode/packages/homepage/ui/src/pages/Homepage.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import KinodeText from '../components/KinodeText'
|
||||
import KinodeBird from '../components/KinodeBird'
|
||||
import useHomepageStore from '../store/homepageStore'
|
||||
import { FaGear, FaV } from 'react-icons/fa6'
|
||||
import AppsDock from '../components/AppsDock'
|
||||
import AllApps from '../components/AllApps'
|
||||
|
||||
interface AppStoreApp {
|
||||
package: string,
|
||||
publisher: string,
|
||||
state: {
|
||||
our_version: string
|
||||
}
|
||||
}
|
||||
function Homepage() {
|
||||
const [our, setOur] = useState('')
|
||||
const { apps, setApps, isHosted, fetchHostedStatus } = useHomepageStore()
|
||||
|
||||
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)) {
|
||||
appz.push({
|
||||
package_name: app.package,
|
||||
path: '',
|
||||
label: app.package,
|
||||
state: app.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 }
|
||||
}
|
||||
}
|
||||
})
|
||||
setApps(appz)
|
||||
})
|
||||
})
|
||||
fetch('/our')
|
||||
.then(res => res.text())
|
||||
.then(data => {
|
||||
if (data.match(/^[a-zA-Z0-9\-\.]+\.[a-zA-Z]+$/)) {
|
||||
setOur(data)
|
||||
fetchHostedStatus(data)
|
||||
}
|
||||
})
|
||||
}, [our])
|
||||
|
||||
return (
|
||||
<div className="flex-col-center relative h-screen w-screen overflow-hidden special-bg-homepage">
|
||||
<h5 className='absolute top-8 left-8 right-8 flex gap-4 c'>
|
||||
{isHosted && <a
|
||||
href={`https://${our.replace('.os', '')}.hosting.kinode.net/`}
|
||||
className='button icon'
|
||||
>
|
||||
<FaV />
|
||||
</a>}
|
||||
{our}
|
||||
<a
|
||||
href='/settings:settings:sys'
|
||||
className='button icon ml-auto'
|
||||
>
|
||||
<FaGear />
|
||||
</a>
|
||||
</h5>
|
||||
<div className="flex-col-center gap-6 mt-8 mx-0 mb-16">
|
||||
<h3 className='text-center'>Welcome to</h3>
|
||||
<KinodeText />
|
||||
<KinodeBird />
|
||||
</div>
|
||||
<AppsDock apps={apps.filter(a => a.is_favorite)} />
|
||||
<AllApps apps={apps} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Homepage
|
3
kinode/packages/homepage/ui/src/pages/Settings.tsx
Normal file
3
kinode/packages/homepage/ui/src/pages/Settings.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const Settings = () => <div>
|
||||
Settings go here!
|
||||
</div>
|
@ -1,6 +1,16 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
|
||||
export interface HomepageApp {
|
||||
package_name: string,
|
||||
path: string
|
||||
label: string,
|
||||
base64_icon?: string,
|
||||
is_favorite: boolean,
|
||||
state?: {
|
||||
our_version: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface HomepageStore {
|
||||
get: () => HomepageStore
|
||||
@ -9,6 +19,10 @@ export interface HomepageStore {
|
||||
isHosted: boolean
|
||||
setIsHosted: (isHosted: boolean) => void
|
||||
fetchHostedStatus: (our: string) => Promise<void>
|
||||
favoriteApp: (appId: string) => Promise<Response>
|
||||
|
||||
apps: HomepageApp[]
|
||||
setApps: (apps: HomepageApp[]) => void
|
||||
}
|
||||
|
||||
const useHomepageStore = create<HomepageStore>()(
|
||||
@ -17,6 +31,8 @@ const useHomepageStore = create<HomepageStore>()(
|
||||
get,
|
||||
set,
|
||||
|
||||
apps: [],
|
||||
setApps: (apps: HomepageApp[]) => set({ apps }),
|
||||
isHosted: false,
|
||||
setIsHosted: (isHosted: boolean) => set({ isHosted }),
|
||||
fetchHostedStatus: async (our: string) => {
|
||||
@ -30,6 +46,7 @@ const useHomepageStore = create<HomepageStore>()(
|
||||
set({ isHosted: hosted })
|
||||
}
|
||||
},
|
||||
favoriteApp: async (appId: string) => await fetch(`/apps/${appId.replace('/', '')}/favorite`, { method: 'POST' }),
|
||||
}),
|
||||
{
|
||||
name: 'homepage_store', // unique name
|
||||
|
106
kinode/packages/homepage/ui/src/utils/colors.ts
Normal file
106
kinode/packages/homepage/ui/src/utils/colors.ts
Normal file
@ -0,0 +1,106 @@
|
||||
export type RgbType = { r: number, g: number, b: number }
|
||||
export type HslType = { h: number, s: number, l: number }
|
||||
|
||||
export const rgbToHsl: (rgb: RgbType) => HslType = ({ r, g, b }) => {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
let max = Math.max(r, g, b);
|
||||
let min = Math.min(r, g, b);
|
||||
let d = max - min;
|
||||
let h = 0;
|
||||
if (d === 0) h = 0;
|
||||
else if (max === r) h = ((((g - b) / d) % 6) + 6) % 6; // the javascript modulo operator handles negative numbers differently than most other languages
|
||||
else if (max === g) h = (b - r) / d + 2;
|
||||
else if (max === b) h = (r - g) / d + 4;
|
||||
let l = (min + max) / 2;
|
||||
let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
|
||||
return { h: h * 60, s, l };
|
||||
}
|
||||
|
||||
export const hslToRgb: (hsl: HslType) => RgbType = ({ h, s, l }) => {
|
||||
let c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
let hp = h / 60.0;
|
||||
let x = c * (1 - Math.abs((hp % 2) - 1));
|
||||
let rgb1 = [0, 0, 0];
|
||||
if (isNaN(h)) rgb1 = [0, 0, 0];
|
||||
else if (hp <= 1) rgb1 = [c, x, 0];
|
||||
else if (hp <= 2) rgb1 = [x, c, 0];
|
||||
else if (hp <= 3) rgb1 = [0, c, x];
|
||||
else if (hp <= 4) rgb1 = [0, x, c];
|
||||
else if (hp <= 5) rgb1 = [x, 0, c];
|
||||
else if (hp <= 6) rgb1 = [c, 0, x];
|
||||
let m = l - c * 0.5;
|
||||
return {
|
||||
r: Math.round(255 * (rgb1[0] + m)),
|
||||
g: Math.round(255 * (rgb1[1] + m)),
|
||||
b: Math.round(255 * (rgb1[2] + m))
|
||||
};
|
||||
}
|
||||
|
||||
export const hexToRgb: (hex: string) => RgbType = (hex) => {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 0, g: 0, b: 0 };
|
||||
}
|
||||
|
||||
export const rgbToHex: (rgb: RgbType) => string = ({ r, g, b }) => {
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
export const generateDistinguishableColors = (numColors: number, mod?: number, sat?: number, val?: number) => {
|
||||
const colors: string[] = [];
|
||||
const saturation = sat || 0.75;
|
||||
const value = val || 0.75;
|
||||
|
||||
for (let i = 0; i < numColors; i++) {
|
||||
const hue = i / numColors * (+(mod || 1) || 1);
|
||||
const [r, g, b] = hsvToRgb(hue, saturation, value);
|
||||
const hexColor = rgbToHex2(r, g, b);
|
||||
colors.push(hexColor);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
export const hsvToRgb = (h: number, s: number, v: number) => {
|
||||
let [r, g, b] = [0, 0, 0];
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
(r = v), (g = t), (b = p); // eslint-disable-line
|
||||
break;
|
||||
case 1:
|
||||
(r = q), (g = v), (b = p); // eslint-disable-line
|
||||
break;
|
||||
case 2:
|
||||
(r = p), (g = v), (b = t); // eslint-disable-line
|
||||
break;
|
||||
case 3:
|
||||
(r = p), (g = q), (b = v); // eslint-disable-line
|
||||
break;
|
||||
case 4:
|
||||
(r = t), (g = p), (b = v); // eslint-disable-line
|
||||
break;
|
||||
case 5:
|
||||
(r = v), (g = p), (b = q); // eslint-disable-line
|
||||
break;
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
export const componentToHex = (c: number) => {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
export const rgbToHex2 = (r: number, g: number, b: number) => {
|
||||
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user