remove dashboard v2 files and update docker compose
This commit is contained in:
parent
98d6884391
commit
6085224a5d
|
@ -82,8 +82,9 @@ services:
|
|||
# ============================================================
|
||||
# dashboard-v2:
|
||||
# build:
|
||||
# context: ./packages/dashboard-v2
|
||||
# context: https://github.com/SkynetLabs/webportal-accounts-dashboard.git#master
|
||||
# dockerfile: Dockerfile
|
||||
# image: skynetlabs/webportal-accounts-dashboard@1.0.0
|
||||
# container_name: dashboard-v2
|
||||
# restart: unless-stopped
|
||||
# logging: *default-logging
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
node_modules/
|
||||
.cache/
|
||||
public/
|
||||
storybook-build/
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
globals: {
|
||||
__PATH_PREFIX__: true,
|
||||
},
|
||||
extends: ["react-app", "plugin:storybook/recommended"],
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
node_modules/
|
||||
.cache/
|
||||
public/
|
||||
storybook-build/
|
|
@ -1,4 +0,0 @@
|
|||
node_modules/
|
||||
.cache/
|
||||
public/
|
||||
storybook-build/
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"printWidth": 120
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
module.exports = {
|
||||
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"storybook-addon-gatsby",
|
||||
{
|
||||
name: "@storybook/addon-postcss",
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require("postcss"),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
core: {
|
||||
builder: "webpack5",
|
||||
},
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import "tailwindcss/tailwind.css";
|
||||
import "@fontsource/sora/300.css"; // light
|
||||
import "@fontsource/sora/400.css"; // normal
|
||||
import "@fontsource/sora/500.css"; // medium
|
||||
import "@fontsource/sora/600.css"; // semibold
|
||||
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||
|
||||
import "../src/styles/global.css";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
layout: "fullscreen",
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
FROM node:16.14.2-alpine
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
COPY package.json \
|
||||
yarn.lock \
|
||||
./
|
||||
|
||||
RUN yarn --frozen-lockfile
|
||||
|
||||
COPY static ./static
|
||||
COPY src ./src
|
||||
COPY gatsby*.js \
|
||||
postcss.config.js \
|
||||
tailwind.config.js \
|
||||
./
|
||||
|
||||
CMD ["sh", "-c", "yarn build && yarn serve --host 0.0.0.0 -p 9000"]
|
|
@ -1,30 +0,0 @@
|
|||
# Skynet Account Dashboard
|
||||
|
||||
Code behind [account.skynetpro.net](https://account.skynetpro.net/)
|
||||
|
||||
## Development
|
||||
|
||||
This is a Gatsby application. To run it locally, all you need is:
|
||||
|
||||
- `yarn install`
|
||||
- `yarn start`
|
||||
|
||||
## Accessing remote APIs
|
||||
|
||||
To have a fully functioning local environment, you'll need to make the browser believe you're actually on the same domain as a working API (i.e. a remote dev or production server) -- otherwise the browser will block the session cookie.
|
||||
To do the trick, configure proper environment variables in the `.env.development` file.
|
||||
This file allows to easily control which domain name you want to use locally and which API you'd like to access.
|
||||
|
||||
Example:
|
||||
|
||||
```env
|
||||
GATSBY_PORTAL_DOMAIN=skynetfree.net # Use skynetfree.net APIs
|
||||
GATSBY_HOST=local.skynetfree.net # Address of your local build
|
||||
```
|
||||
|
||||
> It's recommended to keep the 2LD the same, so any cookies dispatched by the API work without issues.
|
||||
|
||||
With the file configured, run `yarn develop:secure` -- it will run `gatsby develop` with `--https -p=443` options.
|
||||
If you're on macOS, you may need to `sudo` the command to successfully bind to port `443` (https).
|
||||
|
||||
Gatsby will automatically add a proper entry to your `/etc/hosts` file and clean it up when process exits.
|
|
@ -1,32 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { SWRConfig } from "swr";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import "@fontsource/sora/300.css"; // light
|
||||
import "@fontsource/sora/400.css"; // normal
|
||||
import "@fontsource/sora/500.css"; // medium
|
||||
import "@fontsource/sora/600.css"; // semibold
|
||||
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||
import "./src/styles/global.css";
|
||||
import swrConfig from "./src/lib/swrConfig";
|
||||
import { MODAL_ROOT_ID } from "./src/components/Modal";
|
||||
import { PortalSettingsProvider } from "./src/contexts/portal-settings";
|
||||
|
||||
const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY);
|
||||
|
||||
export function wrapPageElement({ element, props }) {
|
||||
const Layout = element.type.Layout ?? React.Fragment;
|
||||
return (
|
||||
<PortalSettingsProvider>
|
||||
<SWRConfig value={swrConfig}>
|
||||
<Elements stripe={stripePromise}>
|
||||
<Layout {...props}>
|
||||
{element}
|
||||
<div id={MODAL_ROOT_ID} />
|
||||
</Layout>
|
||||
</Elements>
|
||||
</SWRConfig>
|
||||
</PortalSettingsProvider>
|
||||
);
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
require("dotenv").config({
|
||||
path: `.env.${process.env.NODE_ENV}`,
|
||||
});
|
||||
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
const { GATSBY_PORTAL_DOMAIN } = process.env;
|
||||
|
||||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: `Account Dashboard`,
|
||||
siteUrl: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
||||
},
|
||||
pathPrefix: "/v2",
|
||||
trailingSlash: "never",
|
||||
plugins: [
|
||||
"gatsby-plugin-image",
|
||||
"gatsby-plugin-provide-react",
|
||||
"gatsby-plugin-react-helmet",
|
||||
"gatsby-plugin-sharp",
|
||||
"gatsby-transformer-sharp",
|
||||
"gatsby-plugin-styled-components",
|
||||
"gatsby-plugin-postcss",
|
||||
{
|
||||
resolve: "gatsby-source-filesystem",
|
||||
options: {
|
||||
name: "images",
|
||||
path: "./static/images/",
|
||||
},
|
||||
__key: "images",
|
||||
},
|
||||
],
|
||||
developMiddleware: (app) => {
|
||||
// Proxy Accounts service API requests:
|
||||
app.use(
|
||||
"/api/",
|
||||
createProxyMiddleware({
|
||||
target: `https://account.${GATSBY_PORTAL_DOMAIN}`,
|
||||
secure: false, // Do not reject self-signed certificates.
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Proxy /skynet requests (e.g. uploads)
|
||||
app.use(
|
||||
["/skynet", "/__internal/"],
|
||||
createProxyMiddleware({
|
||||
target: `https://${GATSBY_PORTAL_DOMAIN}`,
|
||||
secure: false, // Do not reject self-signed certificates.
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
"^/skynet": "",
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { SWRConfig } from "swr";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import "@fontsource/sora/300.css"; // light
|
||||
import "@fontsource/sora/400.css"; // normal
|
||||
import "@fontsource/sora/500.css"; // medium
|
||||
import "@fontsource/sora/600.css"; // semibold
|
||||
import "@fontsource/source-sans-pro/400.css"; // normal
|
||||
import "@fontsource/source-sans-pro/600.css"; // semibold
|
||||
import "./src/styles/global.css";
|
||||
import swrConfig from "./src/lib/swrConfig";
|
||||
import { MODAL_ROOT_ID } from "./src/components/Modal";
|
||||
import { PortalSettingsProvider } from "./src/contexts/portal-settings";
|
||||
|
||||
const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY);
|
||||
|
||||
export function wrapPageElement({ element, props }) {
|
||||
const Layout = element.type.Layout ?? React.Fragment;
|
||||
return (
|
||||
<PortalSettingsProvider>
|
||||
<SWRConfig value={swrConfig}>
|
||||
<Elements stripe={stripePromise}>
|
||||
<Layout {...props}>
|
||||
{element}
|
||||
<div id={MODAL_ROOT_ID} />
|
||||
</Layout>
|
||||
</Elements>
|
||||
</SWRConfig>
|
||||
</PortalSettingsProvider>
|
||||
);
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
{
|
||||
"name": "accounts-dashboard",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Accounts Dashboard",
|
||||
"author": "Skynet Labs",
|
||||
"keywords": [
|
||||
"gatsby"
|
||||
],
|
||||
"scripts": {
|
||||
"develop": "gatsby develop",
|
||||
"develop:secure": "dotenv -e .env.development -- gatsby develop --https -p=443",
|
||||
"start": "gatsby develop",
|
||||
"build": "gatsby build --prefix-paths",
|
||||
"serve": "gatsby serve --prefix-paths",
|
||||
"clean": "gatsby clean",
|
||||
"lint": "eslint .",
|
||||
"prettier": "prettier .",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook -o storybook-build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/sora": "^4.5.3",
|
||||
"@fontsource/source-sans-pro": "^4.5.3",
|
||||
"@stripe/react-stripe-js": "^1.7.1",
|
||||
"@stripe/stripe-js": "^1.27.0",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"dayjs": "^1.10.8",
|
||||
"formik": "^2.2.9",
|
||||
"gatsby": "^4.6.2",
|
||||
"gatsby-plugin-postcss": "^5.7.0",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"ky": "^0.30.0",
|
||||
"nanoid": "^3.3.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.6",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-dropzone": "^12.0.4",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-use": "^17.3.2",
|
||||
"skynet-js": "4.0.27-beta",
|
||||
"swr": "^1.2.2",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.4",
|
||||
"@storybook/addon-actions": "^6.4.19",
|
||||
"@storybook/addon-essentials": "^6.4.19",
|
||||
"@storybook/addon-interactions": "^6.4.19",
|
||||
"@storybook/addon-links": "^6.4.19",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.4.19",
|
||||
"@storybook/manager-webpack5": "^6.4.19",
|
||||
"@storybook/react": "^6.4.19",
|
||||
"@storybook/testing-library": "^0.0.9",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-preval": "^5.1.0",
|
||||
"babel-plugin-styled-components": "^2.0.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv-cli": "^5.1.0",
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"eslint-plugin-storybook": "^0.5.6",
|
||||
"gatsby-plugin-alias-imports": "^1.0.5",
|
||||
"gatsby-plugin-image": "^2.6.0",
|
||||
"gatsby-plugin-preval": "^1.0.0",
|
||||
"gatsby-plugin-provide-react": "^1.0.2",
|
||||
"gatsby-plugin-react-helmet": "^5.6.0",
|
||||
"gatsby-plugin-sharp": "^4.6.0",
|
||||
"gatsby-plugin-styled-components": "^5.8.0",
|
||||
"gatsby-source-filesystem": "^4.6.0",
|
||||
"gatsby-transformer-sharp": "^4.6.0",
|
||||
"http-proxy-middleware": "^1.3.1",
|
||||
"prettier": "2.5.1",
|
||||
"react-is": "^17.0.2",
|
||||
"storybook-addon-gatsby": "^0.0.5",
|
||||
"styled-components": "^5.3.3"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: [require("tailwindcss/nesting"), require("tailwindcss"), require("autoprefixer")],
|
||||
};
|
|
@ -1,157 +0,0 @@
|
|||
import dayjs from "dayjs";
|
||||
import cn from "classnames";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Alert } from "../Alert";
|
||||
import { Button } from "../Button";
|
||||
import { AddSkylinkToSponsorKeyForm } from "../forms/AddSkylinkToSponsorKeyForm";
|
||||
import { CogIcon, TrashIcon } from "../Icons";
|
||||
import { Modal } from "../Modal";
|
||||
|
||||
import { useAPIKeyEdit } from "./useAPIKeyEdit";
|
||||
import { useAPIKeyRemoval } from "./useAPIKeyRemoval";
|
||||
|
||||
export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
|
||||
const { id, name, createdAt, skylinks } = apiKey;
|
||||
const isSponsorKey = apiKey.public === "true";
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const onSkylinkListEdited = useCallback(() => {
|
||||
setError(null);
|
||||
onEdited();
|
||||
}, [onEdited]);
|
||||
|
||||
const onSkylinkListEditFailure = (errorMessage) => setError(errorMessage);
|
||||
|
||||
const {
|
||||
removalError,
|
||||
removalInitiated,
|
||||
prompt: promptRemoval,
|
||||
abort: abortRemoval,
|
||||
confirm: confirmRemoval,
|
||||
} = useAPIKeyRemoval({
|
||||
key: apiKey,
|
||||
onSuccess: onRemoved,
|
||||
onFailure: onRemovalError,
|
||||
});
|
||||
|
||||
const {
|
||||
editInitiated,
|
||||
prompt: promptEdit,
|
||||
abort: abortEdit,
|
||||
addSkylink,
|
||||
removeSkylink,
|
||||
} = useAPIKeyEdit({
|
||||
key: apiKey,
|
||||
onSkylinkListUpdate: onSkylinkListEdited,
|
||||
onSkylinkListUpdateFailure: onSkylinkListEditFailure,
|
||||
});
|
||||
|
||||
const closeEditModal = useCallback(() => {
|
||||
setError(null);
|
||||
abortEdit();
|
||||
}, [abortEdit]);
|
||||
|
||||
const skylinksNumber = skylinks?.length ?? 0;
|
||||
const isNotConfigured = isSponsorKey && skylinksNumber === 0;
|
||||
const skylinksPhrasePrefix = skylinksNumber === 0 ? "No" : skylinksNumber;
|
||||
const skylinksPhrase = `${skylinksPhrasePrefix} ${skylinksNumber === 1 ? "skylink" : "skylinks"} sponsored`;
|
||||
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"grid grid-cols-2 sm:grid-cols-[1fr_repeat(2,_max-content)] py-3 px-4 gap-x-8 items-center bg-white odd:bg-palette-100/50"
|
||||
)}
|
||||
>
|
||||
<span className="col-span-2 sm:col-span-1 flex items-center">
|
||||
<span className="flex flex-col">
|
||||
<span className={cn("truncate", { "text-palette-300": !name })}>{name || "unnamed key"}</span>
|
||||
{isSponsorKey && (
|
||||
<button
|
||||
onClick={promptEdit}
|
||||
className={cn("text-xs hover:underline decoration-dotted", {
|
||||
"text-error": isNotConfigured,
|
||||
"text-palette-400": !isNotConfigured,
|
||||
})}
|
||||
>
|
||||
{skylinksPhrase}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="col-span-2 my-4 border-t border-t-palette-200/50 sm:hidden" />
|
||||
<span className="text-palette-400">{dayjs(createdAt).format("MMM DD, YYYY")}</span>
|
||||
<span className="flex items-center justify-end">
|
||||
{isSponsorKey && (
|
||||
<button
|
||||
title="Add or remove skylinks"
|
||||
aria-label="Add or remove skylinks"
|
||||
className="p-1 transition-colors hover:text-primary"
|
||||
onClick={promptEdit}
|
||||
>
|
||||
<CogIcon size={22} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
title="Delete this API key"
|
||||
aria-label="Delete this API key"
|
||||
className="p-1 transition-colors hover:text-error"
|
||||
onClick={promptRemoval}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
{removalInitiated && (
|
||||
<Modal onClose={abortRemoval} className="flex flex-col gap-4 text-center">
|
||||
<h4>Delete API key</h4>
|
||||
<div>
|
||||
<p>Are you sure you want to delete the following API key?</p>
|
||||
<p className="font-semibold">{name || id}</p>
|
||||
</div>
|
||||
{removalError && <Alert $variant="error">{removalError}</Alert>}
|
||||
|
||||
<div className="flex gap-4 justify-center mt-4">
|
||||
<Button $primary onClick={abortRemoval}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={confirmRemoval}>Delete</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
{editInitiated && (
|
||||
<Modal onClose={closeEditModal} className="flex flex-col gap-4 text-center sm:px-8 sm:py-6">
|
||||
<h4>Sponsored skylinks</h4>
|
||||
{skylinks?.length > 0 ? (
|
||||
<ul className="text-xs flex flex-col gap-2">
|
||||
{skylinks.map((skylink) => (
|
||||
<li key={skylink} className="grid grid-cols-[1fr_min-content] w-full gap-4 items-center">
|
||||
<code className="whitespace-nowrap select-all truncate bg-palette-100 odd:bg-white p-1">
|
||||
{skylink}
|
||||
</code>
|
||||
<button
|
||||
className="p-1 transition-colors hover:text-error"
|
||||
onClick={() => removeSkylink(skylink)}
|
||||
aria-label="Remove skylink"
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Alert $variant="info">No skylinks here yet. You can add the first one below 🙃</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{error && <Alert $variant="error">{error}</Alert>}
|
||||
<AddSkylinkToSponsorKeyForm addSkylink={addSkylink} />
|
||||
</div>
|
||||
<div className="flex gap-4 justify-center mt-4">
|
||||
<Button onClick={closeEditModal}>Close</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import { APIKey } from "./APIKey";
|
||||
|
||||
export const APIKeyList = ({ keys, reloadKeys, title }) => {
|
||||
return (
|
||||
<>
|
||||
<h6 className="text-palette-300 mb-4">{title}</h6>
|
||||
<ul className="mt-4">
|
||||
{keys.map((key) => (
|
||||
<APIKey key={key.id} apiKey={key} onEdited={reloadKeys} onRemoved={reloadKeys} />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./APIKeyList";
|
|
@ -1,43 +0,0 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import accountsService from "../../services/accountsService";
|
||||
|
||||
export const useAPIKeyEdit = ({ key, onSkylinkListUpdate, onSkylinkListUpdateFailure }) => {
|
||||
const [editInitiated, setEditInitiated] = useState(false);
|
||||
|
||||
const prompt = () => setEditInitiated(true);
|
||||
const abort = () => setEditInitiated(false);
|
||||
const updateSkylinkList = useCallback(
|
||||
async (action, skylink) => {
|
||||
try {
|
||||
await accountsService.patch(`user/apikeys/${key.id}`, {
|
||||
json: {
|
||||
[action]: [skylink],
|
||||
},
|
||||
});
|
||||
onSkylinkListUpdate();
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
const { message } = await err.response.json();
|
||||
onSkylinkListUpdateFailure(message);
|
||||
} else {
|
||||
onSkylinkListUpdateFailure("Unknown error occured, please try again.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[onSkylinkListUpdate, onSkylinkListUpdateFailure, key]
|
||||
);
|
||||
const addSkylink = (skylink) => updateSkylinkList("add", skylink);
|
||||
const removeSkylink = (skylink) => updateSkylinkList("remove", skylink);
|
||||
|
||||
return {
|
||||
editInitiated,
|
||||
prompt,
|
||||
abort,
|
||||
addSkylink,
|
||||
removeSkylink,
|
||||
};
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import accountsService from "../../services/accountsService";
|
||||
|
||||
export const useAPIKeyRemoval = ({ key, onSuccess }) => {
|
||||
const [removalInitiated, setRemovalInitiated] = useState(false);
|
||||
const [removalError, setRemovalError] = useState(null);
|
||||
|
||||
const prompt = () => {
|
||||
setRemovalError(null);
|
||||
setRemovalInitiated(true);
|
||||
};
|
||||
const abort = () => setRemovalInitiated(false);
|
||||
|
||||
const confirm = useCallback(async () => {
|
||||
setRemovalError(null);
|
||||
try {
|
||||
await accountsService.delete(`user/apikeys/${key.id}`);
|
||||
setRemovalInitiated(false);
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
let message = "There was an error processing your request. Please try again later.";
|
||||
|
||||
if (err.response) {
|
||||
const response = await err.response.json();
|
||||
if (response.message) {
|
||||
message = response.message;
|
||||
}
|
||||
}
|
||||
|
||||
setRemovalError(message);
|
||||
}
|
||||
}, [onSuccess, key]);
|
||||
|
||||
return {
|
||||
removalInitiated,
|
||||
removalError,
|
||||
prompt,
|
||||
abort,
|
||||
confirm,
|
||||
};
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import styled from "styled-components";
|
||||
import cn from "classnames";
|
||||
|
||||
export const Alert = styled.div.attrs(({ $variant }) => ({
|
||||
className: cn("px-3 py-2 sm:px-6 sm:py-4 rounded border", {
|
||||
"bg-blue-100 border-blue-200 text-palette-400": $variant === "info",
|
||||
"bg-red-100 border-red-200 text-error": $variant === "error",
|
||||
"bg-green-100 border-green-200 text-palette-400": $variant === "success",
|
||||
}),
|
||||
}))``;
|
|
@ -1 +0,0 @@
|
|||
export * from "./Alert";
|
|
@ -1,36 +0,0 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
// import { SimpleUploadIcon } from "../Icons";
|
||||
|
||||
import avatarPlaceholder from "../../../static/images/avatar-placeholder.svg";
|
||||
|
||||
export const AvatarUploader = (props) => {
|
||||
const { user } = useUser();
|
||||
const [imageUrl, setImageUrl] = useState(avatarPlaceholder);
|
||||
|
||||
useEffect(() => {
|
||||
setImageUrl(user.avatarUrl ?? avatarPlaceholder);
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<div
|
||||
className={`flex justify-center items-center xl:w-[245px] xl:h-[245px] bg-contain bg-none xl:bg-[url(/images/avatar-bg.svg)]`}
|
||||
>
|
||||
<img src={imageUrl} className="w-[160px]" alt="" />
|
||||
</div>
|
||||
{/* TODO: uncomment when avatar uploads work
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
|
||||
type="button"
|
||||
onClick={console.info.bind(console)}
|
||||
>
|
||||
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
|
||||
</button>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./AvatarUploader";
|
|
@ -1,47 +0,0 @@
|
|||
import cn from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const Button = styled.button.attrs(({ as: polymorphicAs, disabled, $primary, type }) => {
|
||||
// We want to default to type=button in most cases, but sometimes we use this component
|
||||
// as a polymorphic one (i.e. for links), and then we should avoid setting `type` property,
|
||||
// as it breaks styling in Safari.
|
||||
const typeAttr = polymorphicAs && polymorphicAs !== "button" ? undefined : type;
|
||||
|
||||
return {
|
||||
type: typeAttr,
|
||||
className: cn(
|
||||
"px-6 py-2.5 inline-block rounded-full font-sans uppercase text-xs tracking-wide transition-[opacity_filter]",
|
||||
{
|
||||
"bg-primary text-palette-600": $primary,
|
||||
"bg-white border-2 border-black text-palette-600": !$primary,
|
||||
"cursor-not-allowed opacity-60": disabled,
|
||||
"hover:brightness-90": !disabled,
|
||||
}
|
||||
),
|
||||
};
|
||||
})``;
|
||||
|
||||
Button.propTypes = {
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
$primary: PropTypes.bool,
|
||||
/**
|
||||
* Prevent interaction on the button
|
||||
*/
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* Type of button (button / submit)
|
||||
*/
|
||||
type: PropTypes.oneOf(["button", "submit"]),
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
$primary: false,
|
||||
disabled: false,
|
||||
type: "button",
|
||||
};
|
|
@ -1,38 +0,0 @@
|
|||
import { Button } from "./Button";
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: "SkynetLibrary/Button",
|
||||
component: Button,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
backgroundColor: { control: "color" },
|
||||
},
|
||||
};
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template = (args) => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
primary: true,
|
||||
label: "Button",
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
label: "Button",
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
size: "large",
|
||||
label: "Button",
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: "small",
|
||||
label: "Button",
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./Button";
|
|
@ -1,50 +0,0 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import copy from "copy-text-to-clipboard";
|
||||
import styled from "styled-components";
|
||||
import { useClickAway } from "react-use";
|
||||
|
||||
import { CopyIcon } from "./Icons";
|
||||
|
||||
const Button = styled.button.attrs({
|
||||
className: "relative inline-flex items-center hover:text-primary",
|
||||
})``;
|
||||
|
||||
const TooltipContainer = styled.div.attrs(({ $visible }) => ({
|
||||
className: `absolute left-full top-1/2 z-10
|
||||
bg-white rounded border border-primary/30 shadow-md
|
||||
pointer-events-none transition-opacity duration-150 ease-in-out
|
||||
${$visible ? "opacity-100" : "opacity-0"}`,
|
||||
}))`
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
||||
const TooltipContent = styled.div.attrs({
|
||||
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
|
||||
})``;
|
||||
|
||||
export const CopyButton = ({ value, className, ariaLabel = "Copy" }) => {
|
||||
const containerRef = useRef();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [timer, setTimer] = useState(null);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
clearTimeout(timer);
|
||||
copy(value);
|
||||
setCopied(true);
|
||||
|
||||
setTimer(setTimeout(() => setCopied(false), 1500));
|
||||
}, [value, timer]);
|
||||
|
||||
useClickAway(containerRef, () => setCopied(false));
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`inline-flex relative overflow-visible pr-2 ${className ?? ""}`}>
|
||||
<Button onClick={handleCopy} className={copied ? "text-primary" : ""} aria-label={ariaLabel}>
|
||||
<CopyIcon size={16} />
|
||||
</Button>
|
||||
<TooltipContainer $visible={copied}>
|
||||
<TooltipContent>Copied to clipboard</TooltipContent>
|
||||
</TooltipContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
import useActivePlan from "../../hooks/useActivePlan";
|
||||
import humanBytes from "../../lib/humanBytes";
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
|
||||
import LatestPayment from "./LatestPayment";
|
||||
import SuggestedPlan from "./SuggestedPlan";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const CurrentPlan = () => {
|
||||
const { user, error: userError } = useUser();
|
||||
const { plans, activePlan, error: plansError } = useActivePlan(user);
|
||||
|
||||
if (!user || !activePlan) {
|
||||
return <ContainerLoadingIndicator />;
|
||||
}
|
||||
|
||||
if (userError || plansError) {
|
||||
return (
|
||||
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
|
||||
<p>An error occurred while loading this data.</p>
|
||||
<p>We'll retry automatically.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<h4>{activePlan.name}</h4>
|
||||
<div className="text-palette-400 justify-between flex flex-col grow">
|
||||
{activePlan.price === 0 && activePlan.limits && (
|
||||
<p>{humanBytes(activePlan.limits.storageLimit)} without paying a dime! 🎉</p>
|
||||
)}
|
||||
{activePlan.price !== 0 &&
|
||||
(user.subscriptionCancelAtPeriodEnd ? (
|
||||
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
|
||||
) : (
|
||||
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</p>
|
||||
))}
|
||||
|
||||
{user.subscriptionStatus && <LatestPayment user={user} />}
|
||||
<SuggestedPlan plans={plans} activePlan={activePlan} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentPlan;
|
|
@ -1,18 +0,0 @@
|
|||
import dayjs from "dayjs";
|
||||
|
||||
// TODO: this is not an accurate information, we need this data from the backend
|
||||
const LatestPayment = ({ user }) => (
|
||||
<div className="flex mt-6 justify-between items-center bg-palette-100/50 py-4 px-6 border-l-2 border-primary">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<span>Latest payment</span>
|
||||
<span className="lg:before:content-['-'] lg:before:px-2 text-xs lg:text-base">
|
||||
{dayjs(user.subscribedUntil).subtract(1, "month").format("MM/DD/YYYY")}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="rounded py-1 px-2 bg-primary/10 font-sans text-primary uppercase text-xs">Success</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default LatestPayment;
|
|
@ -1,24 +0,0 @@
|
|||
import { Link } from "gatsby";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Button } from "../Button";
|
||||
|
||||
const SuggestedPlan = ({ plans, activePlan }) => {
|
||||
const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan.tier), [plans, activePlan]);
|
||||
|
||||
if (!nextPlan) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-7">
|
||||
<p className="font-sans font-semibold text-xs uppercase text-primary">Discover {nextPlan.name}</p>
|
||||
<p className="pt-1 text-xs sm:text-base">{nextPlan.description}</p>
|
||||
<Button $primary as={Link} to="/payments" className="mt-6">
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestedPlan;
|
|
@ -1,3 +0,0 @@
|
|||
import CurrentPlan from "./CurrentPlan";
|
||||
|
||||
export default CurrentPlan;
|
|
@ -1,116 +0,0 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "gatsby";
|
||||
import cn from "classnames";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
import useActivePlan from "../../hooks/useActivePlan";
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
|
||||
import { GraphBar } from "./GraphBar";
|
||||
import { UsageGraph } from "./UsageGraph";
|
||||
import humanBytes from "../../lib/humanBytes";
|
||||
|
||||
const useUsageData = () => {
|
||||
const { user } = useUser();
|
||||
const { activePlan, error } = useActivePlan(user);
|
||||
const { data: stats, error: statsError } = useSWR("user/stats");
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [usage, setUsage] = useState({});
|
||||
|
||||
const hasError = error || statsError;
|
||||
const hasData = activePlan && stats;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasData || hasError) {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (hasData && !hasError) {
|
||||
setUsage({
|
||||
filesUsed: stats?.numUploads,
|
||||
filesLimit: activePlan?.limits?.maxNumberUploads,
|
||||
storageUsed: stats?.totalUploadsSize,
|
||||
storageLimit: activePlan?.limits?.storageLimit,
|
||||
});
|
||||
}
|
||||
}, [hasData, hasError, stats, activePlan]);
|
||||
|
||||
return {
|
||||
error: error || statsError,
|
||||
loading,
|
||||
usage,
|
||||
};
|
||||
};
|
||||
|
||||
const size = (bytes) => {
|
||||
const text = humanBytes(bytes ?? 0, { precision: 0 });
|
||||
const [value, unit] = text.split(" ");
|
||||
|
||||
return {
|
||||
text,
|
||||
value,
|
||||
unit,
|
||||
};
|
||||
};
|
||||
|
||||
const ErrorMessage = () => (
|
||||
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
|
||||
<p>We were not able to fetch the current usage data.</p>
|
||||
<p>We'll try again automatically.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function CurrentUsage() {
|
||||
const { activePlan, plans } = useActivePlan();
|
||||
const { usage, error, loading } = useUsageData();
|
||||
const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan?.tier), [plans, activePlan]);
|
||||
const storageUsage = size(usage.storageUsed);
|
||||
const storageLimit = size(usage.storageLimit);
|
||||
const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
|
||||
|
||||
if (loading) {
|
||||
return <ContainerLoadingIndicator />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>
|
||||
{storageUsage.text} of {storageLimit.text}
|
||||
</h4>
|
||||
<p className="text-palette-400">
|
||||
{usage.filesUsed} of {usage.filesLimit} files
|
||||
</p>
|
||||
<div className="relative mt-7 font-sans uppercase text-xs">
|
||||
<div className="flex place-content-between">
|
||||
<span>Storage</span>
|
||||
<span>{storageLimit.text}</span>
|
||||
</div>
|
||||
<UsageGraph>
|
||||
<GraphBar value={usage.storageUsed} limit={usage.storageLimit} label={storageUsage} className="normal-case" />
|
||||
<GraphBar value={usage.filesUsed} limit={usage.filesLimit} label={filesUsedLabel} />
|
||||
</UsageGraph>
|
||||
<div className="flex place-content-between">
|
||||
<span>Files</span>
|
||||
<span className="inline-flex place-content-between w-[37%]">
|
||||
<Link
|
||||
to="/payments"
|
||||
className={cn(
|
||||
"text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline",
|
||||
{ invisible: !nextPlan }
|
||||
)}
|
||||
>
|
||||
UPGRADE
|
||||
</Link>{" "}
|
||||
<span>{usage.filesLimit}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
const Bar = styled.div.attrs({
|
||||
className: `relative flex justify-end h-4 bg-primary rounded-l rounded-r-lg`,
|
||||
})`
|
||||
min-width: 1rem;
|
||||
width: ${({ $percentage }) => $percentage}%;
|
||||
`;
|
||||
|
||||
const BarTip = styled.span.attrs({
|
||||
className: "relative w-4 h-4 border-2 rounded-full bg-white border-primary",
|
||||
})``;
|
||||
|
||||
const BarLabel = styled.span.attrs({
|
||||
className: "usage-label bg-white rounded border-2 border-palette-200 px-3 whitespace-nowrap absolute shadow",
|
||||
})`
|
||||
${({ $percentage }) => `
|
||||
left: max(0%, ${$percentage}%);
|
||||
top: -0.5rem;
|
||||
transform: translateX(-${$percentage}%);
|
||||
`}
|
||||
`;
|
||||
|
||||
export const GraphBar = ({ value, limit, label, className }) => {
|
||||
const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100;
|
||||
|
||||
return (
|
||||
<div className={`relative flex items-center ${className}`}>
|
||||
<Bar $percentage={percentage}>
|
||||
<BarTip />
|
||||
</Bar>
|
||||
<BarLabel $percentage={percentage}>
|
||||
<span className="font-sora text-lg">{label.value}</span> <span>{label.unit}</span>
|
||||
</BarLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
import usageGraphBg from "../../../static/images/usage-graph-bg.svg";
|
||||
|
||||
export const UsageGraph = styled.div.attrs({
|
||||
className: "w-full my-3 grid grid-flow-row grid-rows-2",
|
||||
})`
|
||||
height: 146px;
|
||||
background: url(${usageGraphBg}) no-repeat;
|
||||
background-size: cover;
|
||||
`;
|
|
@ -1,3 +0,0 @@
|
|||
import CurrentUsage from "./CurrentUsage";
|
||||
|
||||
export default CurrentUsage;
|
|
@ -1,63 +0,0 @@
|
|||
import { useRef, useState } from "react";
|
||||
import { useClickAway } from "react-use";
|
||||
import PropTypes from "prop-types";
|
||||
import styled, { css, keyframes } from "styled-components";
|
||||
|
||||
import { ChevronDownIcon } from "../Icons";
|
||||
|
||||
const dropDown = keyframes`
|
||||
0% {
|
||||
transform: rotateX(-90deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(0deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div.attrs({ className: `relative inline-flex` })``;
|
||||
|
||||
const Trigger = styled.button.attrs({
|
||||
className: "flex items-center",
|
||||
})``;
|
||||
|
||||
const TriggerIcon = styled(ChevronDownIcon).attrs({
|
||||
className: "transition-transform text-primary",
|
||||
})`
|
||||
transform: ${({ open }) => (open ? "rotateX(180deg)" : "none")};
|
||||
`;
|
||||
|
||||
const Flyout = styled.div.attrs(({ open }) => ({
|
||||
className: `absolute top-full right-0 p-0 z-10
|
||||
border rounded border-palette-100
|
||||
bg-white shadow-md shadow-palette-200/50
|
||||
${open ? "visible" : "invisible"}`,
|
||||
}))`
|
||||
transform-origin: top center;
|
||||
animation: ${({ open }) =>
|
||||
open
|
||||
? css`
|
||||
${dropDown} .15s ease-in-out forwards;
|
||||
`
|
||||
: "none"};
|
||||
`;
|
||||
|
||||
export const DropdownMenu = ({ title, children }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const menuRef = useRef();
|
||||
|
||||
useClickAway(menuRef, () => setOpen(false));
|
||||
|
||||
return (
|
||||
<Container ref={menuRef}>
|
||||
<Trigger onClick={() => setOpen((open) => !open)}>
|
||||
{title} <TriggerIcon open={open} />
|
||||
</Trigger>
|
||||
<Flyout open={open}>{children}</Flyout>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
DropdownMenu.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
import { Panel } from "../Panel";
|
||||
import { DropdownMenu, DropdownMenuLink } from ".";
|
||||
import { CogIcon, LockClosedIcon } from "../Icons";
|
||||
|
||||
export default {
|
||||
title: "SkynetLibrary/DropdownMenu",
|
||||
component: DropdownMenu,
|
||||
subcomponents: {
|
||||
DropdownMenuLink,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Panel style={{ margin: 50, textAlign: "center" }}>
|
||||
<Story />
|
||||
</Panel>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const NavigationDropdown = () => (
|
||||
<DropdownMenu title="My account">
|
||||
<DropdownMenuLink href="/settings" icon={CogIcon} label="Settings" active />
|
||||
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
|
||||
</DropdownMenu>
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
import styled from "styled-components";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const DropdownLink = styled.a.attrs({
|
||||
className: `m-0 border-t border-palette-200/50 h-[60px]
|
||||
whitespace-nowrap transition-colors
|
||||
hover:bg-palette-100/50 flex items-center
|
||||
pr-8 pl-6 py-4 gap-4 first:border-0`,
|
||||
})``;
|
||||
|
||||
export const DropdownMenuLink = ({ active, icon: Icon, label, ...props }) => (
|
||||
<DropdownLink {...props}>
|
||||
{Icon ? <Icon /> : null}
|
||||
<span className="text-palette-500">{label}</span>
|
||||
</DropdownLink>
|
||||
);
|
||||
|
||||
DropdownMenuLink.propTypes = {
|
||||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired,
|
||||
active: PropTypes.bool,
|
||||
icon: PropTypes.func,
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./DropdownMenu";
|
||||
export * from "./DropdownMenuLink";
|
|
@ -1,57 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { useMedia } from "react-use";
|
||||
|
||||
import theme from "../../lib/theme";
|
||||
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
|
||||
import FileTable from "./FileTable";
|
||||
import useFormattedFilesData from "./useFormattedFilesData";
|
||||
import { MobileFileList } from "./MobileFileList";
|
||||
import { Pagination } from "./Pagination";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const FileList = ({ type }) => {
|
||||
const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const baseUrl = `user/${type}?pageSize=${PAGE_SIZE}`;
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
mutate: refreshList,
|
||||
} = useSWR(`${baseUrl}&offset=${offset}`, {
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
const items = useFormattedFilesData(data?.items || []);
|
||||
const count = data?.count || 0;
|
||||
|
||||
// Next page preloading
|
||||
const hasMoreRecords = data ? data.offset + data.pageSize < data.count : false;
|
||||
const nextPageOffset = hasMoreRecords ? data.offset + data.pageSize : offset;
|
||||
useSWR(`${baseUrl}&offset=${nextPageOffset}`);
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||
{!data && !error && <ContainerLoadingIndicator />}
|
||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||
{data && <p>No {type} found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMediumScreenOrLarger ? (
|
||||
<FileTable onUpdated={refreshList} items={items} />
|
||||
) : (
|
||||
<MobileFileList items={items} onUpdated={refreshList} />
|
||||
)}
|
||||
<Pagination count={count} offset={offset} setOffset={setOffset} pageSize={PAGE_SIZE} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileList;
|
|
@ -1,79 +0,0 @@
|
|||
import { CogIcon, ShareIcon } from "../Icons";
|
||||
import { PopoverMenu } from "../PopoverMenu/PopoverMenu";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table";
|
||||
import { CopyButton } from "../CopyButton";
|
||||
import { useSkylinkOptions } from "./useSkylinkOptions";
|
||||
import { useSkylinkSharing } from "./useSkylinkSharing";
|
||||
|
||||
const SkylinkOptionsMenu = ({ skylink, onUpdated }) => {
|
||||
const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated });
|
||||
|
||||
return (
|
||||
<PopoverMenu inProgress={inProgress} options={options} openClassName="text-primary">
|
||||
<button aria-label="Manage this skylink">
|
||||
<CogIcon />
|
||||
</button>
|
||||
</PopoverMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const SkylinkSharingMenu = ({ skylink }) => {
|
||||
const { options } = useSkylinkSharing(skylink);
|
||||
|
||||
return (
|
||||
<PopoverMenu options={options} openClassName="text-primary">
|
||||
<button aria-label="Share this skylink">
|
||||
<ShareIcon size={22} />
|
||||
</button>
|
||||
</PopoverMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default function FileTable({ items, onUpdated }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
<TableRow noHoverEffect>
|
||||
<TableHeadCell className="w-[180px] xl:w-[360px]">Name</TableHeadCell>
|
||||
<TableHeadCell className="w-[80px]">Type</TableHeadCell>
|
||||
<TableHeadCell className="w-[100px]" align="right">
|
||||
Size
|
||||
</TableHeadCell>
|
||||
<TableHeadCell className="w-[160px]">Uploaded</TableHeadCell>
|
||||
<TableHeadCell className="hidden lg:table-cell">Skylink</TableHeadCell>
|
||||
<TableHeadCell className="w-[90px]">Activity</TableHeadCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
const { id, name, type, size, date, skylink } = item;
|
||||
|
||||
return (
|
||||
<TableRow key={id}>
|
||||
<TableCell className="w-[180px] xl:w-[360px]">{name}</TableCell>
|
||||
<TableCell className="w-[80px]">{type}</TableCell>
|
||||
<TableCell className="w-[100px]" align="right">
|
||||
{size}
|
||||
</TableCell>
|
||||
<TableCell className="w-[160px]">{date}</TableCell>
|
||||
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
|
||||
<div className="flex items-center">
|
||||
<CopyButton value={skylink} className="mr-2" aria-label="Copy skylink" />
|
||||
<span className="w-full inline-block truncate">{skylink}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-[90px] !overflow-visible">
|
||||
<div className="flex text-palette-600 gap-4">
|
||||
<SkylinkOptionsMenu skylink={skylink} onUpdated={onUpdated} />
|
||||
<SkylinkSharingMenu skylink={skylink} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import cn from "classnames";
|
||||
|
||||
import { ChevronDownIcon } from "../Icons";
|
||||
import { useSkylinkSharing } from "./useSkylinkSharing";
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
import { useSkylinkOptions } from "./useSkylinkOptions";
|
||||
|
||||
const SharingMenu = ({ skylink }) => {
|
||||
const { options } = useSkylinkSharing(skylink);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 bg-white px-4 py-6 w-1/2">
|
||||
{options.map(({ label, callback }, index) => (
|
||||
<button key={index} className="uppercase text-left" onClick={callback}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const OptionsMenu = ({ skylink, onUpdated }) => {
|
||||
const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated });
|
||||
|
||||
return (
|
||||
<div className={cn("relative px-4 py-6 w-1/2", { "bg-primary/10": !inProgress })}>
|
||||
<div className={cn("flex flex-col gap-4", { "opacity-0": inProgress })}>
|
||||
{options.map(({ label, callback }, index) => (
|
||||
<button key={index} className="uppercase text-left" onClick={callback}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{inProgress && (
|
||||
<ContainerLoadingIndicator className="absolute inset-0 !p-0 z-50 bg-primary/10 !text-palette-200" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItem = ({ item, onUpdated }) => {
|
||||
const { name, type, size, date, skylink } = item;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const toggle = () => setOpen((open) => !open);
|
||||
|
||||
return (
|
||||
<div className={cn("p-4 flex flex-col bg-palette-100", { "bg-opacity-50": !open })}>
|
||||
<div className="flex items-center gap-4 justify-between">
|
||||
<div className="info flex flex-col gap-2 truncate">
|
||||
<div className="truncate">{name}</div>
|
||||
<div className="flex divide-x divide-palette-200 text-xs">
|
||||
<span className="px-1">{type}</span>
|
||||
<span className="px-1">{size}</span>
|
||||
<span className="px-1">{date}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={toggle}>
|
||||
<ChevronDownIcon className={cn("transition-[transform]", { "-scale-100": open })} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex transition-[max-height_padding] overflow-hidden text-xs text-left font-sans tracking-normal",
|
||||
{ "pt-4 max-h-[150px]": open, "pt-0 max-h-0": !open }
|
||||
)}
|
||||
>
|
||||
<SharingMenu skylink={skylink} />
|
||||
<OptionsMenu skylink={skylink} onUpdated={onUpdated} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MobileFileList = ({ items, onUpdated }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{items.map((item) => (
|
||||
<ListItem key={item.id} item={item} onUpdated={onUpdated} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
import { Button } from "../Button";
|
||||
|
||||
export const Pagination = ({ count, offset, setOffset, pageSize }) => {
|
||||
const start = count ? offset + 1 : 0;
|
||||
const end = offset + pageSize > count ? count : offset + pageSize;
|
||||
|
||||
const showPaginationButtons = offset > 0 || count > end;
|
||||
|
||||
return (
|
||||
<nav className="px-4 py-3 flex items-center justify-between" aria-label="Pagination">
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-sm text-gray-700">
|
||||
Showing {start} to {end} of {count} results
|
||||
</p>
|
||||
</div>
|
||||
{showPaginationButtons && (
|
||||
<div className="flex-1 flex justify-between sm:justify-end space-x-3">
|
||||
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)} className="!border-0">
|
||||
Previous page
|
||||
</Button>
|
||||
<Button
|
||||
disabled={offset + pageSize >= count}
|
||||
onClick={() => setOffset(offset + pageSize)}
|
||||
className="!border-0"
|
||||
>
|
||||
Next page
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./FileList";
|
|
@ -1,27 +0,0 @@
|
|||
import { useMemo } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { DATE_FORMAT } from "../../lib/config";
|
||||
import humanBytes from "../../lib/humanBytes";
|
||||
|
||||
const parseFileName = (fileName) => {
|
||||
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
|
||||
|
||||
return [fileName.substr(0, lastDotIndex), fileName.substr(lastDotIndex)];
|
||||
};
|
||||
|
||||
const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest }) => {
|
||||
const [name, type] = parseFileName(rawFileName);
|
||||
const date = dayjs(uploadedOn || downloadedOn).format(DATE_FORMAT);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
date,
|
||||
size: humanBytes(size, { precision: 2 }),
|
||||
type,
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
const useFormattedFilesData = (items) => useMemo(() => items.map(formatItem), [items]);
|
||||
|
||||
export default useFormattedFilesData;
|
|
@ -1,35 +0,0 @@
|
|||
import { useMemo, useState } from "react";
|
||||
|
||||
import accountsService from "../../services/accountsService";
|
||||
import skynetClient from "../../services/skynetClient";
|
||||
|
||||
export const useSkylinkOptions = ({ skylink, onUpdated }) => {
|
||||
const [inProgress, setInProgress] = useState(false);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "Preview",
|
||||
callback: async () => window.open(await skynetClient.getSkylinkUrl(skylink)),
|
||||
},
|
||||
{
|
||||
label: "Download",
|
||||
callback: () => skynetClient.downloadFile(skylink),
|
||||
},
|
||||
{
|
||||
label: "Unpin",
|
||||
callback: async () => {
|
||||
setInProgress(true);
|
||||
await accountsService.delete(`user/uploads/${skylink}`);
|
||||
await onUpdated(); // No need to setInProgress(false), since at this point this hook should already be unmounted
|
||||
},
|
||||
},
|
||||
],
|
||||
[skylink, onUpdated]
|
||||
);
|
||||
|
||||
return {
|
||||
inProgress,
|
||||
options,
|
||||
};
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import copy from "copy-text-to-clipboard";
|
||||
|
||||
import skynetClient from "../../services/skynetClient";
|
||||
|
||||
const COPY_LINK_LABEL = "Copy link";
|
||||
|
||||
export const useSkylinkSharing = (skylink) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copyLabel, setCopyLabel] = useState(COPY_LINK_LABEL);
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
setCopyLabel("Copied!");
|
||||
|
||||
const timeout = setTimeout(() => setCopied(false), 1500);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
} else {
|
||||
setCopyLabel(COPY_LINK_LABEL);
|
||||
}
|
||||
}, [copied]);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: copyLabel,
|
||||
callback: async () => {
|
||||
setCopied(true);
|
||||
copy(await skynetClient.getSkylinkUrl(skylink));
|
||||
},
|
||||
},
|
||||
],
|
||||
[skylink, copyLabel]
|
||||
);
|
||||
|
||||
return {
|
||||
options,
|
||||
};
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { PageContainer } from "../PageContainer";
|
||||
|
||||
const FooterLink = styled.a.attrs({
|
||||
className: "text-palette-400 underline decoration-dotted decoration-offset-4 decoration-1",
|
||||
rel: "noreferrer",
|
||||
target: "_blank",
|
||||
})``;
|
||||
|
||||
export const Footer = () => (
|
||||
<PageContainer className="font-content text-palette-300 py-4">
|
||||
<p>
|
||||
Made by <FooterLink href="https://skynetlabs.com">Skynet Labs</FooterLink>. Open-sourced{" "}
|
||||
<FooterLink href="https://github.com/SkynetLabs/skynet-webportal">on Github</FooterLink>.
|
||||
</p>
|
||||
</PageContainer>
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
export * from "./Footer";
|
|
@ -1,56 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
import cn from "classnames";
|
||||
import { Field } from "formik";
|
||||
|
||||
export const TextField = ({ id, label, name, error, touched, className, ...props }) => {
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
{label && (
|
||||
<label className="font-sans uppercase text-palette-300 text-xs" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<Field
|
||||
id={id}
|
||||
name={name}
|
||||
className={cn("w-full py-2 px-4 bg-palette-100 rounded-sm placeholder:text-palette-200 outline-1", className, {
|
||||
"focus:outline outline-palette-200": !error || !touched,
|
||||
"outline outline-error": touched && error,
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
{touched && error && (
|
||||
<div className="text-error">
|
||||
<small>{error}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/** Besides noted properties, it accepts all props accepted by:
|
||||
* - a regular <input> element
|
||||
* - Formik's <Field> component
|
||||
*/
|
||||
TextField.propTypes = {
|
||||
/**
|
||||
* ID for the field. Used to couple <label> and <input> elements
|
||||
*/
|
||||
id: PropTypes.string,
|
||||
/**
|
||||
* Label for the field
|
||||
*/
|
||||
label: PropTypes.string,
|
||||
/**
|
||||
* Name of the field
|
||||
*/
|
||||
name: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Validation error message
|
||||
*/
|
||||
error: PropTypes.string,
|
||||
/**
|
||||
* Indicates wether or not the user touched the field already.
|
||||
*/
|
||||
touched: PropTypes.bool,
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./TextField";
|
|
@ -1,6 +0,0 @@
|
|||
import { Link } from "gatsby";
|
||||
import styled from "styled-components";
|
||||
|
||||
export default styled(Link).attrs({
|
||||
className: "text-primary underline-offset-2 decoration-1 decoration-dotted hover:text-primary-light hover:underline",
|
||||
})``;
|
|
@ -1,60 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const IconButton = ({ primary, size, icon, ...props }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`${
|
||||
size === "small"
|
||||
? "h-iconButtonSm w-buttonIconSm"
|
||||
: size === "large"
|
||||
? "h-iconButtonLg w-iconButtonLg"
|
||||
: "w-iconButton h-iconButton"
|
||||
} rounded-full
|
||||
inline-flex justify-center items-center
|
||||
${primary ? "bg-primary" : null}`}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
size === "small"
|
||||
? "h-buttonIconSm w-buttonIconSm"
|
||||
: size === "large"
|
||||
? "h-buttonIconLg w-buttonIconLg"
|
||||
: "h-buttonIcon w-buttonIcon"
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
IconButton.propTypes = {
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
primary: PropTypes.bool,
|
||||
/**
|
||||
* How large should the button be?
|
||||
*/
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
/**
|
||||
* Icon component
|
||||
*/
|
||||
icon: PropTypes.element.isRequired,
|
||||
/**
|
||||
* Optional click handler
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
IconButton.defaultProps = {
|
||||
backgroundColor: null,
|
||||
primary: false,
|
||||
size: "medium",
|
||||
onClick: undefined,
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
import { IconButton } from "./IconButton";
|
||||
import { ArrowRightIcon } from "../Icons";
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: "SkynetLibrary/IconButton",
|
||||
component: IconButton,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
backgroundColor: { control: "color" },
|
||||
},
|
||||
};
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template = (args) => <IconButton {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
primary: true,
|
||||
icon: <ArrowRightIcon />,
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
icon: <ArrowRightIcon />,
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
size: "large",
|
||||
icon: <ArrowRightIcon />,
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: "small",
|
||||
icon: <ArrowRightIcon />,
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./IconButton";
|
|
@ -1,46 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const IconButtonText = ({ primary, label, icon, ...props }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex justify-center items-center w-iconButtonTextWidth py-iconButtonTextY`}
|
||||
{...props}
|
||||
>
|
||||
<div className={`h-buttonTextIcon w-buttonTextIcon ${primary ? "text-primary" : "text-palette-600"}`}>{icon}</div>
|
||||
<p
|
||||
className={"ml-iconButtonTextTextLeft tracking-wide text-iconButtonText font-sans font-light text-palette-600"}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
IconButtonText.propTypes = {
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
primary: PropTypes.bool,
|
||||
/**
|
||||
* Button Label
|
||||
*/
|
||||
label: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Icon component
|
||||
*/
|
||||
icon: PropTypes.element.isRequired,
|
||||
/**
|
||||
* Optional click handler
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
IconButtonText.defaultProps = {
|
||||
primary: false,
|
||||
label: "",
|
||||
onClick: undefined,
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
import { IconButtonText } from "./IconButtonText";
|
||||
import { CogIcon } from "../Icons";
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: "SkynetLibrary/IconButtonText",
|
||||
component: IconButtonText,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
backgroundColor: { control: "color" },
|
||||
},
|
||||
};
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template = (args) => <IconButtonText {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
primary: true,
|
||||
label: "Settings",
|
||||
icon: <CogIcon />,
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
label: "Settings",
|
||||
icon: <CogIcon />,
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./IconButtonText";
|
|
@ -1,37 +0,0 @@
|
|||
import { Panel } from "../Panel";
|
||||
import * as icons from ".";
|
||||
|
||||
export default {
|
||||
title: "SkynetLibrary/Icons",
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ margin: 50 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const DefaultSizeIcon = () => <icons.SkynetLogoIcon />;
|
||||
|
||||
export const LargeIcon = () => <icons.SkynetLogoIcon size={60} />;
|
||||
|
||||
export const AllIcons = () => {
|
||||
const sizes = [24, 32, 60];
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(icons).map(([iconName, IconComponent]) => (
|
||||
<Panel key={`panel-${iconName}`}>
|
||||
<pre>{iconName}</pre>
|
||||
|
||||
<div style={{ padding: 10, border: "1px dashed #fafafa", display: "flex", alignItems: "center", gap: 50 }}>
|
||||
{sizes.map((size) => (
|
||||
<IconComponent key={`${iconName}-${size}`} size={size} />
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const ArrowRightIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M10 15h9.616L16.3 11.71l1.409-1.42 4.995 4.954a1 1 0 0 1 .09 1.32l-.084.094-5 5.046-1.42-1.408 3.265-3.295L10 17v-2z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,25 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
|
||||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CheckmarkIcon = withIconProps(({ size, circled, ...props }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
{circled && <circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />}
|
||||
<polygon
|
||||
fill="currentColor"
|
||||
points="22.45 11.19 23.86 12.61 14.44 22.03 9.69 17.28 11.1 15.86 14.44 19.2 22.45 11.19"
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
|
||||
CheckmarkIcon.propTypes = {
|
||||
...CheckmarkIcon.propTypes,
|
||||
circled: PropTypes.bool,
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const ChevronDownIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M21.5 14.005 16.546 19 11.5 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,18 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CircledArrowUpIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.21,10.51a1,1,0,0,1,1.32.09l4.95,5L21.06,17l-3.29-3.32V23h-2V13.75L12.48,17,11.07,15.6l5-5Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,19 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CircledErrorIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />
|
||||
<polygon
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
points="21.72 10.25 23.14 11.66 18.19 16.61 23.14 21.56 21.72 22.98 16.77 18.02 11.82 22.98 10.41 21.56 15.36 16.61 10.41 11.66 11.82 10.25 16.77 15.2 21.72 10.25"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,22 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CircledProgressIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />
|
||||
<rect fill="currentColor" x="15" y="22" width="2" height="4" />
|
||||
<rect fill="currentColor" x="8.34" y="20.66" width="4" height="2" transform="translate(-12.28 13.66) rotate(-45)" />
|
||||
<rect fill="currentColor" x="20.66" y="19.66" width="2" height="4" transform="translate(-8.97 21.66) rotate(-45)" />
|
||||
<rect fill="currentColor" x="6" y="15" width="4" height="2" />
|
||||
<rect fill="currentColor" x="22" y="15" width="4" height="2" />
|
||||
<rect fill="currentColor" x="9.34" y="8.34" width="2" height="4" transform="translate(-4.28 10.34) rotate(-45)" />
|
||||
<rect fill="currentColor" x="19.66" y="9.34" width="4" height="2" transform="translate(-0.97 18.34) rotate(-45)" />
|
||||
<rect fill="currentColor" x="15" y="6" width="2" height="4" />
|
||||
</svg>
|
||||
));
|
|
@ -1,11 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CogIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M16 4a3 3 0 0 1 3 3v.086c.001.26.156.493.404.6a.647.647 0 0 0 .709-.123l.204-.195a3 3 0 0 1 4.1 4.38l-.052.051a.65.65 0 0 0-.13.717 1 1 0 0 1 .047.13l.012.06.045.062c.1.12.242.2.397.225l.094.007H25a3 3 0 0 1 0 6h-.086a.654.654 0 0 0-.6.404.647.647 0 0 0 .123.709l.195.204a3 3 0 0 1-4.38 4.1l-.051-.052a.654.654 0 0 0-.727-.126.649.649 0 0 0-.394.591V25a3 3 0 0 1-6 0v-.067c-.006-.266-.175-.502-.484-.618a.647.647 0 0 0-.709.122l-.204.195a3 3 0 0 1-4.1-4.38l.052-.051a.654.654 0 0 0 .126-.727.649.649 0 0 0-.591-.394H7a3 3 0 0 1 0-6h.067c.266-.006.502-.175.618-.484a.647.647 0 0 0-.122-.709l-.195-.204a3 3 0 0 1 4.38-4.1l.051.052a.65.65 0 0 0 .717.13 1 1 0 0 1 .13-.047l.06-.013.062-.044c.12-.1.2-.242.225-.397L13 7.17V7a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v.174a2.65 2.65 0 0 1-1.606 2.425A1 1 0 0 1 13 9.68l-.032.043a2.654 2.654 0 0 1-2.433-.537l-.142-.129-.06-.06a1 1 0 1 0-1.416 1.416l.068.068c.757.774.967 1.932.554 2.864l-.073.177A2.66 2.66 0 0 1 7.09 15.08H7a1 1 0 0 0 0 2h.174a2.646 2.646 0 0 1 2.42 1.596 2.654 2.654 0 0 1-.537 2.931l-.06.06a1 1 0 1 0 1.416 1.416l.068-.068c.774-.757 1.932-.967 2.864-.554l.177.073a2.66 2.66 0 0 1 1.558 2.376V25a1 1 0 0 0 2 0v-.174a2.646 2.646 0 0 1 1.596-2.42 2.654 2.654 0 0 1 2.931.537l.06.06a1 1 0 1 0 1.416-1.416l-.068-.068a2.646 2.646 0 0 1-.534-2.913A2.651 2.651 0 0 1 24.91 17H25a1 1 0 0 0 0-2h-.174a2.65 2.65 0 0 1-2.425-1.606A1 1 0 0 1 22.32 13l-.043-.032a2.654 2.654 0 0 1 .537-2.433l.129-.142.06-.06a1 1 0 1 0-1.416-1.416l-.068.068a2.646 2.646 0 0 1-2.913.534A2.651 2.651 0 0 1 17 7.09V7a1 1 0 0 0-1-1zm0 6a4 4 0 1 1 0 8 4 4 0 0 1 0-8zm0 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,10 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CopyIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M26.35,11.29A5.65,5.65,0,0,1,32,16.94v9.41A5.65,5.65,0,0,1,26.35,32H16.94a5.65,5.65,0,0,1-5.65-5.65V16.94a5.65,5.65,0,0,1,5.65-5.65Zm0,3.77H16.94a1.88,1.88,0,0,0-1.88,1.88v9.41a1.89,1.89,0,0,0,1.88,1.89h9.41a1.89,1.89,0,0,0,1.89-1.89V16.94A1.89,1.89,0,0,0,26.35,15.06ZM16.22,0A4.49,4.49,0,0,1,20.7,4.18V5.79A1.89,1.89,0,0,1,17,6V4.49a.73.73,0,0,0-.58-.71l-.14,0H4.49a.74.74,0,0,0-.71.58l0,.15V16.22a.73.73,0,0,0,.58.71H5.79A1.88,1.88,0,0,1,6,20.69l-.22,0H4.49A4.49,4.49,0,0,1,0,16.53v-12A4.48,4.48,0,0,1,4.18,0h12Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,24 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const FolderUploadIcon = withIconProps((props) => (
|
||||
<svg width="64" height="56" viewBox="0 0 64 56" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path fill="#00c65e" fillRule="evenodd" d="M26.1,48.27V34H20.35L31,23,41.34,34H36V52Z" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#222829"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
d="M27,56V34H21L32,23,43,34H37V56M32,33v2m0,2v2m0,2v2m0,2v2m0,2v2m0,2v2"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#222829"
|
||||
strokeWidth="2"
|
||||
d="M58,17H40m18,4H40M12,17h2m2,0h2m2,0h2M20,51H4M59,8V5H23L19,1H1V48a3,3,0,0,0,6,0V11H63V51H43.8"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
));
|
|
@ -1,11 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const ImportantNoteIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<g fill="currentColor" fillRule="nonzero">
|
||||
<path d="M16.028 6c5.523 0 10 4.477 10 10s-4.477 10-10 10h-9c-.405.017-.78-.212-.95-.58-.156-.372-.074-.802.21-1.09l2-2C6.82 20.549 6.02 18.31 6.028 16c0-5.523 4.477-10 10-10zm3.05 2.607c-3.526-1.458-7.592-.222-9.71 2.953-2.119 3.174-1.7 7.403 1 10.1.189.185.296.436.3.7 0 .267-.109.523-.3.71l-.93.93h6.59c3.817-.003 7.1-2.701 7.841-6.445.742-3.744-1.264-7.49-4.79-8.948zM16.028 18c.552 0 1 .448 1 1s-.448 1-1 1-1-.448-1-1 .448-1 1-1zM17 12v5h-2v-5h2z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
));
|
|
@ -1,11 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const InfoIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M16 5c6.075 0 11 4.925 11 11s-4.925 11-11 11S5 22.075 5 16 9.925 5 16 5zm0 2a9 9 0 1 0 0 18 9 9 0 0 0 0-18zm0 6a1 1 0 0 1 .993.883L17 14v7a1 1 0 0 1-1.993.117L15 21v-7a1 1 0 0 1 1-1zm.01-3a1 1 0 0 1 .117 1.993L16 12a1 1 0 0 1-.117-1.993L16.01 10z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,11 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const LockClosedIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M14.083 6.383A5 5 0 0 1 19.53 7.46a5.09 5.09 0 0 1 1.31 2.29 1.002 1.002 0 0 1-1.94.5 3.08 3.08 0 0 0-.78-1.38A3 3 0 0 0 13 11v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H11a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3v-2a5 5 0 0 1 3.083-4.617zM21 15H11a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1zm-4 2v5h-2v-5h2z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,14 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const PlusIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<path d="M18.67,0V13.33H32v5.34H18.67V32H13.33V18.67H0V13.33H13.33V0Z" fill="currentColor" />
|
||||
</svg>
|
||||
));
|
|
@ -1,10 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const SearchIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9,0a9,9,0,0,1,7,14.62l3.68,3.67a1,1,0,0,1-1.32,1.5l-.1-.08L14.62,16A9,9,0,1,1,9,0ZM9,2a7,7,0,1,0,4.87,12l.07-.09.09-.07A7,7,0,0,0,9,2Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,18 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const ShareIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 29.09 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M24.73,0a5.82,5.82,0,1,1-4.14,9.91l-7.72,4.51a5.85,5.85,0,0,1,0,3.16l7.73,4.5a5.81,5.81,0,1,1-1.47,2.51l-7.72-4.5a5.82,5.82,0,1,1,0-8.22l0,0L19.13,7.4a5.82,5.82,0,0,1,4-7.18A5.69,5.69,0,0,1,24.73,0Zm0,23.27a2.93,2.93,0,0,0-2.43,1.3,1,1,0,0,1-.07.15l-.09.14-.05.09A2.91,2.91,0,1,0,26,23.54,2.86,2.86,0,0,0,24.73,23.27ZM7.27,13.09a2.91,2.91,0,1,0,2.51,4.37l0-.05A2.93,2.93,0,0,0,10.18,16a2.89,2.89,0,0,0-.4-1.46v0A2.9,2.9,0,0,0,7.27,13.09ZM24.73,2.91a2.92,2.92,0,0,0-2.55,4.32l0,0v0a2.91,2.91,0,1,0,2.5-4.4Z"
|
||||
transform="translate(-1.46 0)"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,10 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const SimpleUploadIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19,12a1,1,0,0,1,1,.88V17a3,3,0,0,1-2.82,3H3a3,3,0,0,1-3-2.82V13a1,1,0,0,1,2-.12V17a1,1,0,0,0,.88,1H17a1,1,0,0,0,1-.88V13A1,1,0,0,1,19,12ZM10,0h0a1,1,0,0,1,.62.22l.09.07,5,5a1,1,0,0,1-1.32,1.5l-.1-.08L11,3.41V13a1,1,0,0,1-2,.12V3.41L5.71,6.71a1,1,0,0,1-1.32.08l-.1-.08a1,1,0,0,1-.08-1.32l.08-.1,5-5L9.38.22h0L9.29.29A1.05,1.05,0,0,1,10,0Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,8 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const SkynetLogoIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg role="img" width={size} fill="#00C65E" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<title>Skynet</title>
|
||||
<path d="m-.0004 6.4602 21.3893 11.297c.561.2935.6633 1.0532.1999 1.4846h-.011a10.0399 10.0399 0 0 1-2.2335 1.5307c-6.912 3.4734-14.9917-1.838-14.5438-9.5605l2.8601 1.9752c.856 4.508 5.6187 7.1094 9.8742 5.3932zm8.6477 3.1509 14.3661 5.6785a.8704.8704 0 0 1 .5197 1.0466v.0182c-.1537.5377-.7668.7938-1.2575.5252zm5.2896-7.4375c2.7093-.2325 6.0946.7869 8.1116 3.3871 1.699 2.1951 2.0497 4.8772 1.9298 7.6465v-.007c-.0478.5874-.6494.9616-1.1975.745l-9.7652-3.8596 9.0656 2.4313a7.296 7.296 0 0 0-1.0677-4.5631c-2.9683-4.7678-9.9847-4.5344-12.6297.4201a7.5048 7.5048 0 0 0-.398.8831L5.5546 7.9614c.069-.1017.1417-.198.2144-.2962.1163-.2416.2417-.487.3798-.7268 1.6118-2.7911 4.3102-4.4338 7.1558-4.6973.2108-.0182.4215-.049.6323-.0672z"></path>
|
||||
</svg>
|
||||
));
|
|
@ -1,10 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const TrashIcon = withIconProps(({ size, ...props }) => (
|
||||
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14.01 15.33" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8.33,0a2.33,2.33,0,0,1,2.33,2.17v.5H13a1,1,0,0,1,.12,2h-.46V13a2.32,2.32,0,0,1-2.17,2.33H3.67a2.33,2.33,0,0,1-2.33-2.17V4.67H1a1,1,0,0,1-.12-2H3.33V2.33A2.33,2.33,0,0,1,5.51,0H8.33Zm-5,4.67V13a.34.34,0,0,0,.27.33h6.73a.33.33,0,0,0,.33-.26V4.67ZM8.33,2H5.67a.33.33,0,0,0-.33.27v.4H8.66V2.33A.33.33,0,0,0,8.4,2Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
|
@ -1,40 +0,0 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const UploadIcon = withIconProps((props) => (
|
||||
<svg
|
||||
width="64"
|
||||
height="55"
|
||||
viewBox="0 0 64 55"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
shapeRendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenOdd"
|
||||
fill="#00C65E"
|
||||
d="M26.1,47.3V33h-5.7L31,22l10.3,11H36v18L26.1,47.3z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#0D0D0D"
|
||||
strokeWidth="2"
|
||||
d="M42,41h9c6.6,0,12-5.4,12-12c0-5.9-4-11-10-11c0-5-3-9-9-9h-1c-2.7-4.9-8-8-14-8c-8.8,0-16,7.2-16,16
|
||||
C6.4,17,1,22.4,1,29c0,6.6,5.4,12,12,12h9"
|
||||
/>
|
||||
<path fill="none" stroke="#0D0D0D" strokeWidth="2" d="M19,18c0-6.1,5-11,10-11" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#0D0D0D"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
d="M26,55V33h-6l11-11l11,11h-6v22 M31,32v2 M31,36v2 M31,40v2 M31,44v2 M31,48v2 M31,52v2"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
));
|
|
@ -1,17 +0,0 @@
|
|||
export * from "./icons/ChevronDownIcon";
|
||||
export * from "./icons/CogIcon";
|
||||
export * from "./icons/LockClosedIcon";
|
||||
export * from "./icons/SkynetLogoIcon";
|
||||
export * from "./icons/ArrowRightIcon";
|
||||
export * from "./icons/InfoIcon";
|
||||
export * from "./icons/CheckmarkIcon";
|
||||
export * from "./icons/CircledErrorIcon";
|
||||
export * from "./icons/CircledProgressIcon";
|
||||
export * from "./icons/CircledArrowUpIcon";
|
||||
export * from "./icons/PlusIcon";
|
||||
export * from "./icons/SearchIcon";
|
||||
export * from "./icons/CopyIcon";
|
||||
export * from "./icons/ShareIcon";
|
||||
export * from "./icons/SimpleUploadIcon";
|
||||
export * from "./icons/TrashIcon";
|
||||
export * from "./icons/ImportantNoteIcon";
|
|
@ -1,19 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
|
||||
const propTypes = {
|
||||
/**
|
||||
* Size of the icon's bounding box.
|
||||
*/
|
||||
size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
size: 32,
|
||||
};
|
||||
|
||||
export const withIconProps = (IconComponent) => {
|
||||
IconComponent.propTypes = propTypes;
|
||||
IconComponent.defaultProps = defaultProps;
|
||||
|
||||
return IconComponent;
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
import * as React from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||
import useFormattedFilesData from "../FileList/useFormattedFilesData";
|
||||
|
||||
import { ViewAllLink } from "./ViewAllLink";
|
||||
|
||||
export default function ActivityTable({ type }) {
|
||||
const { data, error } = useSWR(`user/${type}?pageSize=3`);
|
||||
const items = useFormattedFilesData(data?.items || []);
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||
{/* TODO: proper error message */}
|
||||
{!data && !error && <ContainerLoadingIndicator />}
|
||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||
{data && !error && <p>No files found.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table style={{ tableLayout: "fixed" }}>
|
||||
<TableBody>
|
||||
{items.map(({ id, name, type, size, date, skylink }) => (
|
||||
<TableRow key={id}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell className="w-[80px]">{type}</TableCell>
|
||||
<TableCell className="w-[80px]" align="right">
|
||||
{size}
|
||||
</TableCell>
|
||||
<TableCell className="w-[180px]">{date}</TableCell>
|
||||
<TableCell>{skylink}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<ViewAllLink to={`/files?tab=${type}`} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { Panel } from "../Panel";
|
||||
|
||||
import ActivityTable from "./ActivityTable";
|
||||
|
||||
export default function LatestActivity() {
|
||||
return (
|
||||
<Panel title="Latest uploads">
|
||||
<ActivityTable type="uploads" />
|
||||
</Panel>
|
||||
);
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { Link } from "gatsby";
|
||||
|
||||
import { ArrowRightIcon } from "../Icons";
|
||||
|
||||
export const ViewAllLink = (props) => (
|
||||
<Link className="inline-flex mt-6 items-center gap-3 ease-in-out hover:brightness-90" {...props}>
|
||||
<span className="bg-primary rounded-full w-[32px] h-[32px] inline-flex justify-center items-center">
|
||||
<ArrowRightIcon />
|
||||
</span>
|
||||
<span className="font-sans text-xs uppercase text-palette-400">View all</span>
|
||||
</Link>
|
||||
);
|
|
@ -1,3 +0,0 @@
|
|||
import LatestActivity from "./LatestActivity";
|
||||
|
||||
export default LatestActivity;
|
|
@ -1,18 +0,0 @@
|
|||
import styled from "styled-components";
|
||||
import { CircledProgressIcon } from "../Icons";
|
||||
|
||||
/**
|
||||
* This loading indicator is designed to be replace entire blocks (i.e. components)
|
||||
* while they are fetching required data.
|
||||
*
|
||||
* It will take 50% of the parent's height, but won't get bigger than 150x150 px.
|
||||
*/
|
||||
const Wrapper = styled.div.attrs({
|
||||
className: "flex w-full h-full justify-center items-center p-8 text-palette-100",
|
||||
})``;
|
||||
|
||||
export const ContainerLoadingIndicator = (props) => (
|
||||
<Wrapper {...props}>
|
||||
<CircledProgressIcon size="50%" className="max-w-[150px] max-h-[150px] animate-[spin_3s_linear_infinite]" />
|
||||
</Wrapper>
|
||||
);
|
|
@ -1,7 +0,0 @@
|
|||
import { ContainerLoadingIndicator } from "./ContainerLoadingIndicator";
|
||||
|
||||
export const FullScreenLoadingIndicator = () => (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
|
||||
<ContainerLoadingIndicator className="!text-palette-200/50" />
|
||||
</div>
|
||||
);
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ContainerLoadingIndicator";
|
||||
export * from "./FullScreenLoadingIndicator";
|
|
@ -1,42 +0,0 @@
|
|||
import { Helmet } from "react-helmet";
|
||||
import { graphql, useStaticQuery } from "gatsby";
|
||||
|
||||
import favicon from "../../../static/favicon.ico";
|
||||
import favicon16 from "../../../static/favicon-16x16.png";
|
||||
import favicon32 from "../../../static/favicon-32x32.png";
|
||||
import appleIcon144 from "../../../static/apple-touch-icon-144x144.png";
|
||||
import appleIcon152 from "../../../static/apple-touch-icon-152x152.png";
|
||||
import msTileIcon from "../../../static/mstile-144x144.png";
|
||||
|
||||
export const Metadata = ({ children }) => {
|
||||
const { site } = useStaticQuery(
|
||||
graphql`
|
||||
query Q {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const { title } = site.siteMetadata;
|
||||
|
||||
return (
|
||||
<Helmet htmlAttributes={{ lang: "en" }} titleTemplate={`%s | ${title}`} defaultTitle={title}>
|
||||
<meta name="application-name" content="Skynet Account Dashboard" />
|
||||
<link rel="icon" type="image/x-icon" href={favicon} />
|
||||
<link rel="icon" type="image/png" href={favicon16} sizes="16x16" />
|
||||
<link rel="icon" type="image/png" href={favicon32} sizes="32x32" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href={appleIcon144} />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href={appleIcon152} />
|
||||
<meta name="msapplication-TileColor" content="#FFFFFF" />
|
||||
<meta name="msapplication-TileImage" content={msTileIcon} />
|
||||
|
||||
<meta name="description" content="Manage your Skynet uploads, account subscription, settings and API keys" />
|
||||
<link rel="preconnect" href={`https://${process.env.GATSBY_PORTAL_DOMAIN}/`} />
|
||||
{children}
|
||||
</Helmet>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from "./Metadata";
|
|
@ -1,37 +0,0 @@
|
|||
import cn from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { PlusIcon } from "../Icons";
|
||||
import { Panel } from "../Panel";
|
||||
|
||||
import { ModalPortal } from "./ModalPortal";
|
||||
import { Overlay } from "./Overlay";
|
||||
|
||||
export const Modal = ({ children, className, onClose }) => (
|
||||
<ModalPortal>
|
||||
<Overlay onClick={onClose}>
|
||||
<div className={cn("relative w-modal max-w-modal shadow-sm rounded")}>
|
||||
<button onClick={onClose} className="absolute top-[20px] right-[20px]" aria-label="Close">
|
||||
<PlusIcon size={14} className="rotate-45" />
|
||||
</button>
|
||||
<Panel className={cn("px-8 py-6 sm:px-12 sm:py-10", className)}>{children}</Panel>
|
||||
</div>
|
||||
</Overlay>
|
||||
</ModalPortal>
|
||||
);
|
||||
|
||||
Modal.propTypes = {
|
||||
/**
|
||||
* Modal's body.
|
||||
*/
|
||||
children: PropTypes.node.isRequired,
|
||||
/**
|
||||
* Handler function to be called when user clicks on the "X" icon,
|
||||
* or outside of the modal.
|
||||
*/
|
||||
onClose: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Additional CSS classes to be applied to modal's body.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
export const MODAL_ROOT_ID = "__modal-root";
|
||||
|
||||
export const ModalPortal = ({ children }) => {
|
||||
const ref = useRef();
|
||||
const [isClientSide, setIsClientSide] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = document.querySelector(MODAL_ROOT_ID) || document.body;
|
||||
setIsClientSide(true);
|
||||
}, []);
|
||||
|
||||
return isClientSide ? createPortal(children, ref.current) : null;
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
import { useRef } from "react";
|
||||
import { useLockBodyScroll } from "react-use";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export const Overlay = ({ children, onClick }) => {
|
||||
const overlayRef = useRef(null);
|
||||
|
||||
useLockBodyScroll(true);
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (event.target !== overlayRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
role="presentation"
|
||||
onClick={handleClick}
|
||||
className="fixed inset-0 z-50 bg-palette-100/80 flex items-center justify-center"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Overlay.propTypes = {
|
||||
/**
|
||||
* Overlay's body.
|
||||
*/
|
||||
children: PropTypes.node.isRequired,
|
||||
/**
|
||||
* Handler function to be called when user clicks on the overlay
|
||||
* (but not the overlay's content).
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ModalPortal";
|
||||
export * from "./Modal";
|
|
@ -1,110 +0,0 @@
|
|||
import { Link, navigate } from "gatsby";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { screen } from "../../lib/cssHelpers";
|
||||
import { useUser } from "../../contexts/user";
|
||||
import { DropdownMenu, DropdownMenuLink } from "../DropdownMenu";
|
||||
import { CogIcon, LockClosedIcon, SkynetLogoIcon } from "../Icons";
|
||||
import { PageContainer } from "../PageContainer";
|
||||
|
||||
import { NavBarLink, NavBarSection } from ".";
|
||||
import accountsService from "../../services/accountsService";
|
||||
|
||||
const NavBarContainer = styled.div.attrs({
|
||||
className: `grid sticky top-0 bg-white z-10 shadow-sm`,
|
||||
})``;
|
||||
|
||||
const NavBarBody = styled.nav.attrs({
|
||||
className: "grid font-sans font-light text-xs sm:text-sm",
|
||||
})`
|
||||
height: 100px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 60px 40px;
|
||||
grid-template-areas:
|
||||
"logo dropdown"
|
||||
"navigation navigation";
|
||||
|
||||
${screen(
|
||||
"sm",
|
||||
`
|
||||
height: 80px;
|
||||
grid-template-columns: auto max-content 1fr;
|
||||
grid-template-areas: "logo navigation dropdown";
|
||||
grid-template-rows: auto;
|
||||
`
|
||||
)}
|
||||
|
||||
.navigation-area {
|
||||
grid-area: navigation;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
grid-area: logo;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.dropdown-area {
|
||||
grid-area: dropdown;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NavBar = () => {
|
||||
const { mutate: setUserState } = useUser();
|
||||
|
||||
const onLogout = async () => {
|
||||
try {
|
||||
await accountsService.post("logout");
|
||||
// Don't refresh user state from server, as it will now respond with UNAUTHORIZED
|
||||
// and user will be redirected to /auth/login with return_to query param (which we want empty).
|
||||
await setUserState(null, { revalidate: false });
|
||||
navigate("/auth/login");
|
||||
} catch {
|
||||
// Do nothing.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NavBarContainer>
|
||||
<PageContainer className="px-0">
|
||||
<NavBarBody>
|
||||
<NavBarSection className="logo-area pl-2 pr-4 md:px-0 md:w-[110px] justify-center sm:justify-start">
|
||||
<SkynetLogoIcon size={48} />
|
||||
</NavBarSection>
|
||||
<NavBarSection className="navigation-area border-t border-palette-100">
|
||||
<NavBarLink to="/" as={Link} activeClassName="!border-b-primary">
|
||||
Dashboard
|
||||
</NavBarLink>
|
||||
<NavBarLink to="/files" as={Link} activeClassName="!border-b-primary">
|
||||
Files
|
||||
</NavBarLink>
|
||||
<NavBarLink to="/payments" as={Link} activeClassName="!border-b-primary">
|
||||
Payments
|
||||
</NavBarLink>
|
||||
</NavBarSection>
|
||||
<NavBarSection className="dropdown-area justify-end">
|
||||
<DropdownMenu title="My account">
|
||||
<DropdownMenuLink
|
||||
to="/settings"
|
||||
as={Link}
|
||||
activeClassName="text-primary"
|
||||
icon={CogIcon}
|
||||
label="Settings"
|
||||
partiallyActive
|
||||
/>
|
||||
<DropdownMenuLink
|
||||
as="button"
|
||||
onClick={onLogout}
|
||||
activeClassName="text-primary"
|
||||
className="cursor-pointer w-full"
|
||||
icon={LockClosedIcon}
|
||||
label="Log out"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</NavBarSection>
|
||||
</NavBarBody>
|
||||
</PageContainer>
|
||||
</NavBarContainer>
|
||||
);
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
import { NavBar, NavBarLink, NavBarSection } from ".";
|
||||
|
||||
export default {
|
||||
title: "SkynetLibrary/NavBar",
|
||||
component: NavBar,
|
||||
subcomponents: {
|
||||
NavBarSection,
|
||||
NavBarLink,
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (props) => <NavBar {...props} />;
|
||||
|
||||
export const DashboardTopNavigation = Template.bind({});
|
||||
DashboardTopNavigation.args = {};
|
|
@ -1,19 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
|
||||
export const NavBarLink = styled.a.attrs(({ active }) => ({
|
||||
className: `
|
||||
sm:min-w-[133px] lg:min-w-[168px]
|
||||
flex h-full items-center justify-center
|
||||
border-x border-x-palette-100 border-b-2
|
||||
text-palette-600 transition-colors hover:bg-palette-100/50
|
||||
${active ? "border-b-primary" : "border-b-palette-200/50"}
|
||||
`,
|
||||
}))``;
|
||||
|
||||
NavBarLink.propTypes = {
|
||||
/**
|
||||
* When set to true, an additional indicator will be rendered showing the item as active.
|
||||
*/
|
||||
active: PropTypes.bool,
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
export const NavBarSection = styled.div.attrs({ className: "flex items-center" })``;
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./NavBar";
|
||||
export * from "./NavBarSection";
|
||||
export * from "./NavBarLink";
|
|
@ -1,13 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
|
||||
export const PageContainer = styled.div.attrs({
|
||||
className: `mx-auto w-page lg:w-page-lg xl:w-page-xl px-2 md:px-16 lg:px-0`,
|
||||
})``;
|
||||
|
||||
PageContainer.propTypes = {
|
||||
/**
|
||||
* Optional `class` attribute.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue