Add settings and peers integration and css nesting

This commit is contained in:
Arnaud 2024-10-30 19:29:14 +01:00
parent f2e3ed6958
commit bc1da83d03
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
66 changed files with 2219 additions and 1924 deletions

193
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.7",
"license": "MIT",
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.27",
"@codex-storage/marketplace-ui-components": "^0.0.28",
"@codex-storage/sdk-js": "^0.0.12",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",
@ -17,6 +17,7 @@
"@tanstack/react-router": "^1.58.7",
"dotted-map": "^2.2.3",
"echarts": "^5.5.1",
"emoji-picker-react": "^4.12.0",
"idb-keyval": "^6.2.1",
"lucide-react": "^0.445.0",
"react": "^18.3.1",
@ -36,8 +37,9 @@
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.12",
"postcss": "^8.4.47",
"postcss-nesting": "^13.0.1",
"prettier": "^3.3.3",
"sass-embedded": "^1.79.4",
"typescript": "5.5.4",
"vite": "^5.4.7"
},
@ -371,12 +373,14 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.1.0.tgz",
"integrity": "sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==",
"dev": true
"dev": true,
"optional": true,
"peer": true
},
"node_modules/@codex-storage/marketplace-ui-components": {
"version": "0.0.27",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.27.tgz",
"integrity": "sha512-jCNUFeHNUptKSupSCQriwTXjK0zC8Yi7kcVWI20p9GtfNKPMRykhrpqGgQ6AdMMB9ZAliY5m+PH9ie3J/PWicw==",
"version": "0.0.28",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.28.tgz",
"integrity": "sha512-C2Wj6Rb4RzdkXRtXsj+dnzP5NwnJbmlHfQ1R2jxqgaJ4DXBDnAnEGiiljHzHE2oP0P4Exx7CQBjrP7vWChMpNg==",
"dependencies": {
"lucide-react": "^0.453.0"
},
@ -385,6 +389,7 @@
},
"peerDependencies": {
"@codex-storage/sdk-js": ">=0.0.12",
"postcss-nesting": "^13.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
@ -408,6 +413,48 @@
"node": ">=20"
}
},
"node_modules/@csstools/selector-resolve-nested": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz",
"integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
},
"peerDependencies": {
"postcss-selector-parser": "^7.0.0"
}
},
"node_modules/@csstools/selector-specificity": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz",
"integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
},
"peerDependencies": {
"postcss-selector-parser": "^7.0.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"dev": true,
@ -2101,7 +2148,9 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"dev": true
"dev": true,
"optional": true,
"peer": true
},
"node_modules/callsites": {
"version": "3.1.0",
@ -2209,7 +2258,9 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"dev": true
"dev": true,
"optional": true,
"peer": true
},
"node_modules/concat-map": {
"version": "0.0.1",
@ -2234,6 +2285,17 @@
"node": ">= 8"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"dev": true,
@ -2312,6 +2374,20 @@
"integrity": "sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==",
"dev": true
},
"node_modules/emoji-picker-react": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.0.tgz",
"integrity": "sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==",
"dependencies": {
"flairup": "1.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@ -2680,6 +2756,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flairup": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz",
"integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA=="
},
"node_modules/flat-cache": {
"version": "3.2.0",
"dev": true,
@ -2828,7 +2909,9 @@
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"dev": true
"dev": true,
"optional": true,
"peer": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
@ -3086,7 +3169,6 @@
},
"node_modules/nanoid": {
"version": "3.3.7",
"dev": true,
"funding": [
{
"type": "github",
@ -3209,7 +3291,6 @@
},
"node_modules/picocolors": {
"version": "1.1.0",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@ -3254,8 +3335,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.45",
"dev": true,
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@ -3270,16 +3352,53 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-nesting": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz",
"integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"dependencies": {
"@csstools/selector-resolve-nested": "^3.0.0",
"@csstools/selector-specificity": "^5.0.0",
"postcss-selector-parser": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"postcss": "^8.4"
}
},
"node_modules/postcss-selector-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz",
"integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"dev": true,
@ -3497,6 +3616,8 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@ -3506,6 +3627,8 @@
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.80.3.tgz",
"integrity": "sha512-aTxTl4ToSAWg7ILFgAe+kMenj+zNlwHmHK/ZNPrOM8+HTef1Q6zuxolptYLijmHdZHKSMOkWYHgo5MMN6+GIyg==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"buffer-builder": "^0.2.0",
@ -3556,6 +3679,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3572,6 +3696,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3588,6 +3713,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3604,6 +3730,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3620,6 +3747,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3636,6 +3764,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3652,6 +3781,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3668,6 +3798,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3684,6 +3815,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3700,6 +3832,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3716,6 +3849,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3732,6 +3866,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3748,6 +3883,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3764,6 +3900,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3780,6 +3917,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3796,6 +3934,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3812,6 +3951,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3828,6 +3968,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3844,6 +3985,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3860,6 +4002,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -3869,6 +4012,8 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=8"
}
@ -3878,6 +4023,8 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@ -3925,7 +4072,6 @@
},
"node_modules/source-map-js": {
"version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -4013,7 +4159,9 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"dev": true
"dev": true,
"optional": true,
"peer": true
},
"node_modules/type-check": {
"version": "0.4.0",
@ -4122,6 +4270,11 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/valibot": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-0.32.0.tgz",
@ -4131,7 +4284,9 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"dev": true
"dev": true,
"optional": true,
"peer": true
},
"node_modules/vite": {
"version": "5.4.9",

View File

@ -24,14 +24,15 @@
"React"
],
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.27",
"@codex-storage/marketplace-ui-components": "^0.0.28",
"@codex-storage/sdk-js": "^0.0.12",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",
"@tanstack/react-query": "^5.51.15",
"@tanstack/react-router": "^1.58.7",
"echarts": "^5.5.1",
"dotted-map": "^2.2.3",
"echarts": "^5.5.1",
"emoji-picker-react": "^4.12.0",
"idb-keyval": "^6.2.1",
"lucide-react": "^0.445.0",
"react": "^18.3.1",
@ -51,8 +52,9 @@
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.12",
"postcss": "^8.4.47",
"postcss-nesting": "^13.0.1",
"prettier": "^3.3.3",
"sass-embedded": "^1.79.4",
"typescript": "5.5.4",
"vite": "^5.4.7"
},

6
postcss.config.json Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
'postcss-nesting': { /* plugin options */ },
},
}

View File

@ -0,0 +1,12 @@
<span>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.5C3.85775 15.5 0.5 12.1423 0.5 8C0.5 3.85775 3.85775 0.5 8 0.5C12.1423 0.5 15.5 3.85775 15.5 8C15.5 12.1423 12.1423 15.5 8 15.5ZM8 6.9395L5.879 4.81775L4.81775 5.879L6.9395 8L4.81775 10.121L5.879 11.1823L8 9.0605L10.121 11.1823L11.1823 10.121L9.0605 8L11.1823 5.879L10.121 4.81775L8 6.9395Z"
fill="#FB3748" />
</svg>
</span>

View File

@ -1,7 +1,13 @@
import "./appBar.css";
import { DashboardIcon } from "../DashboardIcon/DashboardIcon";
import { NodeIndicator } from "../NodeIndicator/NodeIndicator";
import { HttpNetworkIndicator } from "../HttpNetworkIndicator/HttpNetworkIndicator";
import { classnames } from "../../utils/classnames";
import { useNetwork } from "../../network/useNetwork";
import { NetworkFlashIcon } from "../NetworkFlashIcon/NetworkFlashIcon";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { useCodexConnection } from "../../hooks/useCodexConnection";
import { NodesIcon } from "../Menu/NodesIcon";
import { usePersistence } from "../../hooks/usePersistence";
type Props = {
/**
@ -11,29 +17,53 @@ type Props = {
onExpand: () => void;
};
export function AppBar(props: Props) {
console.info(props);
export function AppBar(_: Props) {
console.debug(_);
const online = useNetwork();
const queryClient = useQueryClient();
const codex = useCodexConnection();
const persistence = usePersistence(codex.enabled);
useEffect(() => {
queryClient.invalidateQueries({
type: "active",
refetchType: "all",
});
}, [queryClient, codex.enabled]);
const offline = !online || !codex.enabled;
return (
<div className="appBar">
<div className="appBar-left">
<div
className={classnames(
["app-bar"],
["app-bar--offline", offline],
["app-bar--no-persistence", !persistence.enabled]
)}>
<div className="row gap">
{/* <a className="appBar-burger" onClick={onExpand}>
<Menu size={"1.25rem"} />
</a> */}
<div className="appBar-icon">
<span>
<DashboardIcon />
</div>
<div className="appBar-textContainer">
<div className="appBar-title">Dashboard</div>
<div className="appBar-subtitle">
Get Overview of your Codex Vault
</div>
</span>
<div>
<h1>Dashboard</h1>
<h2>Get Overview of your Codex Vault</h2>
</div>
</div>
<div className="appBar-right">
<HttpNetworkIndicator />
<NodeIndicator />
</div>
<aside className="row gap">
<div className="row gap">
<NetworkFlashIcon />
<span>Network</span>
</div>
<div className="row gap">
<NodesIcon variant={codex.enabled ? "success" : "failure"} />
<span>Node</span>
</div>
</aside>
</div>
);
}

View File

@ -1,60 +1,80 @@
.appBar {
.app-bar {
height: 80px;
justify-content: space-between;
border-bottom: 1px solid var(--codex-border-color);
view-transition-name: main-header;
display: flex;
padding: 20px 40px 20px 40px;
border-bottom: 1px solid #2b303b;
padding-right: 48px;
padding-left: 48px;
padding-top: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #96969633;
box-sizing: border-box;
background-color: #1c1c1c;
border-right: 12px solid transparent;
position: sticky;
top: 0;
z-index: 1;
&:not(.app-bar--offline):not(.app-bar--no-persistence) {
border-right-color: #6ccc93;
}
&.app-bar--offline {
border-right-color: var(--codex-input-color-error);
}
&.app-bar--no-persistence {
border-right-color: rgb(var(--codex-color-warning));
}
h1 {
font:
500 18px/24px "Inter",
sans-serif;
letter-spacing: -0.015em;
color: white;
}
h2 {
font:
400 14px/20px "Inter",
sans-serif;
letter-spacing: -0.006em;
color: #969696cc;
}
> div {
span {
background: #141414;
height: 48px;
width: 48px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #353639;
border-radius: 50%;
}
}
aside {
svg {
padding: 10px;
background-color: #141414;
border-radius: var(--codex-border-radius);
}
span {
font:
500 14px/20px "Inter",
sans-serif;
letter-spacing: -0.006em;
color: #8d8d8d;
}
}
}
.appBar-burger {
cursor: pointer;
color: var(--codex-color);
display: flex;
}
.appBar,
.appBar-left,
.appBar-right {
display: flex;
align-items: center;
gap: 16px;
}
.appBar-icon {
background: #141414;
height: 48px;
width: 48px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #353639;
border-radius: 50%;
}
.appBar-title {
font-family: Inter;
font-size: 18px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.015em;
color: white;
}
.appBar-subtitle {
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.006em;
color: #969696cc;
}
@media (min-width: 1000px) {
/* @media (min-width: 1000px) {
.appBar-burger {
display: none;
}
}
} */

View File

@ -1,8 +1,4 @@
import {
Cell,
Row,
SimpleText,
} from "@codex-storage/marketplace-ui-components";
import { Cell, Row } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import "./AvailabilityDiskRow.css";
import { classnames } from "../../utils/classnames";
@ -26,9 +22,9 @@ export function AvailabilityDiskRow({ bytes }: Props) {
<div>
<b>Node</b>
</div>
<SimpleText size="small" variant="light">
<small className="text--light">
{PrettyBytes(bytes)} allocated for the node
</SimpleText>
</small>
</div>
</div>
</Cell>,

View File

@ -1,6 +1,6 @@
import "./AvailabilityIdCell.css";
import { Strings } from "../../utils/strings";
import { Cell, SimpleText } from "@codex-storage/marketplace-ui-components";
import { Cell } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import { availabilityColors } from "./availability.colors";
import { AvailabilityWithSlots } from "./types";
@ -19,18 +19,13 @@ export function AvailabilityIdCell({ value, index }: Props) {
<div>
<b>{value.name || Strings.shortId(value.id)}</b>
</div>
<SimpleText size="small" variant="light">
<small className="text--light">
{PrettyBytes(value.totalSize)} allocated for the availability
</SimpleText>
</small>
<div>
{/* <div>
<SimpleText size="small" variant="light">
{a.id}
</SimpleText>
</div> */}
<SimpleText size="small" variant="light">
<small className="text--light">
Max collateral {value.maxCollateral} | Min price {value.minPrice}
</SimpleText>
</small>
</div>
</div>
</div>

View File

@ -1,8 +1,4 @@
import {
Cell,
Row,
SimpleText,
} from "@codex-storage/marketplace-ui-components";
import { Cell, Row } from "@codex-storage/marketplace-ui-components";
import { PrettyBytes } from "../../utils/bytes";
import "./AvailabilitySlotRow.css";
import { classnames } from "../../utils/classnames";
@ -52,9 +48,9 @@ export function AvailabilitySlotRow({ bytes, active, id }: Props) {
<div>
<b>Slot {id}</b>
</div>
<SimpleText size="small" variant="light">
<small className="text--light">
{PrettyBytes(bytes)} allocated for the slot
</SimpleText>
</small>
</div>
</div>
</Cell>,

View File

@ -0,0 +1,15 @@
.background-img {
position: absolute;
right: -40px;
max-height: 90%;
width: auto;
/* z-index: -1; */
transition: opacity 0.35s;
opacity: 0.3;
@media (min-width: 1200px) {
& {
opacity: 1;
}
}
}

View File

@ -0,0 +1,35 @@
import "./BackgroundImage.css";
export function BackgroundImage() {
return (
<picture>
<source
srcSet="/img/onboarding@4x.webp 4x,
/img/onboarding@3x.webp 3x,
/img/onboarding@2x.webp 2x,
/img/onboarding@1.5x.webp 1.5x,
/img/onboarding.webp 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/webp"
/>
<source
srcSet="/img/onboarding@4x.png 4x,
/img/onboarding@3x.png 3x,
/img/onboarding@2x.png 2x,
/img/onboarding@1.5x.png 1.5x,
/img/onboarding.png 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/png"
/>
<img
src="/img/onboarding.png"
alt="Background Image"
className="background-img"
/>
</picture>
);
}

View File

@ -1,7 +1,4 @@
import {
ButtonIcon,
SimpleText,
} from "@codex-storage/marketplace-ui-components";
import { ButtonIcon } from "@codex-storage/marketplace-ui-components";
import "./CardNumbers.css";
import { Check, CircleX, Pencil } from "lucide-react";
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
@ -165,12 +162,7 @@ export function CardNumbers({
{error ? (
<small className="cardNumber-errorText">{error}</small>
) : (
<SimpleText
size="small"
variant="light"
className="cardNumber-helperText">
{helper}
</SimpleText>
<small className="cardNumber-helperText">{helper}</small>
)}
</div>
);

View File

@ -1,21 +1,15 @@
type Props = {
className?: string;
};
export function ErrorCircleIcon({ className = "" }: Props) {
export function ErrorCircleIcon() {
return (
<span className={"onboarding-healthCheck-icon " + className}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.5C3.85775 15.5 0.5 12.1423 0.5 8C0.5 3.85775 3.85775 0.5 8 0.5C12.1423 0.5 15.5 3.85775 15.5 8C15.5 12.1423 12.1423 15.5 8 15.5ZM8 6.9395L5.879 4.81775L4.81775 5.879L6.9395 8L4.81775 10.121L5.879 11.1823L8 9.0605L10.121 11.1823L11.1823 10.121L9.0605 8L11.1823 5.879L10.121 4.81775L8 6.9395Z"
fill="#FB3748"
/>
</svg>
</span>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.5C3.85775 15.5 0.5 12.1423 0.5 8C0.5 3.85775 3.85775 0.5 8 0.5C12.1423 0.5 15.5 3.85775 15.5 8C15.5 12.1423 12.1423 15.5 8 15.5ZM8 6.9395L5.879 4.81775L4.81775 5.879L6.9395 8L4.81775 10.121L5.879 11.1823L8 9.0605L10.121 11.1823L11.1823 10.121L9.0605 8L11.1823 5.879L10.121 4.81775L8 6.9395Z"
fill="#FB3748"
/>
</svg>
);
}

View File

@ -1,10 +1,9 @@
import { CircleX } from "lucide-react";
import { SimpleText } from "@codex-storage/marketplace-ui-components";
export function ErrorIcon() {
return (
<SimpleText variant="error">
<span className="text--error">
<CircleX size="4rem" />
</SimpleText>
</span>
);
}

View File

@ -0,0 +1,85 @@
.address {
display: flex;
align-items: center;
position: relative;
gap: 16px;
> div {
position: relative;
}
svg {
position: absolute;
top: 68px;
bottom: 0;
right: 18px;
}
.refresh {
position: relative;
top: 24px;
cursor: pointer;
svg {
position: initial;
}
&.address--fetching .refresh svg {
animation: rotate 2s linear infinite;
}
}
input[type="number"] {
width: 150px;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
}
.helper {
font-family: Azeret Mono;
font-size: 12px;
font-weight: 400;
line-height: 14px;
color: #828282;
padding-left: 1.25rem;
margin-top: 1.75rem;
margin-bottom: 3rem;
}
.health-checks {
margin-bottom: 32px;
li {
display: flex;
align-items: center;
padding: 16px 0;
gap: 16px;
border-top: 1px solid #96969633;
border-bottom: 1px solid #96969633;
&:first-child {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.011em;
}
span {
display: flex;
align-items: center;
width: 20px;
height: 20px;
justify-content: center;
}
}
}

View File

@ -0,0 +1,199 @@
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState, ClipboardEvent } from "react";
import { useDebug } from "../../hooks/useDebug";
import { usePersistence } from "../../hooks/usePersistence";
import { usePortForwarding } from "../../hooks/usePortForwarding";
import { CodexSdk } from "../../proxy";
import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon";
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
import { WarningIcon } from "../WarningIcon/WarningIcon";
import { HealthCheckIcon } from "./HealthCheckIcon";
import { Input } from "@codex-storage/marketplace-ui-components";
import { classnames } from "../../utils/classnames";
import { DebugUtils } from "../../utils/debug";
import { Strings } from "../../utils/strings";
import { RefreshIcon } from "../RefreshIcon/RefreshIcon";
import "./HealthChecks.css";
type Props = {
online: boolean;
onStepValid: (valid: boolean) => void;
};
const throwOnError = false;
const defaultPort = 8070;
export function HealthChecks({ online, onStepValid }: Props) {
const codex = useDebug(throwOnError);
const portForwarding = usePortForwarding(codex.data);
const persistence = usePersistence(codex.isSuccess);
const [isInvalid, setIsInvalid] = useState(false);
const [url, setUrl] = useState(CodexSdk.url);
const queryClient = useQueryClient();
useEffect(() => {
if (codex.isSuccess) {
persistence.refetch();
portForwarding.refetch().then(({ isError }) => onStepValid(isError));
} else {
onStepValid(false);
}
}, [
persistence.refetch,
onStepValid,
portForwarding.refetch,
codex.isSuccess,
]);
const onAddressChange = (e: React.FormEvent<HTMLInputElement>) => {
const [, port] = Strings.splitURLAndPort(url);
const element = e.currentTarget;
const value = e.currentTarget.value;
if (
value.startsWith("http://") === false &&
value.startsWith("https://") === false
) {
setIsInvalid(true);
return;
}
setIsInvalid(!element.checkValidity());
setUrl(value + ":" + port);
};
const onPaste = (e: ClipboardEvent) => {
const text = e.clipboardData?.getData("text") || "";
try {
new URL(text);
setUrl(text);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
// Nothing to do here
}
};
const onPortChange = (e: React.FormEvent<HTMLInputElement>) => {
const [address] = Strings.splitURLAndPort(url);
const element = e.currentTarget;
const value = element.value;
setUrl(address + ":" + value);
};
const onSave = () => {
if (isInvalid === true) {
return;
}
CodexSdk.updateURL(url)
.then(() => queryClient.invalidateQueries())
.then(() => codex.refetch());
};
let forwardingPortValue = defaultPort;
if (codex.isSuccess && codex.data) {
const port = DebugUtils.getTcpPort(codex.data);
if (!port.error) {
forwardingPortValue = port.data;
}
}
const [address, port] = Strings.splitURLAndPort(url);
return (
<>
<div
className={classnames(
["address"],
["address--fetching", portForwarding.isFetching || codex.isPending]
)}>
<div>
<Input
onPaste={onPaste}
id="url"
type="url"
label="Address"
isInvalid={isInvalid}
onChange={onAddressChange}
value={address}
placeholder="127.0.0.1"></Input>
{isInvalid ? (
<ErrorCircleIcon />
) : (
<SuccessCheckIcon variant="default" />
)}
</div>
<div>
<Input
id="port"
label="Port"
type="number"
onChange={onPortChange}
value={port}
placeholder="8080"></Input>
<SuccessCheckIcon variant="default"></SuccessCheckIcon>
</div>
<div className="refresh">
<RefreshIcon onClick={onSave}></RefreshIcon>
</div>
</div>
<ul className="helper">
<li>Port forwarding should be default {forwardingPortValue}.</li>
</ul>
<ul className="health-checks">
<li>
<span>
<HealthCheckIcon />
</span>
Health Check
</li>
<li>
<span>
{online ? (
<SuccessCheckIcon variant="primary"></SuccessCheckIcon>
) : (
<ErrorCircleIcon />
)}
</span>
Internet connection
</li>
<li>
<span>
{portForwarding.enabled ? (
<SuccessCheckIcon variant="primary"></SuccessCheckIcon>
) : (
<ErrorCircleIcon />
)}
</span>
Port forwarding
</li>
<li>
<span>
{codex.isSuccess ? (
<SuccessCheckIcon variant="primary"></SuccessCheckIcon>
) : (
<ErrorCircleIcon />
)}
</span>
Codex connection
</li>
<li>
<span>
{persistence.enabled ? (
<SuccessCheckIcon variant="primary"></SuccessCheckIcon>
) : (
<WarningIcon />
)}
</span>
Marketplace
</li>
</ul>
</>
);
}

View File

@ -1,24 +0,0 @@
.network-indicator {
display: flex;
align-items: center;
gap: 16px;
}
.network-indicator-icon {
background-color: #141414;
border-radius: var(--codex-border-radius);
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.network-indicator-text {
font-family: Inter;
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.006em;
color: #8d8d8d;
}

View File

@ -1,16 +0,0 @@
import { useNetwork } from "../../network/useNetwork";
import { NetworkFlashIcon } from "../NetworkFlashIcon/NetworkFlashIcon";
import "./HttpNetworkIndicator.css";
export function HttpNetworkIndicator() {
const online = useNetwork();
return (
<div className="network-indicator">
<div className="network-indicator-icon">
<NetworkFlashIcon online={online} />
</div>
<span className="network-indicator-text">Network</span>
</div>
);
}

View File

@ -1,14 +1,20 @@
import { attributes } from "../../utils/attributes";
type Props = {
onClick?: () => void;
width?: number;
height?: number;
};
export function Logo({ onClick }: Props) {
export function Logo({ onClick, width, height }: Props) {
return (
<svg
onClick={onClick}
className="logo"
width="30"
height="34"
{...attributes({
width: width?.toString() || false,
height: height?.toString() || false,
})}
viewBox="0 0 30 34"
fill="none"
xmlns="http://www.w3.org/2000/svg">

File diff suppressed because one or more lines are too long

View File

@ -6,10 +6,23 @@ import { Logo } from "../Logo/Logo";
import { Logotype } from "../Logotype/Logotype";
import { ExpandIcon } from "./ExpandIcon";
import { classnames } from "../../utils/classnames";
import { Link } from "@tanstack/react-router";
import { HomeIcon } from "./HomeIcon";
import { WalletIcon } from "./WalletIcon";
import { FilesIcon } from "./FilesIcon";
import { NodesIcon } from "./NodesIcon";
import { AnalyticsIcon } from "./AnalyticsIcon";
import { DeviceIcon } from "./DeviceIcon";
import { PurchaseIcon } from "./PurchaseIcon";
import { HostIcon } from "./HostIcon";
import { PeersIcon } from "./PeersIcon";
import { LogsIcon } from "./LogsIcon";
import { SettingsIcon } from "./SettingsIcon";
import { HelpIcon } from "./HelpIcon";
import { DisclaimerIcon } from "./DisclaimerIcon";
export type MenuItemComponentProps = {
onClick: () => void;
className: string;
};
export type MenuItem =
@ -33,27 +46,9 @@ type Props = {
onClose: () => void;
onOpen?: () => void;
/**
* The menu items to be displayed
*/
items: MenuItem[];
className?: string;
/**
* The application version
*/
version?: string;
};
export function Menu({
expanded,
onClose,
onOpen,
items,
className = "",
}: Props) {
export function Menu({ expanded, onClose, onOpen }: Props) {
const [isExpanded, setIsExpanded] = useState<boolean | null>(null);
useEffect(() => {
@ -70,44 +65,122 @@ export function Menu({
const onExpandMenu = () => setIsExpanded(isExpanded === false ? true : false);
const renderItem = (i: MenuItem, index: number) => {
switch (i.type) {
case "separator":
return <hr className="menu-item-separator" key={index}></hr>;
case "space":
return <div className="menu-space" key={index}></div>;
case "item":
return (
<i.Component onClick={onClose} className="menu-item" key={index} />
);
}
};
return (
<>
<Backdrop onClose={onClose} open={expanded} />
<aside
className={classnames(
[`menu ${className}`],
[`menu`],
["menu--expanded", isExpanded === true],
["menu--unexpanded", isExpanded === false]
)}
{...attributes({ "aria-expanded": expanded })}>
<div className="menu-container">
<div className="menu-header">
<Logo onClick={onLogoClick} />
<Logotype height={34} className={"menu-logotype"} />
<div className="menu-header-right">
<div>
<header>
<Logo onClick={onLogoClick} width={30} />
<Logotype height={34} />
<div>
<ExpandIcon onClick={onExpandMenu}></ExpandIcon>
</div>
{/* <span className="menu-separator">|</span>
<span className="menu-name">Codex</span>
<span className="menu-state">ALPHA {version}</span> */}
</div>
</header>
<div className="menu-items">
{items.map((item, index) => renderItem(item, index))}
<div className="items">
<Link to="/dashboard" activeOptions={{ exact: true }}>
<span>
<HomeIcon />
</span>
<span>Dashboard</span>
</Link>
<Link to="/dashboard/wallet">
<span>
<WalletIcon />
</span>
<span>Wallet</span>
</Link>
<Link to="/dashboard/files">
<span>
<FilesIcon />
</span>
<span>Files</span>
</Link>
<Link
to="/dashboard/nodes"
disabled={true}
aria-disabled={true}
data-title="Coming soon">
<span>
<NodesIcon variant="default" />
</span>
<span>Nodes</span>
</Link>
<Link
to="/dashboard/analytics"
disabled={true}
aria-disabled={true}
title="Coming soon"
data-title="Coming soon">
<span>
<AnalyticsIcon />
</span>
<span>Analytics</span>
</Link>
<Link
to="/dashboard/device"
disabled={true}
aria-disabled={true}
title="Coming soon"
data-title="Coming soon">
<span>
<DeviceIcon />
</span>
<span>Devices</span>
</Link>
<hr />
<Link to="/dashboard/purchases">
<span>
<PurchaseIcon />
</span>
<span>Purchases</span>
</Link>
<Link to="/dashboard/availabilities">
<span>
<HostIcon />
</span>
<span>Host</span>
</Link>
<hr />
<Link to="/dashboard/peers">
<span>
<PeersIcon />
</span>
<span>Peers</span>
</Link>
<Link to="/dashboard/logs">
<span>
<LogsIcon />
</span>
<span>Log</span>
</Link>
<section></section>
<Link to="/dashboard/settings">
<span>
<SettingsIcon />
</span>
<span>Settings</span>
</Link>
<Link to="/dashboard/help">
<span>
<HelpIcon />
</span>
<span>Help</span>
</Link>
<Link to="/dashboard/disclaimer">
<span>
<DisclaimerIcon />
</span>
<span>Disclaimer</span>
</Link>
</div>
</div>
</aside>

View File

@ -14,283 +14,223 @@
transition: width 0.5s;
min-width: 0;
width: 272px;
}
transform: translatex(0px);
.logo {
cursor: pointer;
min-width: 30px;
}
.menu--expanded .logo {
cursor: default;
}
.menu--expanded {
width: 272px;
}
.menu--unexpanded {
width: 80px;
}
/* @keyframes menu-in {
0% {
width: 0;
display: none;
&[aria-expanded] {
transform: translatex(0);
min-width: 200px;
}
100% {
width: auto;
display: inline-block;
&:not(.menu--unexpanded) a[data-title]:hover::after {
content: attr(data-title);
background-color: #2f2f2f;
color: #fff;
padding: 8px;
border-radius: 4px;
font-size: 12px;
line-height: 14px;
display: block;
white-space: nowrap;
position: absolute;
right: 1rem;
overflow: visible;
}
&.menu--expanded {
width: 272px;
}
&.menu--unexpanded {
width: 80px;
.items {
a {
width: 26px;
gap: 0;
display: flex;
justify-content: center;
span + span {
font-size: 0;
}
}
}
}
> div {
display: flex;
flex-direction: column;
padding: 12px;
position: sticky;
top: 0;
height: calc(100vh - 24px);
overflow: auto;
}
header {
padding: 13px;
display: flex;
align-items: center;
gap: 1.5rem;
background-color: #060606;
border-radius: 8px;
> svg:first-child {
cursor: pointer;
min-width: 30px;
}
div {
flex: 1;
text-align: right;
transition: opacity 0.35s;
display: inline-block;
overflow: hidden;
min-width: 0;
svg {
cursor: pointer;
}
&:hover {
animation-name: example;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
}
}
.items {
padding-top: 1.5rem;
display: flex;
flex-direction: column;
position: relative;
height: 100%;
margin-bottom: 2.5rem;
gap: 0.5rem;
border-top: 1px solid #96969633;
&::before {
height: 20px;
width: 8px;
background-color: var(--codex-color-primary);
position: absolute;
content: " ";
transition:
top 1s,
bottom 1s;
border-radius: 4px;
left: -16px;
}
&:has(.active:nth-child(1))::before {
top: 30px;
}
&:has(.active:nth-child(2))::before {
top: 72px;
}
&:has(.active:nth-child(3))::before {
top: 115px;
}
&:has(.active:nth-child(4))::before {
top: 158px;
}
&:has(.active:nth-child(5))::before {
top: 201px;
}
&:has(.active:nth-child(6))::before {
top: 244px;
}
&:has(.active:nth-child(8))::before {
top: 339px;
}
&:has(.active:nth-child(9))::before {
top: 382px;
}
&:has(.active:nth-child(11))::before {
top: 475px;
}
&:has(.active:nth-child(12))::before {
top: 513px;
}
&:has(.active:nth-child(14))::before {
top: calc(100% - 113px);
}
&:has(.active:nth-child(15))::before {
top: calc(100% - 70px);
}
&:has(.active:nth-child(16))::before {
top: calc(100% - 27px);
}
&:not(:first-child) {
margin-top: 0.5rem;
}
hr {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
border: 0.1px solid #96969633;
width: 100%;
}
section {
flex: 1;
}
a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 8px 10px;
margin-bottom: 0;
text-decoration: none;
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.006em;
color: #969696;
border-radius: 8px;
transition: background-color 0.35s;
position: relative;
margin-left: 6px;
&:hover:not([aria-disabled="true"]),
&.active {
background-color: var(--codex-highlight-color);
color: #c7c7c7;
}
span:first-child {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
transition: color 1s;
}
span + span {
display: inline-block;
overflow: hidden;
min-width: 0;
}
&.active span:first-child {
color: var(--codex-color-primary);
}
}
}
}
*/
.menu-logotype,
.menu-header-right,
.menu-text {
display: inline-block;
overflow: hidden;
min-width: 0;
}
/* .menu--expanded .menu-header-right svg,
.menu--expanded .menu-logotype,
.menu--expanded .menu-header-right {
animation-name: header-right-menu-in;
animation-duration: 1s;
animation-fill-mode: forwards;
}
.menu--expanded .menu-header-right svg,
.menu--expanded .menu-logotype { */
/* display: none; */
/* animation-name: logotype-menu-in;
animation-duration: 1s;
animation-fill-mode: forwards;
} */
/* .menu--unexpanded .menu-logotype,
.menu--unexpanded .menu-header-right {
display: none;
} */
.menu-container {
display: flex;
flex-direction: column;
padding: 12px;
position: sticky;
top: 0;
height: calc(100vh - 24px);
overflow: auto;
}
.menu-header-right {
flex: 1;
text-align: right;
transition: opacity 0.35s;
cursor: pointer;
}
.menu-header-right:hover {
animation-name: example;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
.menu-backdrop {
display: none;
}
.menu-items {
border-top: 2px solid var(--codex-border-color);
padding-top: 1.5rem;
display: flex;
flex-direction: column;
position: relative;
height: 100%;
margin-bottom: 2.5rem;
}
.menu[aria-expanded] {
transform: translatex(0);
min-width: 200px;
}
.menu-header {
padding: 12px;
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 1rem;
background-color: #060606;
border-radius: 8px;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
}
.menu-items::before {
height: 20px;
width: 8px;
background-color: var(--codex-color-primary);
position: absolute;
content: " ";
transition:
top 1s,
bottom 1s;
border-radius: 4px;
left: -16px;
}
.menu-items:has(.active:nth-child(1))::before {
top: 30px;
}
.menu-items:has(.active:nth-child(2))::before {
top: 72px;
}
.menu-items:has(.active:nth-child(3))::before {
top: 115px;
}
.menu-items:has(.active:nth-child(4))::before {
top: 158px;
}
.menu-items:has(.active:nth-child(5))::before {
top: 201px;
}
.menu-items:has(.active:nth-child(6))::before {
top: 244px;
}
.menu-items:has(.active:nth-child(8))::before {
top: 332px;
}
.menu-items:has(.active:nth-child(9))::before {
top: 375px;
}
.menu-items:has(.active:nth-child(11))::before {
top: 461px;
}
.menu-items:has(.active:nth-child(12))::before {
top: 504px;
}
.menu-items:has(.active:nth-child(14))::before {
top: calc(100% - 113px);
}
.menu-items:has(.active:nth-child(15))::before {
top: calc(100% - 70px);
}
.menu-items:has(.active:nth-child(16))::before {
top: calc(100% - 27px);
}
.menu-item:not(:first-child) {
margin-top: 0.5rem;
}
.menu-item {
padding: 8px 18px;
margin-bottom: 0;
text-decoration: none;
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.006em;
color: #969696;
border-radius: 8px;
transition: background-color 0.35s;
}
.menu-item:hover,
.menu-item.active {
background-color: var(--codex-highlight-color);
color: #c7c7c7;
}
.menu-item.active .menu-icon {
color: var(--codex-color-primary);
}
.menu-title {
text-transform: uppercase;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 0.75rem;
display: inline-block;
font-weight: 500;
}
.menu-item-separator {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
border: 0.1px solid var(--codex-border-color);
width: 100%;
}
.menu-state {
font-size: 0.6rem;
background-color: var(--codex-background-light);
padding: 0.25rem;
border-radius: var(--codex-border-radius);
position: relative;
top: 1px;
}
.menu-footer {
text-align: center;
}
.menu-version {
padding: 0;
}
.menu-space {
flex: 1;
}
.menu-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
transition: color 1s;
}
@media (min-width: 1000px) {
.menu {
transform: translatex(0px);
/* position: inherit; */
}
}
.menu a {
position: relative;
}
.menu:not(.menu--unexpanded) a[data-title]:hover::after {
content: attr(data-title);
background-color: #2f2f2f;
color: #fff;
padding: 8px;
border-radius: 4px;
font-size: 12px;
line-height: 14px;
display: block;
white-space: nowrap;
position: absolute;
right: 1rem;
overflow: visible;
}

View File

@ -1,18 +1,20 @@
type Props = {
online: boolean;
className?: string;
};
export function NetworkFlashIcon({ online }: Props) {
const color = online ? "#3EE089" : "var(--codex-color-error-hexa)";
export function NetworkFlashIcon({ className }: Props) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
fill="#none"
xmlns="http://www.w3.org/2000/svg">
<path d="M10.75 8.5H16L9.25 18.25V11.5H4L10.75 1.75V8.5Z" fill={color} />
<path
d="M10.75 8.5H16L9.25 18.25V11.5H4L10.75 1.75V8.5Z"
fill={"#3EE089"}
/>
</svg>
);
}

View File

@ -1,24 +0,0 @@
.network-indicator {
display: flex;
align-items: center;
gap: 16px;
}
.network-indicator-icon {
background-color: #141414;
border-radius: var(--codex-border-radius);
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.network-indicator-text {
font-family: Inter;
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.006em;
color: #8d8d8d;
}

View File

@ -1,28 +0,0 @@
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { useCodexConnection } from "../../hooks/useCodexConnection";
import "./NodeIndicator.css";
import { NodesIcon } from "../Menu/NodesIcon";
export function NodeIndicator() {
const queryClient = useQueryClient();
const codex = useCodexConnection();
useEffect(() => {
queryClient.invalidateQueries({
type: "active",
refetchType: "all",
});
}, [queryClient, codex.enabled]);
return (
<>
<div className="network-indicator">
<div className="network-indicator-icon">
<NodesIcon variant={codex.enabled ? "success" : "failure"} />
</div>
<span className="network-indicator-text">Node</span>
</div>
</>
);
}

View File

@ -1,5 +0,0 @@
.onboarding-healthCheckItem {
display: flex;
align-items: center;
gap: 16px;
}

View File

@ -1,21 +0,0 @@
import { classnames } from "../../utils/classnames";
import { OnBoardingStatusIcon } from "./OnBoardingStatusIcon";
import "./HealthCheckItem.css";
type Props = {
value: "success" | "failure" | "warning";
text: string;
};
export function HealthCheckItem({ value, text }: Props) {
return (
<div
data-testid="network"
className={classnames(["onboarding-healthCheckItem"])}>
<OnBoardingStatusIcon value={value} />
<div>
<p>{text}</p>
</div>
</div>
);
}

View File

@ -1,7 +0,0 @@
.onboarding-image {
position: absolute;
right: -40px;
max-height: 90%;
width: auto;
z-index: -1;
}

View File

@ -1,35 +0,0 @@
import "./OnBoardingImage.css";
export function OnBoardingImage() {
return (
<picture>
<source
srcSet="/img/onboarding@4x.webp 4x,
/img/onboarding@3x.webp 3x,
/img/onboarding@2x.webp 2x,
/img/onboarding@1.5x.webp 1.5x,
/img/onboarding.webp 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/webp"
/>
<source
srcSet="/img/onboarding@4x.png 4x,
/img/onboarding@3x.png 3x,
/img/onboarding@2x.png 2x,
/img/onboarding@1.5x.png 1.5x,
/img/onboarding.png 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/png"
/>
<img
src="/img/onboarding.png"
alt="Onboarding Image"
className="onboarding-image"
/>
</picture>
);
}

View File

@ -0,0 +1,217 @@
.onboarding {
width: 100%;
padding: 3rem 6rem;
display: flex;
> section {
display: flex;
flex-direction: column;
justify-content: space-between;
}
> section:first-child {
max-width: 500px;
}
section {
flex: 1;
}
&.onboarding--second .alpha {
flex: 0.3;
}
&.onboarding--third .alpha {
flex: 0.5;
}
section > *:first-child {
flex: 0.5;
}
h1 {
font-family: Inter;
font-size: 36px;
font-weight: 300;
line-height: 43.57px;
letter-spacing: 0.01em;
b {
font-weight: 400;
}
b + b {
font-weight: 900;
text-transform: uppercase;
}
}
footer {
display: flex;
align-items: center;
justify-content: space-between;
flex: 0;
ul {
display: flex;
gap: 8px;
li {
& {
width: 12px;
height: 12px;
background-color: white;
display: inline-block;
border-radius: 50%;
transition: opacity 0.35s;
}
&:hover {
animation-name: pulse;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
&:not([aria-selected]) {
opacity: 0.4;
}
&[aria-selected] {
box-shadow: 0px 0px 12px 0px #fff;
opacity: 1;
}
}
}
}
.alpha {
> div {
margin-top: 4px;
display: block;
}
b {
font-weight: 500;
opacity: 0.6;
display: block;
}
a {
text-decoration: underline;
font-family: Azeret Mono;
font-size: 12px;
font-weight: 400;
line-height: 14px;
margin-top: 16px;
cursor: pointer;
display: inline-flex;
align-items: center;
color: var(--codex-color-error-hexa);
&:hover {
animation-name: example;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
}
p {
margin-top: 1rem;
font-family: Azeret Mono;
font-size: 14px;
font-weight: 400;
line-height: 16.34px;
display: inline-block;
color: var(--codex-color-primary);
}
}
.main {
p {
font-family: Azeret Mono;
font-size: 14px;
font-weight: 400;
line-height: 16.34px;
max-width: 532px;
margin-top: 20px;
color: var(--codex-input-label-color);
}
label {
margin-top: 1rem;
}
}
.get-started {
a {
font-size: 24px;
font-weight: 600;
line-height: 29.05px;
letter-spacing: 0.01em;
margin-top: 32px;
font-family: Inter;
color: #7bfbaf;
gap: 4px;
text-decoration: none;
border-bottom: 2px solid #7bfbaf;
cursor: pointer;
display: inline-flex;
align-items: center;
}
}
.modal {
max-width: 600px;
margin: auto;
h1 {
margin-top: 0;
margin-bottom: 3rem;
}
p {
line-height: 1.5rem;
}
}
.navigation {
cursor: pointer;
position: absolute;
right: 6rem;
bottom: 3rem;
border-bottom: none;
text-decoration: none;
z-index: 1;
&:hover {
animation-name: example;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
&[aria-disabled="true"] {
cursor: not-allowed;
}
}
}
@keyframes rotate {
from {
transform: rotate(0deg); /* Start at 0 degrees */
}
to {
transform: rotate(360deg); /* End at 360 degrees */
}
}
@keyframes pulse {
0% {
opacity: 0.4;
}
30% {
opacity: 0.8;
}
100% {
opacity: 0.4;
}
}

View File

@ -0,0 +1,41 @@
import { ReactElement } from "react";
import { classnames } from "../../utils/classnames";
import { Logotype } from "../Logotype/Logotype";
import { attributes } from "../../utils/attributes";
import "./OnBoardingLayout.css";
import { BackgroundImage } from "../BackgroundImage/BackgroundImage";
type Props = {
children: ReactElement<{ onStepValid: (isValid: boolean) => void }>;
step: number;
defaultIsStepValid: boolean;
};
export function OnBoardingLayout({ children, step }: Props) {
return (
<div
className={classnames(
["onboarding"],
["onboarding--first", step === 0],
["onboarding--second", step === 1],
["onboarding--third", step === 2]
)}>
<section>
<div>
<Logotype width={111} />
</div>
{children}
<footer>
<ul>
<li {...attributes({ "aria-selected": step === 0 })}></li>
<li {...attributes({ "aria-selected": step === 1 })}></li>
<li {...attributes({ "aria-selected": step === 2 })}></li>
</ul>
</footer>
</section>
<BackgroundImage />
</div>
);
}

View File

@ -1,33 +0,0 @@
import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon";
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
type Props = {
value: "success" | "failure" | "warning";
};
export function OnBoardingStatusIcon({ value }: Props) {
switch (value) {
case "success":
return <SuccessCheckIcon variant="primary"></SuccessCheckIcon>;
case "failure":
return <ErrorCircleIcon />;
case "warning":
return (
<span className="onboarding-healthCheck-icon">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.5C3.85775 15.5 0.5 12.1423 0.5 8C0.5 3.85775 3.85775 0.5 8 0.5C12.1423 0.5 15.5 3.85775 15.5 8C15.5 12.1423 12.1423 15.5 8 15.5ZM4.25 7.25V8.75H11.75V7.25H4.25Z"
fill="#FF8447"
/>
</svg>
</span>
);
}
}

View File

@ -1,100 +0,0 @@
import { AlphaIcon } from "./AlphaIcon";
import { AlphaText } from "../AlphaText/AlphaText";
import { Modal, SimpleText } from "@codex-storage/marketplace-ui-components";
import { useState } from "react";
import { ArrowRight } from "lucide-react";
type Props = {
onNextStep: () => void;
};
export function OnBoardingStepOne({ onNextStep }: Props) {
const [modal, setModal] = useState(false);
const onLegalDisclaimerOpen = () => setModal(true);
const onLegalDisclaimerClose = () => setModal(false);
return (
<>
<div className="index-column-section">
<div>
<AlphaIcon variant="error" />
</div>
<div className="index-alphaText">
<p>
<AlphaText variant="default"></AlphaText>
</p>
<p>
<SimpleText className="index-version" variant="normal">
{import.meta.env.PACKAGE_VERSION}
</SimpleText>
</p>
<p>
<SimpleText
className="index-disclaimer"
variant="error"
onClick={onLegalDisclaimerOpen}>
<a className="index-link">Legal Disclaimer</a>
</SimpleText>
</p>
</div>
</div>
<div className="index-column-section">
<h3 className="index-mainTitle">
Hello,
<br /> Welcome to <span className="index-codex">Codex</span>{" "}
<span className="index-vault">Vault</span>
</h3>
<p className="index-description">
<SimpleText variant="light">
Codex is a durable, decentralised data storage protocol, created so
the world community can preserve its most important knowledge
without risk of censorship.
</SimpleText>
</p>
</div>
<div className="index-column-section ">
<SimpleText variant="primary">
<a onClick={onNextStep} className="index-link index-getStarted">
Lets get started <ArrowRight></ArrowRight>
</a>
</SimpleText>
<Modal onClose={onLegalDisclaimerClose} open={modal}>
<h1 className="disclaimer-title" style={{ marginTop: 0 }}>
Disclaimer
</h1>
<p className="disclaimer-text">
The website and the content herein is not intended for public use
and is for informational and demonstration purposes only.
</p>
<br />
<p className="disclaimer-text">
The website and any associated functionalities are provided on an
as is basis without any guarantees, warranties, or representations
of any kind, either express or implied. The website and any
associated functionalities may not reflect the final version of the
project and is subject to changes, updates, or removal at any time
and without notice.
</p>
<br />
<p className="disclaimer-text">
By accessing and using this website, you agree that we, Logos
Collective Association and its affiliates, will not be liable for
any direct, indirect, incidental, or consequential damages arising
from the use of, or inability to use, this website. Any data,
content, or interactions on this site are non-binding and should not
be considered final or actionable. Your use of this website is at
your sole risk.
</p>
</Modal>
</div>
</>
);
}

View File

@ -1,118 +0,0 @@
.onboarding-check {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.onboarding-check-icon--valid {
color: var(--codex-color-primary);
}
.onboarding-check-icon--invalid {
color: rgb(var(--codex-color-error));
}
.onboarding-check-icon--warning {
color: rgb(var(--codex-color-warning));
}
.onboarding-check-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.onboarding-check-refresh {
cursor: pointer;
}
.onboarding-group {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.onboarding-codex {
margin-top: 1rem;
padding-left: 1rem;
}
.onboarding-deviceCheck {
margin-top: 1rem;
margin-bottom: 1rem;
font-family: Azeret Mono;
font-size: 14px;
font-weight: 400;
line-height: 16.34px;
display: inline-block;
}
.onboarding--deviceCheck-block {
flex: 0.3;
}
.onboarding-displayName {
margin-bottom: 1.75rem;
}
.onboarding-healthCheck-item {
display: flex;
align-items: center;
padding: 16px 0;
gap: 16px;
border-top: 1px solid #3c3d3e;
border-bottom: 1px solid #3c3d3e;
}
.onboarding-healthCheck-itemText {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.011em;
}
.onboarding-healthChecks {
margin-bottom: 32px;
}
.onboarding-healthCheck-icon {
display: flex;
align-items: center;
width: 20px;
height: 20px;
justify-content: center;
}
.onboarding-refresh {
position: relative;
top: 14px;
cursor: pointer;
}
.onboarding-refresh--fetching {
animation: rotate 2s linear infinite;
}
.onboarding-portContainer,
.onboarding-addressContainer {
position: relative;
}
.onboarding--addressSuccessIcon {
position: absolute;
top: 53px;
bottom: 0;
right: 18px;
}
@keyframes rotate {
from {
transform: rotate(0deg); /* Start at 0 degrees */
}
to {
transform: rotate(360deg); /* End at 360 degrees */
}
}

View File

@ -1,219 +0,0 @@
import { classnames } from "../../utils/classnames";
import "./OnBoardingStepThree.css";
import { usePortForwarding } from "../../hooks/usePortForwarding";
import { Input, SimpleText } from "@codex-storage/marketplace-ui-components";
import { ClipboardEvent, useEffect, useState } from "react";
import { CodexSdk } from "../../sdk/codex";
import { useQueryClient } from "@tanstack/react-query";
import { usePersistence } from "../../hooks/usePersistence";
import { useDebug } from "../../hooks/useDebug";
import { DebugUtils } from "../../utils/debug";
import { AlphaIcon } from "./AlphaIcon";
import { OnBoardingUtils } from "../../utils/onboarding";
import { RefreshIcon } from "../RefreshIcon/RefreshIcon";
import { HealthCheckIcon } from "./HealthCheckIcon";
import { HealthCheckItem } from "./HealthCheckItem";
import { Strings } from "../../utils/strings";
import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon";
import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon";
type Props = {
online: boolean;
onStepValid: (valid: boolean) => void;
};
const throwOnError = false;
const defaultPort = 8070;
export function OnBoardingStepThree({ online, onStepValid }: Props) {
const codex = useDebug(throwOnError);
const portForwarding = usePortForwarding(codex.data);
const persistence = usePersistence(codex.isSuccess);
const [url, setUrl] = useState(CodexSdk.url);
const queryClient = useQueryClient();
const [isInvalid, setIsInvalid] = useState(false);
useEffect(() => {
onStepValid(online && portForwarding.enabled && codex.isSuccess);
}, [portForwarding.enabled, codex.isSuccess, onStepValid, online]);
useEffect(() => {
if (codex.isSuccess) {
persistence.refetch();
portForwarding.refetch();
}
}, [persistence, portForwarding, codex.isSuccess]);
const onAddressChange = (e: React.FormEvent<HTMLInputElement>) => {
const [, port] = Strings.splitURLAndPort(url);
const element = e.currentTarget;
const value = e.currentTarget.value;
if (
value.startsWith("http://") === false &&
value.startsWith("https://") === false
) {
setIsInvalid(true);
return;
}
console.info("isInvalid", isInvalid);
setIsInvalid(!element.checkValidity());
setUrl(value + ":" + port);
};
const onPaste = (e: ClipboardEvent) => {
const text = e.clipboardData.getData("text");
try {
new URL(text);
setUrl(text);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
// Nothing to do here
}
};
const onPortChange = (e: React.FormEvent<HTMLInputElement>) => {
const [address] = Strings.splitURLAndPort(url);
const element = e.currentTarget;
const value = element.value;
setUrl(address + ":" + value);
};
const onSave = () => {
if (isInvalid === true) {
return;
}
CodexSdk.updateURL(url)
.then(() => queryClient.invalidateQueries())
.then(() => codex.refetch());
};
let forwardingPortValue = defaultPort;
if (codex.isSuccess && codex.data) {
const port = DebugUtils.getTcpPort(codex.data);
if (!port.error) {
forwardingPortValue = port.data;
}
}
const displayName = OnBoardingUtils.getDisplayName();
const [address, port] = Strings.splitURLAndPort(url);
return (
<>
<div className="index-column-section onboarding--deviceCheck-block">
<div>
<AlphaIcon variant="primary" />
</div>
<SimpleText variant="primary" className="onboarding-deviceCheck">
<span>
Connection /<br />
Device Health Check
</span>
</SimpleText>
</div>
<div className="index-column-section">
<h3 className="index-mainTitle onboarding-displayName">
Nice to meet {displayName},<br />
Lets establish our connection.
</h3>
<div className="onboarding-group">
<div className="onboarding-addressAndPort">
<div className="onboarding-addressContainer">
<Input
onPaste={onPaste}
id="url"
type="url"
label="Address"
isInvalid={isInvalid}
onChange={onAddressChange}
value={address}
placeholder="127.0.0.1"></Input>
{isInvalid ? (
<ErrorCircleIcon className="onboarding--addressSuccessIcon" />
) : (
<SuccessCheckIcon
className="onboarding--addressSuccessIcon"
variant="default"
/>
)}
</div>
<div className="onboarding-portContainer">
<Input
inputClassName="onboarding-port"
id="port"
label="Port"
type="number"
onChange={onPortChange}
value={port}
placeholder="8080"></Input>
<SuccessCheckIcon
className="onboarding--addressSuccessIcon"
variant="default"></SuccessCheckIcon>
</div>
<RefreshIcon
onClick={onSave}
className={classnames(
["onboarding-refresh"],
[
"onboarding-refresh--fetching",
portForwarding.isFetching || codex.isPending,
]
)}></RefreshIcon>
</div>
</div>
<ul className="onboarding-portForwardingHelp">
<li>Port forwarding should be default {forwardingPortValue}.</li>
</ul>
<div className="onboarding-healthChecks">
<div className="onboarding-healthCheck-item">
<span className="onboarding-healthCheck-icon">
<HealthCheckIcon />
</span>
<span className="onboarding-healthCheck-itemText">
Health Check
</span>
</div>
<div className="onboarding-healthCheck-item">
<HealthCheckItem
value={online ? "success" : "failure"}
text="Internet connection"
/>
</div>
<div className="onboarding-healthCheck-item">
<HealthCheckItem
value={portForwarding.enabled ? "success" : "failure"}
text="Port forwarding"
/>
</div>
<div className="onboarding-healthCheck-item">
<HealthCheckItem
value={codex.isSuccess ? "success" : "failure"}
text="Codex connection"
/>
</div>
<div className="onboarding-healthCheck-item">
<HealthCheckItem
value={codex.isSuccess ? "success" : "warning"}
text="Marketplace"
/>
</div>
</div>
</div>
</>
);
}

View File

@ -1,44 +0,0 @@
.onboarding-personalization {
margin-top: 1rem;
font-family: Azeret Mono;
font-size: 14px;
font-weight: 400;
line-height: 16.34px;
display: inline-block;
}
.onboarding--personalization-block {
flex: 0.3;
}
.onboarding-addressAndPort {
display: flex;
gap: 1.5rem;
align-items: center;
}
.onboarding-port {
width: 150px;
}
.onboarding-port::-webkit-outer-spin-button,
.onboarding-port::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.onboarding-port[type="number"] {
-moz-appearance: textfield;
}
.onboarding-portForwardingHelp {
font-family: Azeret Mono;
font-size: 12px;
font-weight: 400;
line-height: 14px;
color: #828282;
padding-left: 1.25rem;
margin-top: 1.75rem;
margin-bottom: 3rem;
}

View File

@ -1,46 +0,0 @@
import { Input, SimpleText } from "@codex-storage/marketplace-ui-components";
import { ChangeEvent, useState } from "react";
import { AlphaIcon } from "./AlphaIcon";
import "./OnBoardingStepTwo.css";
import { OnBoardingUtils } from "../../utils/onboarding";
type Props = {
onStepValid: (valid: boolean) => void;
};
export function OnBoardingStepTwo({ onStepValid }: Props) {
const [displayName, setDisplayName] = useState("");
const onDisplayNameChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
OnBoardingUtils.setDisplayName(value);
setDisplayName(value);
onStepValid(!!value);
};
return (
<>
<div className="index-column-section onboarding--personalization-block">
<div>
<AlphaIcon variant="primary" />
</div>
<SimpleText variant="primary" className="onboarding-personalization">
<span>Personalization</span>
</SimpleText>
</div>
<div className="index-column-section">
<h3 className="index-mainTitle">
Lets get you setup. <br />
What do you want to be called?
</h3>
<div className="index-displayName">
<Input
onChange={onDisplayNameChange}
label="Display name"
id="displayName"
value={displayName}></Input>
</div>
</div>
</>
);
}

View File

@ -1,35 +0,0 @@
import { ReactNode, useState } from "react";
import { Menu, MenuItem } from "../Menu/Menu";
import { AppBar } from "../AppBar/AppBar";
import "./page.css";
type Props = {
children: ReactNode;
items: MenuItem[];
version?: string;
};
export function Page({ children, items, version = "" }: Props) {
const [open, setOpen] = useState(false);
const onClose = () => setOpen(false);
const onExpand = () => setOpen(true);
return (
<div className="page">
<Menu
expanded={open}
onClose={onClose}
items={items}
version={version}></Menu>
<main className="page-main">
<AppBar onExpand={onExpand} />
{children}
</main>
</div>
);
}

View File

@ -1,8 +0,0 @@
.page {
display: flex;
flex: 1;
}
.page-main {
flex: 1;
}

View File

@ -1,5 +1,11 @@
.peerCountry {
.peer-country {
display: flex;
align-items: center;
gap: 1rem;
span:first-child {
background-color: #141414;
border-radius: 50%;
padding: 12px;
}
}

View File

@ -56,7 +56,7 @@ export function PeerCountryCell({ address, onPinAdd }: Props) {
return (
<Cell>
<div className="peerCountry">
<div className="peer-country">
{data ? (
<>
<span> {!!data && getFlagEmoji(data.country_iso)}</span>

View File

@ -1,24 +1,21 @@
type Props = {
variant: "primary" | "default";
className?: string;
};
export function SuccessCheckIcon({ variant, className = "" }: Props) {
export function SuccessCheckIcon({ variant }: Props) {
const color = variant === "primary" ? "#1FC16B" : "#444444";
return (
<span className={"onboarding-healthCheck-icon " + className}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.5C3.85775 15.5 0.5 12.1423 0.5 8C0.5 3.85775 3.85775 0.5 8 0.5C12.1423 0.5 15.5 3.85775 15.5 8C15.5 12.1423 12.1423 15.5 8 15.5ZM7.25225 11L12.5548 5.69675L11.4943 4.63625L7.25225 8.879L5.1305 6.75725L4.07 7.81775L7.25225 11Z"
fill={color}
/>
</svg>
</span>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.5C3.85775 15.5 0.5 12.1423 0.5 8C0.5 3.85775 3.85775 0.5 8 0.5C12.1423 0.5 15.5 3.85775 15.5 8C15.5 12.1423 12.1423 15.5 8 15.5ZM7.25225 11L12.5548 5.69675L11.4943 4.63625L7.25225 8.879L5.1305 6.75725L4.07 7.81775L7.25225 11Z"
fill={color}
/>
</svg>
);
}

View File

@ -1,10 +1,9 @@
import { SimpleText } from "@codex-storage/marketplace-ui-components";
import { CircleCheck } from "lucide-react";
export function SuccessIcon() {
return (
<SimpleText variant="primary">
<span className="text--primary">
<CircleCheck size="4rem" className="successIcon" />
</SimpleText>
</span>
);
}

View File

@ -0,0 +1,22 @@
.user-info {
display: flex;
align-items: center;
gap: 32px;
.emoji {
position: relative;
aside {
position: absolute;
top: -140px;
left: 116px;
z-index: 1;
}
.input input {
width: 64px;
text-align: center;
cursor: pointer;
}
}
}

View File

@ -0,0 +1,80 @@
import { ChangeEvent, useState } from "react";
import "./UserInfo.css";
import { OnBoardingUtils } from "../../utils/onboarding";
import { Input } from "@codex-storage/marketplace-ui-components";
import EmojiPicker, {
EmojiClickData,
EmojiStyle,
Theme,
} from "emoji-picker-react";
type Props = {
onNameChange?: (value: string) => void;
};
export function UserInfo({ onNameChange }: Props) {
const [displayName, setDisplayName] = useState(
OnBoardingUtils.getDisplayName()
);
const [emoji, setEmoji] = useState(OnBoardingUtils.getEmoji());
const [areEmojiVisible, setAreEmojiVisible] = useState(false);
const onDisplayNameChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
OnBoardingUtils.setDisplayName(value);
setDisplayName(value);
onNameChange?.(value);
};
const onDisplayEmoji = () => setAreEmojiVisible(!areEmojiVisible);
const onEmojiClick = (emojiData: EmojiClickData) => {
setEmoji(emojiData.emoji);
OnBoardingUtils.setEmoji(emojiData.emoji);
setAreEmojiVisible(false);
};
return (
<div className="user-info">
<div className="emoji">
{areEmojiVisible && (
<EmojiPicker
emojiStyle={EmojiStyle.NATIVE}
theme={Theme.DARK}
lazyLoadEmojis={true}
onEmojiClick={onEmojiClick}
categories={
[
"smileys_people",
"animals_nature",
"food_drink",
"travel_places",
"activities",
"objects",
"symbols",
"flags",
] as any
}
/>
)}
<div>
<Input
onChange={onDisplayNameChange}
onClick={onDisplayEmoji}
label="Account Emoji"
readOnly={true}
id="emoji"
value={emoji}></Input>
</div>
</div>
<div>
<Input
onChange={onDisplayNameChange}
label="Preferred name"
id="displayName"
value={displayName}></Input>
</div>
</div>
);
}

View File

@ -0,0 +1,34 @@
.versions {
display: flex;
gap: 32px;
> svg {
position: relative;
top: 6px;
}
> div {
width: 50px;
text-align: right;
p {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.011em;
color: #99a0ae;
}
small {
font-family: Inter;
font-size: 10px;
font-weight: 400;
line-height: 12.1px;
letter-spacing: 0.01em;
color: white;
text-transform: uppercase;
white-space: nowrap;
}
}
}

View File

@ -0,0 +1,28 @@
import { useDebug } from "../../hooks/useDebug";
import { AlphaText } from "../AlphaText/AlphaText";
import { AlphaIcon } from "../OnBoarding/AlphaIcon";
import "./Versions.css";
const throwOnError = true;
export function Versions() {
const debug = useDebug(throwOnError);
const parts = debug.data?.codex.version.split("\n") || [""];
const version = parts[parts.length - 1];
return (
<div className="versions">
<AlphaIcon variant="error" />
<div>
<p>Client</p>
<small>VER. {version}</small>
</div>
<div>
<p>Vault</p>
<small>VER. {import.meta.env.PACKAGE_VERSION}</small>
<AlphaText variant="failure" width={37}></AlphaText>
</div>
</div>
);
}

View File

@ -0,0 +1,15 @@
export function WarningIcon() {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.5C3.85775 15.5 0.5 12.1423 0.5 8C0.5 3.85775 3.85775 0.5 8 0.5C12.1423 0.5 15.5 3.85775 15.5 8C15.5 12.1423 12.1423 15.5 8 15.5ZM4.25 7.25V8.75H11.75V7.25H4.25Z"
fill="#FF8447"
/>
</svg>
);
}

View File

@ -1,4 +1,3 @@
import { SimpleText } from "@codex-storage/marketplace-ui-components";
import "./Welcome.css";
import { Link } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
@ -8,12 +7,12 @@ export function Welcome() {
<div className="welcome">
<p className="welcome-title">Welcome to Codex Marketplace</p>
<div className="welcome-body">
<SimpleText variant="light">
<span>
Begin your journey with Codex by uploading new files for testing.
Experience the power of our decentralized data storage platform and
explore its features. Your feedback is invaluable as we continue to
improve!
</SimpleText>
</span>
</div>
<Link to="/dashboard/help" className="welcome-link">

View File

@ -46,6 +46,7 @@
--codex-input-border-color: #494949;
--codex-input-background: #232323;
--codex-input-color-error: #fb3748;
--codex-row-gap: 16px;
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%;
@ -136,11 +137,6 @@ main {
max-width: 100%;
}
hr {
border: 0.1px solid var(--codex-border-color);
width: 100%;
}
ul {
margin: 0;
padding: 0;
@ -158,12 +154,6 @@ pre {
word-break: break-word;
}
a {
text-decoration: inherit;
color: var(--codex-color);
transition: opacity 0.35s;
}
a[aria-disabled] {
opacity: 0.6;
cursor: not-allowed;
@ -175,10 +165,6 @@ a[aria-disabled] {
max-width: 100%;
}
.page {
max-width: 100%;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
@ -186,3 +172,12 @@ input:-webkit-autofill:active {
-webkit-transition-delay: 9999s;
transition-delay: 9999s;
}
.row {
display: flex;
align-items: center;
}
.gap {
gap: var(--codex-row-gap);
}

View File

@ -11,6 +11,8 @@
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as OnboardingNameImport } from './routes/onboarding-name'
import { Route as OnboardingChecksImport } from './routes/onboarding-checks'
import { Route as DashboardImport } from './routes/dashboard'
import { Route as IndexImport } from './routes/index'
import { Route as DashboardIndexImport } from './routes/dashboard/index'
@ -32,6 +34,18 @@ import { Route as DashboardAboutImport } from './routes/dashboard/about'
// Create/Update Routes
const OnboardingNameRoute = OnboardingNameImport.update({
id: '/onboarding-name',
path: '/onboarding-name',
getParentRoute: () => rootRoute,
} as any)
const OnboardingChecksRoute = OnboardingChecksImport.update({
id: '/onboarding-checks',
path: '/onboarding-checks',
getParentRoute: () => rootRoute,
} as any)
const DashboardRoute = DashboardImport.update({
id: '/dashboard',
path: '/dashboard',
@ -158,6 +172,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardImport
parentRoute: typeof rootRoute
}
'/onboarding-checks': {
id: '/onboarding-checks'
path: '/onboarding-checks'
fullPath: '/onboarding-checks'
preLoaderRoute: typeof OnboardingChecksImport
parentRoute: typeof rootRoute
}
'/onboarding-name': {
id: '/onboarding-name'
path: '/onboarding-name'
fullPath: '/onboarding-name'
preLoaderRoute: typeof OnboardingNameImport
parentRoute: typeof rootRoute
}
'/dashboard/about': {
id: '/dashboard/about'
path: '/about'
@ -320,6 +348,8 @@ const DashboardRouteWithChildren = DashboardRoute._addFileChildren(
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteWithChildren
'/onboarding-checks': typeof OnboardingChecksRoute
'/onboarding-name': typeof OnboardingNameRoute
'/dashboard/about': typeof DashboardAboutRoute
'/dashboard/analytics': typeof DashboardAnalyticsRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesRoute
@ -340,6 +370,8 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/onboarding-checks': typeof OnboardingChecksRoute
'/onboarding-name': typeof OnboardingNameRoute
'/dashboard/about': typeof DashboardAboutRoute
'/dashboard/analytics': typeof DashboardAnalyticsRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesRoute
@ -362,6 +394,8 @@ export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteWithChildren
'/onboarding-checks': typeof OnboardingChecksRoute
'/onboarding-name': typeof OnboardingNameRoute
'/dashboard/about': typeof DashboardAboutRoute
'/dashboard/analytics': typeof DashboardAnalyticsRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesRoute
@ -385,6 +419,8 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/dashboard'
| '/onboarding-checks'
| '/onboarding-name'
| '/dashboard/about'
| '/dashboard/analytics'
| '/dashboard/availabilities'
@ -404,6 +440,8 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/onboarding-checks'
| '/onboarding-name'
| '/dashboard/about'
| '/dashboard/analytics'
| '/dashboard/availabilities'
@ -424,6 +462,8 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/dashboard'
| '/onboarding-checks'
| '/onboarding-name'
| '/dashboard/about'
| '/dashboard/analytics'
| '/dashboard/availabilities'
@ -446,11 +486,15 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRoute: typeof DashboardRouteWithChildren
OnboardingChecksRoute: typeof OnboardingChecksRoute
OnboardingNameRoute: typeof OnboardingNameRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRoute: DashboardRouteWithChildren,
OnboardingChecksRoute: OnboardingChecksRoute,
OnboardingNameRoute: OnboardingNameRoute,
}
export const routeTree = rootRoute
@ -466,7 +510,9 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
"/dashboard"
"/dashboard",
"/onboarding-checks",
"/onboarding-name"
]
},
"/": {
@ -493,6 +539,12 @@ export const routeTree = rootRoute
"/dashboard/"
]
},
"/onboarding-checks": {
"filePath": "onboarding-checks.tsx"
},
"/onboarding-name": {
"filePath": "onboarding-name.tsx"
},
"/dashboard/about": {
"filePath": "dashboard/about.tsx",
"parent": "/dashboard"

View File

@ -1,200 +1,25 @@
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
import "./dashboard.css";
import { Page } from "../components/Page/Page";
import { HomeIcon } from "../components/Menu/HomeIcon";
import { WalletIcon } from "../components/Menu/WalletIcon";
import { NodesIcon } from "../components/Menu/NodesIcon";
import { FilesIcon } from "../components/Menu/FilesIcon";
import { AnalyticsIcon } from "../components/Menu/AnalyticsIcon";
import { PurchaseIcon } from "../components/Menu/PurchaseIcon";
import { PeersIcon } from "../components/Menu/PeersIcon";
import { LogsIcon } from "../components/Menu/LogsIcon";
import { MenuItem, MenuItemComponentProps } from "../components/Menu/Menu";
import { HelpIcon } from "../components/Menu/HelpIcon";
import { DisclaimerIcon } from "../components/Menu/DisclaimerIcon";
import { SettingsIcon } from "../components/Menu/SettingsIcon";
import { HostIcon } from "../components/Menu/HostIcon";
import { DeviceIcon } from "../components/Menu/DeviceIcon";
import { createFileRoute, Outlet } from "@tanstack/react-router";
import "./layout.css";
import { Menu } from "../components/Menu/Menu";
import { useState } from "react";
import { AppBar } from "../components/AppBar/AppBar";
const Layout = () => {
const items = [
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard" activeOptions={{ exact: true }} {...p}>
<span className="menu-icon">
<HomeIcon />
</span>
<span className="menu-text">Dashboard</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/wallet" {...p}>
<span className="menu-icon">
<WalletIcon />
</span>
<span className="menu-text">Wallet</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/files" {...p}>
<span className="menu-icon">
<FilesIcon />
</span>
<span className="menu-text">Files</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link
to="/dashboard/nodes"
{...p}
disabled={true}
aria-disabled={true}
data-title="Coming soon">
<span className="menu-icon">
<NodesIcon variant="default" />
</span>
<span className="menu-text">Nodes</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link
to="/dashboard/analytics"
{...p}
disabled={true}
aria-disabled={true}
title="Coming soon"
data-title="Coming soon">
<span className="menu-icon">
<AnalyticsIcon />
</span>
<span className="menu-text">Analytics</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link
to="/dashboard/device"
{...p}
disabled={true}
aria-disabled={true}
title="Coming soon"
data-title="Coming soon">
<span className="menu-icon">
<DeviceIcon />
</span>
<span className="menu-text">Devices</span>
</Link>
),
},
{
type: "separator",
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/purchases" {...p}>
<span className="menu-icon">
<PurchaseIcon />
</span>
<span className="menu-text">Purchases</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/availabilities" {...p}>
<span className="menu-icon">
<HostIcon />
</span>
<span className="menu-text">Host</span>
</Link>
),
},
{
type: "separator",
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/peers" {...p}>
<span className="menu-icon">
<PeersIcon />
</span>
<span className="menu-text">Peers</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/logs" {...p}>
<span className="menu-icon">
<LogsIcon />
</span>
<span className="menu-text">Log</span>
</Link>
),
},
{
type: "space",
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/settings" {...p}>
<span className="menu-icon">
<SettingsIcon />
</span>
<span className="menu-text">Settings</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/help" {...p}>
<span className="menu-icon">
<HelpIcon />
</span>
<span className="menu-text">Help</span>
</Link>
),
},
{
type: "item",
Component: (p: MenuItemComponentProps) => (
<Link to="/dashboard/disclaimer" {...p}>
<span className="menu-icon">
<DisclaimerIcon />
</span>
<span className="menu-text">Disclaimer</span>
</Link>
),
},
] satisfies MenuItem[];
const [open, setOpen] = useState(false);
const onClose = () => setOpen(false);
const onExpand = () => setOpen(true);
return (
<Page
children={<Outlet />}
items={items}
version={import.meta.env.PACKAGE_VERSION}
/>
<div className="layout">
<Menu expanded={open} onClose={onClose}></Menu>
<main>
<AppBar onExpand={onExpand} />
<Outlet />
</main>
</div>
);
};

View File

@ -1,34 +1,35 @@
.dashboard-welcomeContainer {
.dashboard {
padding: 24px 48px;
display: flex;
justify-content: space-between;
}
.dashboard-welcome-avatarContainer {
display: flex;
gap: 16px;
}
h3 {
font-family: Inter;
font-size: 12px;
font-weight: 700;
line-height: 14.52px;
letter-spacing: 0.01em;
color: #969696cc;
text-transform: uppercase;
}
.dashboard-welcome-avatarTitle {
font-family: Inter;
font-size: 12px;
font-weight: 700;
line-height: 14.52px;
letter-spacing: 0.01em;
color: #969696cc;
text-transform: uppercase;
}
h4 {
font-family: Inter;
font-size: 32px;
font-weight: 400;
line-height: 38.73px;
letter-spacing: 0.01em;
color: white;
}
.dashboard-welcome-avatarSubtitle {
font-family: Inter;
font-size: 32px;
font-weight: 400;
line-height: 38.73px;
letter-spacing: 0.01em;
color: white;
}
.dashboard-welcome-alpha {
position: relative;
top: 6px;
.emoji {
border-radius: 50%;
width: 52px;
height: 52px;
background-color: #4a9a73;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
}
}

View File

@ -10,19 +10,14 @@ import { Download } from "../../components/Download/Download.tsx";
import { ManifestFetch } from "../../components/ManifestFetch/ManifestFetch.tsx";
import { OnBoardingUtils } from "../../utils/onboarding.ts";
import "./index.css";
import { AlphaIcon } from "../../components/OnBoarding/AlphaIcon.tsx";
import { AlphaText } from "../../components/AlphaText/AlphaText.tsx";
import { useDebug } from "../../hooks/useDebug.ts";
import { Versions } from "../../components/Versions/Versions.tsx";
export const Route = createFileRoute("/dashboard/")({
component: Dashboard,
});
const throwOnError = false;
function Dashboard() {
const queryClient = useQueryClient();
const debug = useDebug(throwOnError);
const onSuccess = () => {
queryClient.invalidateQueries({ queryKey: ["cids"] });
@ -30,62 +25,21 @@ function Dashboard() {
const username = OnBoardingUtils.getDisplayName();
const parts = debug.data?.codex.version.split("\n") || [""];
const version = parts[parts.length - 1];
const emoji = OnBoardingUtils.getEmoji();
return (
<>
<div className="dashboard-welcomeContainer">
<div className="dashboard-welcome-avatarContainer">
<picture>
<source
srcSet="/img/avatar@4x.webp 4x,
/img/avatar@3x.webp 3x,
/img/avatar@2x.webp 2x,
/img/avatar@1.5x.webp 1.5x,
/img/avatar.webp 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/webp"
/>
<source
srcSet="/img/avatar@4x.png 4x,
/img/avatar@3x.png 3x,
/img/avatar@2x.png 2x,
/img/avatar@1.5x.png 1.5x,
/img/avatar.png 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/png"
/>
<img src="/img/avatar.png" height={52} width={52} alt="Avatar" />
</picture>
<div>
<div className="dashboard-welcome-avatarTitle">Welcome back,</div>
<div className="dashboard-welcome-avatarSubtitle">{username}</div>
</div>
</div>
<div className="dashboard-welcome-versions">
<AlphaIcon variant="error" className="dashboard-welcome-alpha" />
<div className="dashboard-welcome-versionContainer">
<p className="dashboard-welcome-versionTitle">Client</p>
<p className="dashboard-welcome-versionValue">VER. {version}</p>
</div>
<div className="dashboard-welcome-versionContainer">
<p className="dashboard-welcome-versionTitle">Vault</p>
<p className="dashboard-welcome-versionValue">
VER. {import.meta.env.PACKAGE_VERSION}
</p>
<AlphaText
variant="failure"
className="dashboard-welcome-alphaText"
width={37}></AlphaText>
</div>
</div>
</div>
<div className="dashboard">
<div className="row gap">
<div className="emoji">{emoji}</div>
<div>
<h3>Welcome back,</h3>
<h4>{username}</h4>
</div>
</div>
<Versions />
</div>
<div>
<div>
<ErrorBoundary
fallback={({ error }) => (

View File

@ -1,41 +1,228 @@
.peers-map {
max-width: 1000px;
width: 100%;
}
.peers-table {
margin-top: 1rem;
width: calc(100% - 4rem);
max-width: calc(1000px - 4rem);
}
.peers {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 4rem;
padding-left: 2rem;
padding-right: 2rem;
max-width: 1320px;
margin-left: auto;
margin-right: auto;
width: calc(100% - 2 * 24px);
padding: 24px;
> div {
max-width: 1320px;
}
> div:first-child {
width: calc(100% - 128px - 16px);
border: 1px solid #96969633;
padding: 16px 16px 16px 128px;
border-radius: 16px;
position: relative;
height: 600px;
circle[fill="#d6ff79"] {
stroke: var(--codex-color-primary);
stroke-width: 0.6px;
fill: #141414;
animation: circle-pulse 1.5s infinite;
}
ul {
list-style-type: none;
width: 71px;
position: absolute;
left: 16px;
top: 16px;
li {
border-bottom: 1px solid #969696cc;
padding: 16px 0;
text-align: right;
}
li:first-child {
font-size: 20px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.006em;
text-align: left;
color: #7b7b7b;
}
li:not(:first-child) {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.011em;
position: relative;
}
li:not(:first-child)::before {
content: " ";
border: 4px solid var(--codex-color-primary);
border-radius: 50%;
height: 8px;
width: 8px;
display: inline-block;
position: absolute;
left: 0;
top: 20px;
}
li:nth-child(3)::before {
border-width: 5px;
height: 11px;
width: 11px;
top: 18px;
}
li:nth-child(4)::before {
border-width: 6px;
height: 12px;
width: 12px;
top: 16px;
}
}
.connections {
background-color: #232323;
border: 1px solid #96969633;
border-radius: 16px;
position: absolute;
bottom: 16px;
left: 16px;
width: 280px;
padding: 16px;
header {
display: flex;
align-items: center;
gap: 8px;
color: #969696;
padding-bottom: 16px;
border-bottom: 1px solid #96969633;
span {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.011em;
color: white;
}
}
main {
> div {
position: relative;
width: 350px;
height: 175px;
overflow: hidden;
transform: scale(0.73);
margin: auto;
left: -32px;
*,
&::before {
box-sizing: border-box;
}
&::before,
&::after {
position: absolute;
}
&::before {
content: "";
width: inherit;
height: inherit;
border: 45px solid #323232;
border-bottom: none;
border-top-left-radius: 175px;
border-top-right-radius: 175px;
}
div {
position: absolute;
top: 100%;
width: inherit;
height: inherit;
border: 45px solid #323232;
border-top: none;
border-bottom-left-radius: 175px;
border-bottom-right-radius: 175px;
transform-origin: 50% 0;
}
div:nth-child(1) {
border-color: var(--codex-color-primary);
transform: rotate(calc(var(--codex-peers-percent) * 1deg));
background-color: transparent;
}
}
span {
font-family: Inter;
font-size: 38.67px;
font-weight: 500;
line-height: 48.34px;
letter-spacing: -0.005em;
text-align: center;
position: absolute;
bottom: 0;
left: calc(50% - 24px);
}
}
footer {
border-top: 1px solid #96969633;
padding-top: 16px;
display: flex;
align-items: center;
gap: 8px;
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.006em;
}
}
}
> div:nth-child(2) {
width: calc(100% - 64px);
}
table {
td:last-child {
div {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 8px;
&.status--active {
color: #1daf61;
background-color: #6fcb9433;
}
&.status--inactive {
color: #fb3748;
background-color: #fb374833;
}
}
}
}
}
.peers circle[fill="#d6ff79"] {
/* fill: yellow; */
animation: dash 3s linear infinite;
stroke: white;
stroke-width: 0.6px;
stroke-dasharray: 0.3;
}
@keyframes dash {
from {
stroke-dashoffset: 2;
@keyframes circle-pulse {
0% {
opacity: 1; /* Fully opaque */
}
to {
stroke-dashoffset: 0;
}
}
@keyframes circleAn {
to {
/* stroke-dashoffset: 100px; */
50% {
r: 2; /* Increased radius */
opacity: 1; /* Slightly transparent */
}
100% {
opacity: 1; /* Fully opaque */
}
}

View File

@ -11,10 +11,17 @@ import "./peers.css";
import { CodexSdk } from "../../sdk/codex";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import { PeersIcon } from "../../components/Menu/PeersIcon";
import { SuccessCheckIcon } from "../../components/SuccessCheckIcon/SuccessCheckIcon";
import { ErrorCircleIcon } from "../../components/ErrorCircleIcon/ErrorCircleIcon";
// This function accepts the same arguments as DottedMap in the example above.
const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" });
type CustomCSSProperties = React.CSSProperties & {
"--codex-peers-percent": number;
};
const Peers = () => {
const [pins, setPins] = useState<[PeerPin, number][]>([]);
const { data } = useQuery({
@ -61,10 +68,10 @@ const Peers = () => {
);
const svgMap = map.getSVG({
radius: 0.42,
color: "#423B38",
radius: 0.32,
color: "#969696",
shape: "circle",
backgroundColor: "#020300",
backgroundColor: "#141414",
});
const headers = ["Country", "PeerId", "Active"];
@ -79,26 +86,55 @@ const Peers = () => {
<Cell>{node.peerId}</Cell>,
<Cell>
{node.seen ? (
<div className="networkIndicator-point networkIndicator-point--online"></div>
<div className="status--active">
<SuccessCheckIcon variant="primary"></SuccessCheckIcon> Active
</div>
) : (
<div className="networkIndicator-point networkIndicator-point--offline"></div>
<div className="status--inactive">
<ErrorCircleIcon></ErrorCircleIcon> Inactive
</div>
)}
</Cell>,
]}></Row>
)) || [];
const actives =
data?.table.nodes.reduce((acc, cur) => acc + (cur.seen ? 1 : 0), 0) || 0;
const total = data?.table.nodes.length || 1;
const styles: CustomCSSProperties = {
"--codex-peers-percent": (actives / total) * 180,
};
return (
<div className="peers">
{/* <img
src={`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`}
className="peers-map"
/> */}
<div>
<ul>
<li>Legend</li>
<li>1-3</li>
<li>3-5</li>
<li>5 +</li>
</ul>
<div className="connections">
<header>
<PeersIcon></PeersIcon>
<span>Connections</span>
</header>
<main style={styles}>
<div>
<div></div>
<span>{actives}</span>
</div>
</main>
<footer>
<SuccessCheckIcon variant="primary"></SuccessCheckIcon>{" "}
<span>Peer connections in Good standing. </span>
</footer>
</div>
<div dangerouslySetInnerHTML={{ __html: svgMap }}></div>
</div>
<div
className="peers-map"
dangerouslySetInnerHTML={{ __html: svgMap }}></div>
<Table headers={headers} rows={rows} className="peers-table" />
<Table headers={headers} rows={rows} />
</div>
);
};

View File

@ -1,11 +1,49 @@
.settings {
border-radius: var(--codex-border-radius);
border: 1px solid var(--codex-border-color);
background-color: var(--codex-background-secondary);
padding: 1rem 1.5rem;
margin: 1rem 1.5rem;
}
padding: 24px 48px;
header {
display: flex;
align-items: center;
justify-content: space-between;
}
main {
margin-top: 16px;
padding: 16px;
max-width: 800px;
z-index: 1;
position: relative;
h3 {
font-family: Inter;
font-size: 18px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.015em;
margin-bottom: 16px;
}
.user-info {
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid #96969633;
}
}
.background-img {
top: 200px;
right: -400px;
}
.refresh {
top: 17px;
}
.address svg {
top: 55px;
}
}
/*
.settings-title {
font-weight: bold;
font-size: 1.125rem;
@ -19,4 +57,4 @@
.settings-debug-loader {
margin: auto;
}
} */

View File

@ -1,16 +1,26 @@
import { createFileRoute } from "@tanstack/react-router";
import "./settings.css";
import { LogLevel } from "../../components/LogLevel/LogLevel";
import { Debug } from "../../components/Debug/Debug";
import { CodexUrlSettings } from "../../components/CodexUrllSettings/CodexUrlSettings";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import { useEffect } from "react";
import { UserInfo } from "../../components/UserInfo/UserInfo";
import { HealthChecks } from "../../components/HealthChecks/HealthChecks";
import { Logotype } from "../../components/Logotype/Logotype";
import { Versions } from "../../components/Versions/Versions";
import { Logo } from "../../components/Logo/Logo";
import { BackgroundImage } from "../../components/BackgroundImage/BackgroundImage";
export const Route = createFileRoute("/dashboard/settings")({
component: () => (
<>
<div className="settings">
<div className="settings">
<header>
<div className="row gap">
<Logo height={48}></Logo>
<Logotype height={46}></Logotype>
</div>
<Versions></Versions>
</header>
<main>
<h3>Personalization</h3>
<ErrorBoundary
fallback={({ error }) => (
<ErrorPlaceholder
@ -18,11 +28,25 @@ export const Route = createFileRoute("/dashboard/settings")({
subtitle="Cannot retrieve the data."
/>
)}>
<LogLevel />
<UserInfo />
</ErrorBoundary>
</div>
<div className="settings">
<h3>Connection</h3>
<ErrorBoundary
fallback={({ error }) => (
<ErrorPlaceholder
error={error}
subtitle="Cannot retrieve the data."
/>
)}>
<HealthChecks online={true} onStepValid={() => {}} />
</ErrorBoundary>
</main>
<BackgroundImage />
{/* <div className="settings">
<ErrorBoundary
fallback={({ error }) => (
<ErrorPlaceholder
@ -57,7 +81,7 @@ export const Route = createFileRoute("/dashboard/settings")({
}}>
<Debug />
</ErrorBoundary>
</div>
</div> */}
{/* <div className="input-floating">
<input
@ -83,6 +107,6 @@ export const Route = createFileRoute("/dashboard/settings")({
Floating
</label>
</div> */}
</>
</div>
),
});

View File

@ -1,202 +0,0 @@
.index {
width: 100%;
padding: 3rem 6rem;
}
.index-logo {
position: relative;
}
.index-network {
display: flex;
position: absolute;
top: -19px;
left: -161px;
gap: 4px;
width: 200px;
justify-content: flex-end;
}
.index-network-text {
font-family: Inter;
font-size: 11px;
font-weight: 500;
line-height: 12px;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--text-soft-400, #99a0ae);
text-wrap: nowrap;
}
.index-container {
display: flex;
justify-content: space-between;
height: 100%;
}
.index-alphaText {
margin-top: 4px;
}
.index-title {
font-weight: 600;
font-size: 36px;
line-height: 43.2px;
}
.index-version {
font-weight: 500;
opacity: 0.6;
}
.index-link {
text-decoration: underline;
font-family: Azeret Mono;
font-size: 12px;
font-weight: 400;
line-height: 14px;
color: inherit;
margin-top: 16px;
cursor: pointer;
display: inline-flex;
align-items: center;
}
.index-link2:hover,
.index-link:hover {
animation-name: example;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
@keyframes example {
0% {
filter: brightness(1);
-webkit-filter: brightness(1);
}
30% {
filter: brightness(0.7);
-webkit-filter: brightness(0.7);
}
100% {
filter: brightness(1);
-webkit-filter: brightness(1);
}
}
.index-mainTitle {
font-family: Inter;
font-size: 36px;
font-weight: 300;
line-height: 43.57px;
letter-spacing: 0.01em;
}
.index-codex {
font-weight: 400;
}
.index-vault {
font-weight: 900;
text-transform: uppercase;
}
.index-description {
font-family: Azeret Mono;
font-size: 14px;
font-weight: 400;
line-height: 16.34px;
max-width: 532px;
margin-top: 20px;
}
.index-subtitle {
font-weight: 900;
font-size: 16px;
text-transform: uppercase;
line-height: 19.36px;
color: var(--codex-color-primary);
}
.index-getStarted {
font-size: 24px;
font-weight: 600;
line-height: 29.05px;
letter-spacing: 0.01em;
margin-top: 32px;
font-family: Inter;
color: #7bfbaf;
gap: 4px;
text-decoration: none;
border-bottom: 2px solid #7bfbaf;
}
.index-columnRight,
.index-column {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.index-column-section {
max-width: 500px;
}
.index-column-section {
flex: 1;
}
.index-column-section:first-child {
flex: 0.5;
}
.index-column-section:last-child {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.index-dots {
display: flex;
gap: 8px;
}
.index-dot {
width: 12px;
height: 12px;
background-color: white;
display: inline-block;
border-radius: 50%;
transition: opacity 0.35s;
}
.index-dot:not(.index-dot--active) {
opacity: 0.4;
}
.index-dot:hover {
animation-name: pulse;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
.index-dot--active {
box-shadow: 0px 0px 12px 0px #fff;
opacity: 1;
}
.index-displayName {
margin-top: 1rem;
}
@keyframes pulse {
0% {
opacity: 0.4;
}
30% {
opacity: 0.8;
}
100% {
opacity: 0.4;
}
}

View File

@ -1,16 +1,11 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import "./index.css";
import { ArrowRightCircle } from "../components/ArrowRightCircle/ArrowRightCircle";
import { useNetwork } from "../network/useNetwork";
import { Logotype } from "../components/Logotype/Logotype";
import { useState } from "react";
import { OnBoardingStepOne } from "../components/OnBoarding/OnBoardingStepOne";
import { OnBoardingStepTwo } from "../components/OnBoarding/OnBoardingStepTwo";
import { classnames } from "../utils/classnames";
import { OnBoardingStepThree } from "../components/OnBoarding/OnBoardingStepThree";
import { attributes } from "../utils/attributes";
import { OnBoardingImage } from "../components/OnBoarding/OnBoardingImage";
import { OnBoardingUtils } from "../utils/onboarding";
import { Modal } from "@codex-storage/marketplace-ui-components";
import { ArrowRight } from "lucide-react";
import { AlphaText } from "../components/AlphaText/AlphaText";
import { AlphaIcon } from "../components/OnBoarding/AlphaIcon";
import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout";
import { ArrowRightCircle } from "../components/ArrowRightCircle/ArrowRightCircle";
export const Route = createFileRoute("/")({
component: Index,
@ -22,87 +17,80 @@ export const Route = createFileRoute("/")({
});
function Index() {
const [isStepValid, setIsStepValid] = useState(true);
// const [step, setStep] = useState(OnBoardingUtils.getStep());
const [step, setStep] = useState(0);
const online = useNetwork();
const [modal, setModal] = useState(false);
const navigate = useNavigate({ from: "/" });
const onStepValid = (valid: boolean) => setIsStepValid(valid);
const onNextStep = () => {
if (!isStepValid) {
return;
}
const onLegalDisclaimerOpen = () => setModal(true);
if (step === 2) {
navigate({ to: "/dashboard" });
return;
}
const onLegalDisclaimerClose = () => setModal(false);
OnBoardingUtils.setStep(step + 1);
setStep(step + 1);
setIsStepValid(false);
};
const components = [
<OnBoardingStepOne onNextStep={onNextStep} />,
<OnBoardingStepTwo onStepValid={onStepValid} />,
<OnBoardingStepThree online={online} onStepValid={onStepValid} />,
];
// const text = online ? "Network connected" : "Network disconnected";
const onNextStep = () => navigate({ to: "/onboarding-name" });
return (
<div className="index">
<div className="index-container">
<div className="index-column">
<div className="index-column-section">
<Logotype width={111} />
</div>
{components[step]}
<div className=" ">
<div className="index-dots">
<span
className={classnames(
["index-dot"],
["index-dot--active", step === 0]
)}></span>
<span
className={classnames(
["index-dot"],
["index-dot--active", step === 1]
)}></span>
<span
className={classnames(
["index-dot"],
["index-dot--active", step === 2]
)}></span>
<>
<OnBoardingLayout defaultIsStepValid={false} step={0}>
<>
<section className="alpha">
<AlphaIcon variant="error" />
<div>
<AlphaText variant="default"></AlphaText>
<b>{import.meta.env.PACKAGE_VERSION}</b>
<a onClick={onLegalDisclaimerOpen}>Legal Disclaimer</a>
</div>
</div>
</div>
<div className="index-column">
<OnBoardingImage />
</div>
<div className="index-columnRight">
<div></div>
{/* <div className="index-logo">
<div className="index-network">
<p className="index-network-text">{text}</p>
<NetworkIcon active={online}></NetworkIcon>
</div>
<CodexLogo></CodexLogo>
</div> */}
<a
className="index-link2"
{...attributes({ "aria-disabled": !isStepValid })}
onClick={onNextStep}>
</section>
<section className="main">
<h1>
Hello,
<br /> Welcome to <b>Codex</b> <b>Vault</b>
</h1>
<p>
Codex is a durable, decentralised data storage protocol, created
so the world community can preserve its most important knowledge
without risk of censorship.
</p>
</section>
<section className="get-started">
<a onClick={onNextStep}>
Lets get started <ArrowRight></ArrowRight>
</a>
<Modal onClose={onLegalDisclaimerClose} open={modal}>
<h1>Disclaimer</h1>
<p>
The website and the content herein is not intended for public
use and is for informational and demonstration purposes only.
</p>
<br />
<p>
The website and any associated functionalities are provided on
an as is basis without any guarantees, warranties, or
representations of any kind, either express or implied. The
website and any associated functionalities may not reflect the
final version of the project and is subject to changes, updates,
or removal at any time and without notice.
</p>
<br />
<p>
By accessing and using this website, you agree that we, Logos
Collective Association and its affiliates, will not be liable
for any direct, indirect, incidental, or consequential damages
arising from the use of, or inability to use, this website. Any
data, content, or interactions on this site are non-binding and
should not be considered final or actionable. Your use of this
website is at your sole risk.
</p>
</Modal>
</section>
<a className="navigation" onClick={onNextStep}>
<ArrowRightCircle></ArrowRightCircle>
</a>
</div>
</div>
</div>
</>
</OnBoardingLayout>
</>
);
}

View File

@ -1,8 +1,12 @@
.dashboard {
padding: 1.5rem;
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
.layout {
display: flex;
flex: 1;
max-width: 100%;
> main {
flex: 1;
background-color: #141414;
}
}
.dashboard-download {
@ -51,9 +55,3 @@
text-transform: uppercase;
white-space: nowrap;
}
@media (min-width: 1000px) {
.dashboard {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@ -0,0 +1,61 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { AlphaIcon } from "../components/OnBoarding/AlphaIcon";
import { OnBoardingUtils } from "../utils/onboarding";
import { attributes } from "../utils/attributes";
import { ArrowRightCircle } from "../components/ArrowRightCircle/ArrowRightCircle";
import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout";
import { HealthChecks } from "../components/HealthChecks/HealthChecks";
import { useNetwork } from "../network/useNetwork";
const OnBoardingChecks = () => {
const online = useNetwork();
const displayName = OnBoardingUtils.getDisplayName();
const [isStepValid, setIsStepValid] = useState(false);
const navigate = useNavigate({ from: "/onboarding-checks" });
const onNextStep = () => {
if (isStepValid) {
navigate({ to: "/onboarding-checks" });
}
};
const onStepValid = (valid: boolean) => setIsStepValid(valid);
return (
<OnBoardingLayout defaultIsStepValid={false} step={2}>
<>
<section className="alpha">
<div>
<AlphaIcon variant="primary" />
</div>
<p>
Connection /<br />
Device Health Check
</p>
</section>
<section className="main">
<h1>
Nice to meet {displayName},<br />
Lets establish our connection.
</h1>
<HealthChecks
online={online}
onStepValid={onStepValid}></HealthChecks>
</section>
<a
className="navigation"
onClick={onNextStep}
{...attributes({ "aria-disabled": !isStepValid })}>
<ArrowRightCircle></ArrowRightCircle>
</a>
</>
</OnBoardingLayout>
);
};
export const Route = createFileRoute("/onboarding-checks")({
component: OnBoardingChecks,
});

View File

@ -0,0 +1,55 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { AlphaIcon } from "../components/OnBoarding/AlphaIcon";
import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout";
import { OnBoardingUtils } from "../utils/onboarding";
import { attributes } from "../utils/attributes";
import { ArrowRightCircle } from "../components/ArrowRightCircle/ArrowRightCircle";
import { UserInfo } from "../components/UserInfo/UserInfo";
const OnBoardingName = () => {
const [isStepValid, setIsStepValid] = useState(
!!OnBoardingUtils.getDisplayName()
);
const navigate = useNavigate({ from: "/onboarding-name" });
const onNameChange = (value: string) => setIsStepValid(!!value);
const onNextStep = () => {
if (isStepValid) {
navigate({ to: "/onboarding-checks" });
}
};
return (
<OnBoardingLayout defaultIsStepValid={false} step={1}>
<>
<section className="alpha">
<div>
<AlphaIcon variant="primary" />
</div>
<p>Personalization</p>
</section>
<section className="main">
<h1>
Lets get you setup. <br />
What do you want to be called?
</h1>
<UserInfo onNameChange={onNameChange} />
</section>
<a
className="navigation"
onClick={onNextStep}
{...attributes({ "aria-disabled": !isStepValid })}>
<ArrowRightCircle></ArrowRightCircle>
</a>
</>
</OnBoardingLayout>
);
};
export const Route = createFileRoute("/onboarding-name")({
component: OnBoardingName,
});

View File

@ -1,3 +1,4 @@
export const OnBoardingUtils = {
getStep() {
return parseInt(localStorage.getItem("onboarding-step") || "0", 10)
@ -13,5 +14,13 @@ export const OnBoardingUtils = {
getDisplayName() {
return localStorage.getItem("display-name") || ""
}
},
setEmoji(emoji: string) {
localStorage.setItem("emoji", emoji)
},
getEmoji() {
return localStorage.getItem("emoji") || "🤖"
},
}

View File

@ -19,7 +19,13 @@ export default defineConfig({
defaultHandler(warning);
},
output: {
manualChunks: {
"emoji-picker-react": ["emoji-picker-react"]
}
}
},
},
resolve: {
alias: {