Merge pull request #7 from codex-storage/page-component

Page component
This commit is contained in:
Arnaud 2024-08-23 15:41:44 +02:00 committed by GitHub
commit 252fb1b4f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1681 additions and 48 deletions

18
.eslintrc.cjs Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@ -36,8 +36,4 @@
font-size: var(--codex-font-size);
color-scheme: dark;
}
p {
margin: 0;
}
</style>

976
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,7 @@
},
"peerDependencies": {
"@codex/sdk-js": "@codex/sdk-js#master",
"@tanstack/react-query": "^5.51.24",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
@ -53,6 +54,11 @@
"@storybook/test": "^8.2.9",
"@tanstack/react-query": "^5.51.24",
"@vitejs/plugin-react": "^4.3.1",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"eslint": "^8.57.0",
"glob": "^7.2.3",
"prettier": "^3.3.3",
"react": "^18.3.1",

View File

@ -38,10 +38,8 @@ export function Alert({
style={style}
{...rest}
>
<p>
<b className="alert-message">{variant} ! </b>
</p>
<p>{message}</p>
<div>{message}</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
import { Menu } from "lucide-react";
import "./appBar.css";
import { ReactNode } from "react";
type Props = {
onExpand: () => void;
Right: ReactNode;
};
export function AppBar({ onExpand, Right }: Props) {
return (
<div className="appBar">
<div className="appBar-left">
<a className="appBar-burger" onClick={onExpand}>
<Menu size={"1.25rem"} />
</a>
<span>Home</span>
</div>
<div className="appBar-right">{Right}</div>
</div>
);
}

View File

@ -0,0 +1,32 @@
.appBar {
height: 40px;
justify-content: space-between;
border-bottom: 1px solid var(--codex-border-color);
view-transition-name: main-header;
display: flex;
padding: 0.75rem 1.5rem;
}
.appBar-burger {
cursor: pointer;
color: var(--codex-color);
display: flex;
}
.appBar,
.appBar-left,
.appBar-right {
display: flex;
align-items: center;
}
.appBar-left,
.appBar-right {
gap: 0.75rem;
}
@media (min-width: 1000px) {
.appBar-burger {
display: none;
}
}

View File

@ -13,7 +13,7 @@ export function EmptyPlaceholder({ title, message, onRetry }: Props) {
<div className="emptyPlaceholder">
<EmptyPlaceholderIcon className="emptyPlaceholder-icon" width={178} />
<b className="emptyPlaceholder-title">{title}</b>
<p className="emptyPlaceholder-message">{message} </p>
<div className="emptyPlaceholder-message">{message} </div>
{onRetry && (
<Button

View File

@ -35,7 +35,7 @@ export function Failure({
<div className="failure">
<h1 className="failure-code">{code}</h1>
<h2 className="failure-title">{title}</h2>
<p className="failure-message">{message}</p>
<div className="failure-message">{message}</div>
{onClick && <Button label={button} onClick={onClick} />}
</div>
);

View File

@ -0,0 +1,28 @@
type Props = {
width?: number;
};
export function LogoInverse({ width = 40 }: Props) {
return (
<svg
width={width}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_274_4287)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M19.7001 32.7386C19.7705 32.7792 19.8502 32.8001 19.9306 32.8001C20.0111 32.8001 20.188 32.7225 20.188 32.7225L30.85 26.581C30.8555 26.5783 30.8632 26.5744 30.8706 26.5701C30.942 26.5295 31.0002 26.4707 31.0406 26.4008C31.0819 26.3302 31.1036 26.25 31.1036 26.1685C31.1036 26.1588 31.1032 26.1499 31.1028 26.1432V13.8597C31.1036 13.8486 31.1036 13.8387 31.1036 13.8337L31.1036 13.8324C31.1036 13.7507 31.0818 13.671 31.0413 13.6009C31.0008 13.5302 30.9421 13.4718 30.872 13.4313C30.8644 13.4268 30.8564 13.4225 30.8484 13.4186L20.1868 7.27756C20.179 7.27228 20.1714 7.26767 20.165 7.26389L20.1644 7.26354C20.0937 7.22255 20.0136 7.20151 19.9325 7.20151H19.9306C19.8495 7.20151 19.77 7.22295 19.701 7.26233C19.692 7.26733 19.6837 7.27257 19.6762 7.27763L9.01204 13.4202C9.00655 13.4229 8.99886 13.4269 8.99144 13.4311C8.92049 13.4717 8.86173 13.531 8.82123 13.6011C8.78067 13.6712 8.75879 13.7509 8.75879 13.8327C8.75879 13.8425 8.75919 13.8513 8.75956 13.858V26.1419C8.75876 26.153 8.75878 26.1629 8.75879 26.1679L8.75879 26.1692C8.75879 26.2508 8.78058 26.3312 8.82193 26.4018C8.86238 26.4712 8.92065 26.5296 8.98909 26.5693L8.99047 26.5701L8.99187 26.5708C9.00023 26.5754 9.00781 26.5793 9.01357 26.5822L19.6768 32.7241C19.6818 32.7275 19.6906 32.7332 19.7001 32.7386ZM30.8023 26.4503C30.7969 26.4533 30.7908 26.4564 30.7847 26.4594C30.788 26.4578 30.7915 26.4561 30.7946 26.4544C30.7973 26.4531 30.7999 26.4517 30.8023 26.4503ZM30.9649 26.1472C30.9651 26.1495 30.9652 26.152 30.9653 26.1545C30.9655 26.1589 30.9657 26.1636 30.9657 26.1685C30.9657 26.167 30.9657 26.1655 30.9656 26.164C30.9655 26.158 30.9652 26.1523 30.9649 26.1472ZM20.3983 26.9771L24.3592 29.2549L20.3951 31.538L20.3983 26.9771ZM15.5034 29.2548L19.4645 26.9769L19.4676 31.538L15.5034 29.2548ZM25.7528 23.8924L29.7137 26.1701L25.7497 28.4532L25.7528 23.8924ZM20.3984 25.3628V20.8084L24.3514 23.0856L20.3984 25.3628ZM15.0439 22.2784V17.724L18.9969 20.0012L15.0439 22.2784ZM20.3984 19.1932V14.6388L24.3514 16.916L20.3984 19.1932ZM19.4649 13.0247L15.5038 10.7468L19.468 8.4636L19.4649 13.0247ZM10.1491 26.1701L14.1131 28.4532L14.11 23.8921L10.1491 26.1701ZM24.8194 23.8927L24.8225 28.4527L20.8658 26.1705L24.8194 23.8927ZM15.0438 23.8927L15.0406 28.4527L18.9974 26.1705L15.0438 23.8927ZM26.2198 23.0856L30.178 20.8025V25.3687L26.2198 23.0856ZM9.68483 20.8025V25.3687L13.643 23.0856L9.68483 20.8025ZM15.5111 23.085L19.4644 25.3624V20.8076L15.5111 23.085ZM29.7125 20.0004L25.7529 17.7227V22.2781L29.7125 20.0004ZM10.1503 20.0008L14.1099 22.2785V17.7231L10.1503 20.0008ZM24.8189 17.7232V22.278L20.8656 20.0006L24.8189 17.7232ZM30.178 14.6329V19.1991L26.2198 16.916L30.178 14.6329ZM13.643 16.916L9.68483 19.1991V14.6329L13.643 16.916ZM19.4644 14.6384V19.1928L15.5114 16.9156L19.4644 14.6384ZM29.7141 13.8315L25.7497 11.5482L25.7528 16.1095L29.7141 13.8315ZM14.1135 11.5484L14.1104 16.1095L10.1491 13.8315L14.1135 11.5484ZM24.8225 11.5489L24.8194 16.1089L20.8658 13.8311L24.8225 11.5489ZM18.9974 13.8311L15.0438 16.1089L15.0406 11.5489L18.9974 13.8311ZM24.3596 10.7467L20.3951 8.46359L20.3983 13.0247L24.3596 10.7467Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_274_4287">
<rect width="40" height="40" fill="black" />
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,80 @@
import { attributes } from "../../utils/attributes";
import "./menu.css";
import { LogoInverse } from "../Logo/LogoInverse";
import { ComponentType, useEffect } from "react";
import { Backdrop } from "../Backdrop/Backdrop";
export type MenuItemComponentProps = {
onClick: () => void;
className: string;
};
export type MenuItem =
| {
type: "separator";
}
| {
type: "menu-item";
Component: ComponentType<MenuItemComponentProps>;
}
| {
type: "menu-title";
title: string;
};
type Props = {
expanded: boolean;
onClose: () => void;
onOpen?: () => void;
items: MenuItem[];
className?: string;
};
export function Menu({ expanded, onClose, onOpen, items, className }: Props) {
useEffect(() => {
if (expanded && onOpen) {
onOpen();
}
}, [expanded, onOpen]);
const renderItem = (i: MenuItem, index: number) => {
switch (i.type) {
case "separator":
return <hr className="menu-item-separator" key={index}></hr>;
case "menu-title":
return (
<small className="menu-title" key={i.title}>
{i.title}
</small>
);
case "menu-item":
return (
<i.Component onClick={onClose} className="menu-item" key={index} />
);
}
};
return (
<>
<Backdrop onClose={onClose} open={expanded} />
<aside
className={`menu ${className}`}
{...attributes({ "aria-expanded": expanded })}
>
<div className="menu-container">
<div className="menu-header">
<LogoInverse width={50} />
<span className="menu-separator">|</span>
<span className="menu-name">Codex</span>
</div>
{items.map((item, index) => renderItem(item, index))}
</div>
</aside>
</>
);
}

View File

@ -0,0 +1,72 @@
.menu {
display: flex;
flex-direction: column;
background-color: var(--codex-background-secondary);
border-radius: var(--codex-border-radius);
transform: translatex(-500px);
transition: transform 0.25s;
position: fixed;
min-width: 200px;
z-index: 1;
view-transition-name: main-menu;
height: 100%;
top: 0;
left: 0;
}
.menu-container {
padding: 0.75rem;
}
.menu-backdrop {
display: none;
}
.menu[aria-expanded] {
transform: translatex(0);
min-width: 200px;
}
.menu-header {
margin-bottom: 0.75rem;
}
.menu-item,
.menu-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.menu-item {
padding: 0.75rem;
color: var(--codex-color);
margin-bottom: 0.75rem;
text-decoration: none;
}
.menu-item:hover,
.menu-item.active {
background-color: var(--codex-background-light);
}
.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 {
border: 0.1px solid var(--codex-border-color);
width: 100%;
}
@media (min-width: 1000px) {
.menu {
transform: translatex(0px);
position: inherit;
}
}

View File

@ -0,0 +1,22 @@
import { classnames } from "../../utils/classnames";
import "./networkIndicator.css";
type Props = {
online: boolean;
text: string;
};
export function NetworkIndicator({ online, text }: Props) {
return (
<div className="networkIndicator">
<div
className={classnames(
["networkIndicator-point"],
["networkIndicator-point--online", online],
["networkIndicator-point--offline", !online]
)}
></div>
<span>{text}</span>
</div>
);
}

View File

@ -0,0 +1,35 @@
.networkIndicator {
display: flex;
align-items: center;
gap: 0.75rem;
}
.networkIndicator-point {
width: 12px;
height: 12px;
border-radius: 50%;
animation-duration: 3s;
animation-name: flash;
animation-iteration-count: infinite;
transition: none;
}
.networkIndicator-point--online {
background-color: var(--codex-color-primary);
}
.networkIndicator-point--offline {
background-color: rgb(217, 53, 38);
}
@keyframes flash {
0% {
opacity: 1;
}
40% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@ -0,0 +1,110 @@
import { ReactNode, useState } from "react";
import { Menu, MenuItem, MenuItemComponentProps } from "../Menu/Menu";
import { AppBar } from "../AppBar/AppBar";
import { NetworkIndicator } from "../NetworkIndicator/NetworkIndicator";
import "./page.css";
import {
Home,
Star,
ShoppingBag,
Server,
Settings,
HelpCircle,
} from "lucide-react";
type Props = {
children: ReactNode;
};
export function Page({ children }: Props) {
const [open, setOpen] = useState(false);
const onClose = () => {
setOpen(false);
};
const onExpand = () => {
setOpen(true);
};
const Right = <NetworkIndicator online={true} text="Online" />;
const items = [
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Home size={"1.25rem"} /> Dashboard
</a>
),
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Star size={"1.25rem"} /> Favorties
</a>
),
},
{
type: "separator",
},
{
type: "menu-title",
title: "rent",
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<ShoppingBag size={"1.25rem"} /> Purchases
</a>
),
},
{
type: "separator",
},
{
type: "menu-title",
title: "host",
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Server size={"1.25rem"} /> Availabilities
</a>
),
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Settings size={"1.25rem"} /> Settings
</a>
),
},
{
type: "separator",
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<HelpCircle size={"1.25rem"} /> Help
</a>
),
},
] satisfies MenuItem[];
return (
<div className="page">
<Menu expanded={open} onClose={onClose} items={items}></Menu>
<main className="page-main">
<AppBar onExpand={onExpand} Right={Right} />
{children}
</main>
</div>
);
}

View File

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

View File

@ -118,14 +118,3 @@ export function Stepper({
</div>
);
}
// <div className="stepper-success">
// <video src="/animations/success.webm" autoPlay />
// <p>
// <b>Success ! </b>
// </p>
// <p className="text-center">
// Your request has been submitted. Check your purchases list to get
// more information about the status.
// </p>
// </div>

View File

@ -72,7 +72,7 @@ export function Toast({
if (message) {
timeout.current = window.setTimeout(() => setMsg(""), duration);
}
}, [message, time]);
}, [message, time, duration]);
const onClose = () => {
if (timeout.current) {

View File

@ -89,7 +89,7 @@ const defaultProvider = () =>
return Promise.resolve({
abort: () => {},
result: Promise.resolve({
error: false as false,
error: false,
data: Date.now().toString(),
}),
} satisfies UploadResponse);

View File

@ -1,4 +1,11 @@
import { useRef, useState, useReducer, Reducer, useEffect } from "react";
import {
useRef,
useState,
useReducer,
Reducer,
useEffect,
useCallback,
} from "react";
import { attributes } from "../utils/attributes";
import { PrettyBytes } from "../utils/bytes";
import { Toast } from "../Toast/Toast";
@ -161,7 +168,8 @@ export function UploadFile({
});
const init = useRef(false);
const onInternalSuccess = (cid: string) => {
const onInternalSuccess = useCallback(
(cid: string) => {
worker.current?.terminate();
queryClient.invalidateQueries({
@ -174,7 +182,9 @@ export function UploadFile({
} else {
dispatch({ type: "completed", cid });
}
};
},
[onSuccess, dispatch, queryClient]
);
const onProgress = (loaded: number, total: number) => {
dispatch({
@ -238,7 +248,7 @@ export function UploadFile({
console.info("running file !!");
mutateAsync(file);
}
}, []);
}, [file, mutateAsync, onInternalSuccess, useWorker, provider]);
const onCancel = () => {
if (worker.current) {

View File

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AppBar } from "../src/components/AppBar/AppBar";
import { fn } from "@storybook/test";
import { NetworkIndicator } from "../src/components/NetworkIndicator/NetworkIndicator";
const meta = {
title: "Components/AppBar",
component: AppBar,
parameters: {
layout: "fullwidth",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof AppBar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
onExpand: fn(),
Right: <NetworkIndicator online={true} text="Online" />,
},
};

View File

@ -18,14 +18,14 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Hello",
children: React.createElement("p", {}, "Hello World !"),
children: React.createElement("div", {}, "Hello World !"),
},
};
export const CustomStyle: Story = {
args: {
title: "Hello",
children: React.createElement("p", {}, "Hello World !"),
children: React.createElement("div", {}, "Hello World !"),
style: { "--codex-border-radius": "0px" },
},
};

12
stories/Menu.stories.css Normal file
View File

@ -0,0 +1,12 @@
.menu-story {
min-height: 500px;
}
.menu-noSticky {
transform: translatex(-1000px) !important;
position: fixed;
}
.menu-noSticky[aria-expanded] {
transform: translatex(0) !important;
}

134
stories/Menu.stories.tsx Normal file
View File

@ -0,0 +1,134 @@
import type { Meta } from "@storybook/react";
import {
Menu,
MenuItem,
MenuItemComponentProps,
} from "../src/components/Menu/Menu";
import { useState } from "react";
import { fn } from "@storybook/test";
import {
HelpCircle,
Home,
Server,
Settings,
ShoppingBag,
Star,
} from "lucide-react";
import "./Menu.stories.css";
const meta = {
title: "Overlays/Menu",
component: Menu,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
argTypes: {},
args: {
onClose: fn(),
onOpen: fn(),
},
} satisfies Meta<typeof Menu>;
export default meta;
type Props = {
onClose: () => void;
onOpen: () => void;
};
const Template = (p: Props) => {
const [open, setOpen] = useState(false);
const onClose = () => {
p.onClose();
setOpen(false);
};
const onOpen = () => {
setOpen(true);
};
const items = [
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Home size={"1.25rem"} /> Dashboard
</a>
),
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Star size={"1.25rem"} /> Favorties
</a>
),
},
{
type: "separator",
},
{
type: "menu-title",
title: "rent",
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<ShoppingBag size={"1.25rem"} /> Purchases
</a>
),
},
{
type: "separator",
},
{
type: "menu-title",
title: "host",
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Server size={"1.25rem"} /> Availabilities
</a>
),
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<Settings size={"1.25rem"} /> Settings
</a>
),
},
{
type: "separator",
},
{
type: "menu-item",
Component: (p: MenuItemComponentProps) => (
<a {...p}>
<HelpCircle size={"1.25rem"} /> Help
</a>
),
},
] satisfies MenuItem[];
return (
<div className="menu-story">
<button onClick={onOpen}>Open menu</button>
<Menu
expanded={open}
onClose={onClose}
onOpen={p.onOpen}
items={items}
className="menu-noSticky"
></Menu>
</div>
);
};
export const Default = Template;

View File

@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/react";
import { NetworkIndicator } from "../src/components/NetworkIndicator/NetworkIndicator";
const meta = {
title: "Components/NetworkIndicator",
component: NetworkIndicator,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof NetworkIndicator>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Online: Story = {
args: {
online: true,
text: "Online",
},
};
export const Offline: Story = {
args: {
online: false,
text: "Offline",
},
};

7
stories/Page.stories.css Normal file
View File

@ -0,0 +1,7 @@
body {
margin: 0;
}
.page {
height: 100vh;
}

24
stories/Page.stories.tsx Normal file
View File

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Page } from "../src/components/Page/Page";
import "./Page.stories.css";
const meta = {
title: "Layouts/Page",
component: Page,
parameters: {
layout: "fullwidth",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: (
<span style={{ padding: "1rem", display: "block" }}>Hello World</span>
),
},
};

View File

@ -1,4 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react";
import type { Meta } from "@storybook/react";
import { Stepper } from "../src/components/Stepper/Stepper";
import React, { useState } from "react";
import { fn } from "@storybook/test";
@ -41,7 +41,7 @@ const Template = (p: Props) => {
return (
<Stepper
Body={() => React.createElement("p", {}, title)}
Body={() => React.createElement("div", {}, title)}
titles={titles}
onChangeStep={onChangeStep}
progress={progress}

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import type { Meta } from "@storybook/react";
import { CircleCheck } from "lucide-react";
import { Toast } from "../src/components/Toast/Toast";
import { MouseEvent, useState } from "react";
import { useState } from "react";
const meta = {
title: "Overlays/Toast",
@ -17,12 +17,11 @@ const meta = {
} satisfies Meta<typeof Toast>;
export default meta;
type Story = StoryObj<typeof meta>;
const Template = () => {
const [time, setTime] = useState(0);
const onClick = (_: MouseEvent) => setTime(Date.now());
const onClick = () => setTime(Date.now());
return (
<div style={{ padding: "2rem" }}>

View File

@ -0,0 +1,3 @@
.upload {
min-width: 400px;
}

View File

@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { UploadResponse } from "@codex/sdk-js";
import { Upload } from "../src/components/Upload/Upload";
import { fn } from "@storybook/test";
import "./Upload.stories.css";
const meta = {
title: "Advanced/Upload",
@ -69,7 +70,7 @@ const slowProvider = () =>
window.clearInterval(timeout);
resolve({
error: false as false,
error: false,
data: Date.now().toString(),
});
}

View File

@ -12,7 +12,7 @@ const { glob } = pkg;
export default defineConfig({
worker: {
rollupOptions: {
external: ["@codex/sdk-js"],
external: ["@codex/sdk-js", "@tanstack/react-query"],
output: {
globals: {
"@codex/sdk-js": "codex-sdk-js",