Merge branch 'master' into include-subscription-info
This commit is contained in:
commit
ccee4be9a8
|
@ -20,18 +20,10 @@ updates:
|
|||
directory: "/packages/website"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/accounts"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/caddy"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/handshake"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/nginx"
|
||||
schedule:
|
||||
|
|
|
@ -43,5 +43,7 @@ jobs:
|
|||
uses: skynetlabs/deploy-to-skynet-action@v2
|
||||
with:
|
||||
upload-dir: packages/website/public
|
||||
portal-url: https://skynetpro.net
|
||||
skynet-jwt: ${{ secrets.SKYNET_JWT }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry-seed: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && secrets.WEBSITE_REGISTRY_SEED || '' }}
|
||||
|
|
|
@ -20,7 +20,11 @@ services:
|
|||
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
|
||||
|
||||
accounts:
|
||||
image: skynetlabs/skynet-accounts
|
||||
build:
|
||||
context: ./docker/accounts
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
branch: main
|
||||
container_name: accounts
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
FROM golang:1.16.7
|
||||
LABEL maintainer="SkynetLabs <devs@siasky.net>"
|
||||
|
||||
ENV GOOS linux
|
||||
ENV GOARCH amd64
|
||||
|
||||
ARG branch=main
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
RUN git clone --single-branch --branch ${branch} https://github.com/SkynetLabs/skynet-accounts.git && \
|
||||
cd skynet-accounts && \
|
||||
go mod download && \
|
||||
make release
|
||||
|
||||
ENV SKYNET_DB_HOST="localhost"
|
||||
ENV SKYNET_DB_PORT="27017"
|
||||
ENV SKYNET_DB_USER="username"
|
||||
ENV SKYNET_DB_PASS="password"
|
||||
ENV SKYNET_ACCOUNTS_PORT=3000
|
||||
|
||||
ENTRYPOINT ["skynet-accounts"]
|
|
@ -25,8 +25,7 @@
|
|||
"dns": {
|
||||
"provider": {
|
||||
"name": "route53"
|
||||
},
|
||||
"ttl": "30m"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,15 @@
|
|||
server {
|
||||
server_name account.{{PORTAL_DOMAIN}}; # example: account.siasky.net
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain {
|
||||
-- fall back to portal domain if server domain is not defined
|
||||
if "{{SERVER_DOMAIN}}" == "" then
|
||||
return "{{PORTAL_DOMAIN}}"
|
||||
end
|
||||
return "{{SERVER_DOMAIN}}"
|
||||
}
|
||||
|
||||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.key;
|
||||
|
||||
|
@ -31,6 +40,9 @@
|
|||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.key;
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
|
||||
|
||||
include /etc/nginx/conf.d/server/server.account;
|
||||
|
||||
set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
|
||||
|
|
|
@ -8,6 +8,15 @@ server {
|
|||
server {
|
||||
server_name {{PORTAL_DOMAIN}}; # example: siasky.net
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain {
|
||||
-- fall back to portal domain if server domain is not defined
|
||||
if "{{SERVER_DOMAIN}}" == "" then
|
||||
return "{{PORTAL_DOMAIN}}"
|
||||
end
|
||||
return "{{SERVER_DOMAIN}}"
|
||||
}
|
||||
|
||||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{PORTAL_DOMAIN}}/{{PORTAL_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{PORTAL_DOMAIN}}/{{PORTAL_DOMAIN}}.key;
|
||||
|
||||
|
@ -30,6 +39,9 @@ server {
|
|||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{SERVER_DOMAIN}}/{{SERVER_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{SERVER_DOMAIN}}/{{SERVER_DOMAIN}}.key;
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
|
||||
|
||||
include /etc/nginx/conf.d/server/server.api;
|
||||
|
||||
set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
|
||||
|
|
|
@ -8,6 +8,15 @@ server {
|
|||
server {
|
||||
server_name *.hns.{{PORTAL_DOMAIN}}; # example: *.hns.siasky.net
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain {
|
||||
-- fall back to portal domain if server domain is not defined
|
||||
if "{{SERVER_DOMAIN}}" == "" then
|
||||
return "{{PORTAL_DOMAIN}}"
|
||||
end
|
||||
return "{{SERVER_DOMAIN}}"
|
||||
}
|
||||
|
||||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{PORTAL_DOMAIN}}/wildcard_.hns.{{PORTAL_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{PORTAL_DOMAIN}}/wildcard_.hns.{{PORTAL_DOMAIN}}.key;
|
||||
|
||||
|
@ -31,6 +40,9 @@ server {
|
|||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{SERVER_DOMAIN}}/wildcard_.hns.{{SERVER_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{SERVER_DOMAIN}}/wildcard_.hns.{{SERVER_DOMAIN}}.key;
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
|
||||
|
||||
proxy_set_header Host {{SERVER_DOMAIN}};
|
||||
include /etc/nginx/conf.d/server/server.hns;
|
||||
|
||||
|
|
|
@ -8,6 +8,15 @@ server {
|
|||
server {
|
||||
server_name *.{{PORTAL_DOMAIN}}; # example: *.siasky.net
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain {
|
||||
-- fall back to portal domain if server domain is not defined
|
||||
if "{{SERVER_DOMAIN}}" == "" then
|
||||
return "{{PORTAL_DOMAIN}}"
|
||||
end
|
||||
return "{{SERVER_DOMAIN}}"
|
||||
}
|
||||
|
||||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.key;
|
||||
|
||||
|
@ -27,6 +36,9 @@ server {
|
|||
server {
|
||||
server_name *.{{SERVER_DOMAIN}}; # example: *.eu-ger-1.siasky.net
|
||||
|
||||
set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
|
||||
set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
|
||||
|
||||
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.crt;
|
||||
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.key;
|
||||
|
||||
|
|
|
@ -81,8 +81,8 @@ proxy_pass https://127.0.0.1/$skylink$path$is_args$args;
|
|||
|
||||
# in case siad returns location header, we need to replace the skylink with the domain name
|
||||
header_filter_by_lua_block {
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
||||
|
||||
if ngx.header.location then
|
||||
-- match location redirect part after the skylink
|
||||
|
|
|
@ -85,8 +85,8 @@ access_by_lua_block {
|
|||
}
|
||||
|
||||
header_filter_by_lua_block {
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
||||
|
||||
-- the block below only makes sense if we are using nginx cache
|
||||
if not ngx.var.skyd_disk_cache_enabled then
|
||||
|
|
|
@ -35,9 +35,23 @@ location /api/register {
|
|||
proxy_pass http://accounts:3000;
|
||||
}
|
||||
|
||||
location /api/user/pubkey/register {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
rewrite /api/(.*) /$1 break;
|
||||
proxy_pass http://accounts:3000;
|
||||
}
|
||||
|
||||
location /api/login {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
rewrite /api/(.*) /$1 break;
|
||||
proxy_pass http://accounts:3000;
|
||||
}
|
||||
|
||||
location /api/logout {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
rewrite /api/(.*) /$1 break;
|
||||
proxy_pass http://accounts:3000;
|
||||
}
|
||||
|
|
|
@ -46,15 +46,29 @@ location /docs {
|
|||
location /skynet/blocklist {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
add_header X-Proxy-Cache $upstream_cache_status;
|
||||
|
||||
proxy_cache skynet;
|
||||
proxy_cache_valid any 1m; # cache blocklist for 1 minute
|
||||
proxy_set_header User-Agent: Sia-Agent;
|
||||
proxy_pass http://sia:9980/skynet/blocklist;
|
||||
}
|
||||
|
||||
location /skynet/portal/blocklist {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
add_header X-Proxy-Cache $upstream_cache_status;
|
||||
|
||||
proxy_cache skynet;
|
||||
proxy_cache_valid 200 204 15m; # cache portal blocklist for 15 minutes
|
||||
proxy_pass http://blocker:4000/blocklist;
|
||||
}
|
||||
|
||||
location /skynet/portals {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
add_header X-Proxy-Cache $upstream_cache_status;
|
||||
|
||||
proxy_cache skynet;
|
||||
proxy_cache_valid any 1m; # cache portals for 1 minute
|
||||
proxy_set_header User-Agent: Sia-Agent;
|
||||
|
@ -64,6 +78,8 @@ location /skynet/portals {
|
|||
location /skynet/stats {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
add_header X-Proxy-Cache $upstream_cache_status;
|
||||
|
||||
proxy_cache skynet;
|
||||
proxy_cache_valid any 1m; # cache stats for 1 minute
|
||||
proxy_set_header User-Agent: Sia-Agent;
|
||||
|
@ -89,6 +105,8 @@ location /serverload {
|
|||
location /skynet/health {
|
||||
include /etc/nginx/conf.d/include/cors;
|
||||
|
||||
add_header X-Proxy-Cache $upstream_cache_status;
|
||||
|
||||
proxy_cache skynet;
|
||||
proxy_cache_key $request_uri; # use whole request uri (uri + args) as cache key
|
||||
proxy_cache_valid any 1m; # cache responses for 1 minute
|
||||
|
@ -247,8 +265,7 @@ location /skynet/tus {
|
|||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# rewrite proxy request to use correct host uri from env variable (required to return correct location header)
|
||||
set_by_lua_block $server_domain { return os.getenv("SERVER_DOMAIN") }
|
||||
proxy_redirect $scheme://$host $scheme://$server_domain;
|
||||
proxy_redirect $scheme://$host $scheme://$skynet_server_domain;
|
||||
|
||||
# proxy /skynet/tus requests to siad endpoint with all arguments
|
||||
proxy_pass http://sia:9980;
|
||||
|
@ -275,8 +292,8 @@ location /skynet/tus {
|
|||
|
||||
# extract skylink from base64 encoded upload metadata and assign to a proper header
|
||||
header_filter_by_lua_block {
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
||||
|
||||
if ngx.header["Upload-Metadata"] then
|
||||
local encodedSkylink = string.match(ngx.header["Upload-Metadata"], "Skylink ([^,?]+)")
|
||||
|
@ -310,8 +327,8 @@ location /skynet/metadata {
|
|||
include /etc/nginx/conf.d/include/portal-access-check;
|
||||
|
||||
header_filter_by_lua_block {
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
||||
}
|
||||
|
||||
proxy_set_header User-Agent: Sia-Agent;
|
||||
|
@ -323,8 +340,8 @@ location /skynet/resolve {
|
|||
include /etc/nginx/conf.d/include/portal-access-check;
|
||||
|
||||
header_filter_by_lua_block {
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
||||
}
|
||||
|
||||
proxy_set_header User-Agent: Sia-Agent;
|
||||
|
|
|
@ -95,8 +95,8 @@ http {
|
|||
|
||||
# include skynet-portal-api and skynet-server-api header on every request
|
||||
header_filter_by_lua_block {
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
|
||||
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
|
||||
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
|
||||
}
|
||||
|
||||
# ratelimit specified IPs
|
||||
|
|
|
@ -3,6 +3,7 @@ module.exports = {
|
|||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"storybook-addon-gatsby",
|
||||
{
|
||||
name: "@storybook/addon-postcss",
|
||||
options: {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: `Accounts Dashboard`,
|
||||
|
@ -9,6 +11,7 @@ module.exports = {
|
|||
"gatsby-plugin-react-helmet",
|
||||
"gatsby-plugin-sharp",
|
||||
"gatsby-transformer-sharp",
|
||||
"gatsby-plugin-styled-components",
|
||||
"gatsby-plugin-postcss",
|
||||
{
|
||||
resolve: "gatsby-source-filesystem",
|
||||
|
@ -19,4 +22,14 @@ module.exports = {
|
|||
__key: "images",
|
||||
},
|
||||
],
|
||||
developMiddleware: (app) => {
|
||||
app.use(
|
||||
"/api/",
|
||||
createProxyMiddleware({
|
||||
target: "https://account.siasky.net",
|
||||
secure: false, // Do not reject self-signed certificates.
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import * as React from "react";
|
||||
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 function wrapPageElement({ element, props }) {
|
||||
const Layout = element.type.Layout ?? React.Fragment;
|
||||
return <Layout {...props}>{element}</Layout>;
|
||||
}
|
|
@ -21,13 +21,23 @@
|
|||
"dependencies": {
|
||||
"@fontsource/sora": "^4.5.3",
|
||||
"@fontsource/source-sans-pro": "^4.5.3",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"dayjs": "^1.10.8",
|
||||
"gatsby": "^4.6.2",
|
||||
"gatsby-plugin-postcss": "^5.7.0",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"nanoid": "^3.3.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.6",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"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": "^3.0.2",
|
||||
"swr": "^1.2.2",
|
||||
"tailwindcss": "^3.0.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -44,20 +54,24 @@
|
|||
"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",
|
||||
"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.7.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,41 +1,21 @@
|
|||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const Button = ({ primary, label, ...props }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`min-w-button min-h-button rounded-full font-sans uppercase tracking-wide text-button
|
||||
${primary ? "bg-primary" : "bg-white border-2 border-black"}`}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const Button = styled.button.attrs(({ $primary }) => ({
|
||||
type: "button",
|
||||
className: `px-6 py-3 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter] hover:brightness-90
|
||||
${$primary ? "bg-primary" : "bg-white border-2 border-black"}`,
|
||||
}))``;
|
||||
Button.propTypes = {
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
primary: PropTypes.bool,
|
||||
/**
|
||||
* What background color to use
|
||||
*/
|
||||
backgroundColor: PropTypes.string,
|
||||
/**
|
||||
* Button contents
|
||||
*/
|
||||
label: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Optional click handler
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
$primary: PropTypes.bool,
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
primary: false,
|
||||
onClick: undefined,
|
||||
$primary: false,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import { useUser } from "../../contexts/user";
|
||||
import useSubscriptionPlans from "../../hooks/useSubscriptionPlans";
|
||||
|
||||
import LatestPayment from "./LatestPayment";
|
||||
import SuggestedPlan from "./SuggestedPlan";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const CurrentPlan = () => {
|
||||
const { user, error: userError } = useUser();
|
||||
const { activePlan, plans, error: plansError } = useSubscriptionPlans(user);
|
||||
|
||||
if (!user || !activePlan) {
|
||||
return (
|
||||
// TODO: a nicer loading indicator
|
||||
<div className="flex flex-col space-y-4 h-full justify-center items-center">Loading...</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
<h4>{activePlan.name}</h4>
|
||||
<div className="text-palette-400">
|
||||
{activePlan.price === 0 && <p>100GB 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>
|
||||
))}
|
||||
<LatestPayment user={user} />
|
||||
<SuggestedPlan plans={plans} activePlan={activePlan} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentPlan;
|
|
@ -0,0 +1,18 @@
|
|||
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;
|
|
@ -0,0 +1,24 @@
|
|||
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={`/upgrade?selectedPlan=${nextPlan.id}`} className="mt-6">
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestedPlan;
|
|
@ -0,0 +1,3 @@
|
|||
import CurrentPlan from "./CurrentPlan";
|
||||
|
||||
export default CurrentPlan;
|
|
@ -0,0 +1,71 @@
|
|||
import * as React from "react";
|
||||
import fileSize from "pretty-bytes";
|
||||
import { Link } from "gatsby";
|
||||
|
||||
import { GraphBar } from "./GraphBar";
|
||||
import { UsageGraph } from "./UsageGraph";
|
||||
|
||||
// TODO: get real data
|
||||
const useUsageData = () => ({
|
||||
files: {
|
||||
used: 19_521,
|
||||
limit: 20_000,
|
||||
},
|
||||
storage: {
|
||||
used: 23_000_000_000,
|
||||
limit: 1_000_000_000_000,
|
||||
},
|
||||
});
|
||||
|
||||
const size = (bytes) => {
|
||||
const text = fileSize(bytes, { maximumFractionDigits: 1 });
|
||||
const [value, unit] = text.split(" ");
|
||||
|
||||
return {
|
||||
text,
|
||||
value,
|
||||
unit,
|
||||
};
|
||||
};
|
||||
|
||||
export default function CurrentUsage() {
|
||||
const { files, storage } = useUsageData();
|
||||
|
||||
const storageUsage = size(storage.used);
|
||||
const storageLimit = size(storage.limit);
|
||||
const filesUsedLabel = React.useMemo(() => ({ value: files.used, unit: "files" }), [files.used]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>
|
||||
{storageUsage.text} of {storageLimit.text}
|
||||
</h4>
|
||||
<p className="text-palette-400">
|
||||
{files.used} of {files.limit} 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={storage.used} limit={storage.limit} label={storageUsage} />
|
||||
<GraphBar value={files.used} limit={files.limit} label={filesUsedLabel} />
|
||||
</UsageGraph>
|
||||
<div className="flex place-content-between">
|
||||
<span>Files</span>
|
||||
<span className="inline-flex place-content-between w-[37%]">
|
||||
<Link
|
||||
to="/upgrade"
|
||||
className="text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline"
|
||||
>
|
||||
UPGRADE
|
||||
</Link>{" "}
|
||||
{/* TODO: proper URL */}
|
||||
<span>{files.limit}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
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: "bg-white rounded border-2 border-palette-200 px-3 whitespace-nowrap absolute shadow",
|
||||
})`
|
||||
right: max(0%, ${({ $percentage }) => 100 - $percentage}%);
|
||||
top: -0.5rem;
|
||||
transform: translateX(50%);
|
||||
`;
|
||||
|
||||
export const GraphBar = ({ value, limit, label }) => {
|
||||
const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<Bar $percentage={percentage}>
|
||||
<BarTip />
|
||||
</Bar>
|
||||
<BarLabel $percentage={percentage}>
|
||||
<span className="font-sora text-lg">{label.value}</span> <span>{label.unit}</span>
|
||||
</BarLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
export const UsageGraph = styled.div.attrs({
|
||||
className: "w-full my-3 grid grid-flow-row grid-rows-2",
|
||||
})`
|
||||
height: 146px;
|
||||
background: url(/images/usage-graph-bg.svg) no-repeat;
|
||||
background-size: cover;
|
||||
`;
|
|
@ -0,0 +1,3 @@
|
|||
import CurrentUsage from "./CurrentUsage";
|
||||
|
||||
export default CurrentUsage;
|
|
@ -0,0 +1,18 @@
|
|||
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>
|
||||
));
|
|
@ -0,0 +1,18 @@
|
|||
import { withIconProps } from "../withIconProps";
|
||||
|
||||
export const CircledCheckmarkIcon = 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"
|
||||
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>
|
||||
));
|
|
@ -0,0 +1,19 @@
|
|||
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>
|
||||
));
|
|
@ -0,0 +1,22 @@
|
|||
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>
|
||||
));
|
|
@ -0,0 +1,24 @@
|
|||
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>
|
||||
));
|
|
@ -0,0 +1,14 @@
|
|||
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>
|
||||
));
|
|
@ -0,0 +1,40 @@
|
|||
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>
|
||||
));
|
|
@ -4,3 +4,8 @@ export * from "./icons/LockClosedIcon";
|
|||
export * from "./icons/SkynetLogoIcon";
|
||||
export * from "./icons/ArrowRightIcon";
|
||||
export * from "./icons/InfoIcon";
|
||||
export * from "./icons/CircledCheckmarkIcon";
|
||||
export * from "./icons/CircledErrorIcon";
|
||||
export * from "./icons/CircledProgressIcon";
|
||||
export * from "./icons/CircledArrowUpIcon";
|
||||
export * from "./icons/PlusIcon";
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import * as React from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
||||
|
||||
import useFormattedActivityData from "./useFormattedActivityData";
|
||||
|
||||
export default function ActivityTable({ type }) {
|
||||
const { data, error } = useSWR(`user/${type}?pageSize=3`);
|
||||
const items = useFormattedActivityData(data?.items || []);
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||
{/* TODO: proper loading indicator / error message */}
|
||||
{!data && !error && <p>Loading...</p>}
|
||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||
{data && <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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react";
|
||||
import { Link } from "gatsby";
|
||||
|
||||
import { Panel } from "../Panel";
|
||||
import { Tab, TabPanel, Tabs } from "../Tabs";
|
||||
import { ArrowRightIcon } from "../Icons";
|
||||
|
||||
import ActivityTable from "./ActivityTable";
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
export default function LatestActivity() {
|
||||
return (
|
||||
<Panel title="Latest activity">
|
||||
<Tabs>
|
||||
<Tab id="uploads" title="Uploads" />
|
||||
<Tab id="downloads" title="Downloads" />
|
||||
<TabPanel tabId="uploads" className="pt-4">
|
||||
<ActivityTable type="uplodads" />
|
||||
<ViewAllLink to="/files?tab=uploads" />
|
||||
</TabPanel>
|
||||
<TabPanel tabId="downloads" className="pt-4">
|
||||
<ActivityTable type="downloads" />
|
||||
<ViewAllLink to="/files?tab=downloads" />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Panel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import LatestActivity from "./LatestActivity";
|
||||
|
||||
export default LatestActivity;
|
|
@ -0,0 +1,26 @@
|
|||
import { useMemo } from "react";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
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("MM/DD/YYYY; HH:MM");
|
||||
|
||||
return {
|
||||
...rest,
|
||||
date,
|
||||
size: prettyBytes(size),
|
||||
type,
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
const useFormattedActivityData = (items) => useMemo(() => items.map(formatItem), [items]);
|
||||
|
||||
export default useFormattedActivityData;
|
|
@ -1,21 +1,78 @@
|
|||
import { Link } from "gatsby";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { screen } from "../../lib/cssHelpers";
|
||||
import { DropdownMenu, DropdownMenuLink } from "../DropdownMenu";
|
||||
import { CogIcon, LockClosedIcon, SkynetLogoIcon } from "../Icons";
|
||||
import { PageContainer } from "../PageContainer";
|
||||
|
||||
import { NavBarLink, NavBarSection } from ".";
|
||||
|
||||
const NavBarContainer = styled.div.attrs({
|
||||
className: `grid sticky top-0 bg-white`,
|
||||
})``;
|
||||
|
||||
const NavBarBody = styled.nav.attrs({
|
||||
className: "grid h-[80px] font-sans font-light text-sm",
|
||||
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 = (props) => (
|
||||
export const NavBar = () => (
|
||||
<NavBarContainer>
|
||||
<PageContainer>
|
||||
<NavBarBody {...props} />
|
||||
<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 href="/settings" icon={CogIcon} label="Settings" />
|
||||
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
|
||||
</DropdownMenu>
|
||||
</NavBarSection>
|
||||
</NavBarBody>
|
||||
</PageContainer>
|
||||
</NavBarContainer>
|
||||
);
|
||||
|
|
|
@ -9,17 +9,7 @@ export default {
|
|||
},
|
||||
};
|
||||
|
||||
const Template = (props) => (
|
||||
<NavBar {...props}>
|
||||
<NavBarSection>
|
||||
<NavBarLink href="/dashboard" active>
|
||||
Dashboard
|
||||
</NavBarLink>
|
||||
<NavBarLink href="/files">Files</NavBarLink>
|
||||
<NavBarLink href="/payments">Payments</NavBarLink>
|
||||
</NavBarSection>
|
||||
</NavBar>
|
||||
);
|
||||
const Template = (props) => <NavBar {...props} />;
|
||||
|
||||
export const DashboardTopNavigation = Template.bind({});
|
||||
DashboardTopNavigation.args = {};
|
||||
|
|
|
@ -3,7 +3,7 @@ import styled from "styled-components";
|
|||
|
||||
export const NavBarLink = styled.a.attrs(({ active }) => ({
|
||||
className: `
|
||||
min-w-[168px]
|
||||
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
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
|||
import styled from "styled-components";
|
||||
|
||||
export const PageContainer = styled.div.attrs({
|
||||
className: `mx-auto w-page md:w-page-md lg:w-page-lg xl:w-page-xl`,
|
||||
className: `mx-auto w-page lg:w-page-lg xl:w-page-xl px-2 md:px-16 lg:px-0`,
|
||||
})``;
|
||||
|
||||
PageContainer.propTypes = {
|
||||
|
|
|
@ -25,7 +25,7 @@ Panel.propTypes = {
|
|||
/**
|
||||
* Label of the panel
|
||||
*/
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
};
|
||||
|
||||
Panel.defaultProps = {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import PropTypes from "prop-types";
|
||||
|
||||
export default function Bullets({ visibleSlides, activeIndex, allSlides, changeSlide }) {
|
||||
if (allSlides <= visibleSlides) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 pt-6">
|
||||
{Array(allSlides - visibleSlides + 1)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`rounded-full w-3 h-3 ${activeIndex === index ? "bg-primary" : "border-2 cursor-pointer"}`}
|
||||
onClick={(event) => changeSlide(event, index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Bullets.propTypes = {
|
||||
allSlides: PropTypes.number.isRequired,
|
||||
activeIndex: PropTypes.number.isRequired,
|
||||
visibleSlides: PropTypes.number.isRequired,
|
||||
changeSlide: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import styled from "styled-components";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Slide = styled.div.attrs(({ isVisible }) => ({
|
||||
className: `slider-slide transition-opacity ${isVisible ? "" : "opacity-50 cursor-pointer"}`,
|
||||
}))``;
|
||||
|
||||
Slide.propTypes = {
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Slide;
|
|
@ -0,0 +1,128 @@
|
|||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import useActiveBreakpoint from "./useActiveBreakpoint";
|
||||
import Bullets from "./Bullets";
|
||||
import Slide from "./Slide";
|
||||
|
||||
const Container = styled.div.attrs({
|
||||
className: "slider w-full",
|
||||
})``;
|
||||
|
||||
/**
|
||||
* Styles applied to the movable element when the number of slide elements
|
||||
* exceeds the number of visible slides for the current breakpoint
|
||||
* */
|
||||
const scrollableStyles = css`
|
||||
${({ $allSlides, $visibleSlides, $activeIndex }) => `
|
||||
transform: translateX(calc(-1 * ${$activeIndex} * ((100% + 1rem) / ${$visibleSlides})));
|
||||
grid-template-columns: repeat(${$allSlides}, calc((100% - ${$visibleSlides - 1}rem) / ${$visibleSlides}));
|
||||
`}
|
||||
`;
|
||||
|
||||
const Scroller = styled.div.attrs({
|
||||
className: "slider-scroller grid gap-4 transition-transform",
|
||||
})`
|
||||
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
|
||||
`;
|
||||
|
||||
const Slider = ({ slides, breakpoints }) => {
|
||||
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
|
||||
const [activeIndex, setActiveIndex] = React.useState(0);
|
||||
const changeSlide = React.useCallback(
|
||||
(event, index) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setActiveIndex(Math.min(index, slides.length - visibleSlides)); // Don't let it scroll too far
|
||||
},
|
||||
[slides, visibleSlides, setActiveIndex]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const maxIndex = slides.length - visibleSlides;
|
||||
|
||||
// Make sure to not scroll too far when screen size changes.
|
||||
if (activeIndex > maxIndex) {
|
||||
setActiveIndex(maxIndex);
|
||||
}
|
||||
}, [slides.length, visibleSlides, activeIndex]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Scroller
|
||||
$visibleSlides={visibleSlides}
|
||||
$allSlides={slides.length}
|
||||
$activeIndex={activeIndex}
|
||||
$scrollable={scrollable}
|
||||
>
|
||||
{slides.map((slide, index) => {
|
||||
const isVisible = index >= activeIndex && index < activeIndex + visibleSlides;
|
||||
|
||||
return (
|
||||
<div key={`slide-${index}`}>
|
||||
<Slide
|
||||
isVisible={isVisible || !scrollable}
|
||||
onClickCapture={scrollable && !isVisible ? (event) => changeSlide(event, index) : null}
|
||||
>
|
||||
{slide}
|
||||
</Slide>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Scroller>
|
||||
{scrollable && (
|
||||
<Bullets
|
||||
activeIndex={activeIndex}
|
||||
allSlides={slides.length}
|
||||
visibleSlides={visibleSlides}
|
||||
changeSlide={changeSlide}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Slider.propTypes = {
|
||||
slides: PropTypes.arrayOf(PropTypes.node.isRequired),
|
||||
breakpoints: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
/**
|
||||
* Breakpoint name as defined in Tailwind config. If not defined, config
|
||||
* will be applied to all non-configured screen sizes.
|
||||
*/
|
||||
name: PropTypes.string,
|
||||
/**
|
||||
* Number of slides visible for a given breakpoint.
|
||||
*/
|
||||
visibleSlides: PropTypes.number.isRequired,
|
||||
/**
|
||||
* Whether or not the list should be scrollable horizontally at the given breakpoint.
|
||||
* If set to false, all slides will be visible & rendered in a column.
|
||||
*/
|
||||
scrollable: PropTypes.bool.isRequired,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
Slider.defaultProps = {
|
||||
breakpoints: [
|
||||
{
|
||||
name: "xl",
|
||||
scrollable: true,
|
||||
visibleSlides: 3,
|
||||
},
|
||||
{
|
||||
name: "md",
|
||||
scrollable: true,
|
||||
visibleSlides: 2,
|
||||
},
|
||||
{
|
||||
// For the smallest screens, we won't scroll but instead stack the slides vertically.
|
||||
scrollable: false,
|
||||
visibleSlides: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Slider;
|
|
@ -0,0 +1 @@
|
|||
export * from "./Slider";
|
|
@ -0,0 +1,38 @@
|
|||
import { useEffect, useMemo, useCallback, useState } from "react";
|
||||
import { useWindowSize } from "react-use";
|
||||
|
||||
import theme from "../../lib/theme";
|
||||
|
||||
const { screens } = theme;
|
||||
|
||||
export default function useActiveBreakpoint(breakpoints) {
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const monitoredBreakpoints = useMemo(
|
||||
() =>
|
||||
breakpoints
|
||||
.slice()
|
||||
// Map breakpoint names to their min-width configured in Tailwind
|
||||
.map(({ name, ...config }) => {
|
||||
// If breakpoint name is not configured,
|
||||
// we'll apply this config to all unmatched breakpoints.
|
||||
const minWidth = screens[name] ? parseInt(screens[name], 10) : -Infinity;
|
||||
|
||||
return { minWidth, ...config };
|
||||
})
|
||||
// Since our breakpoints are setup with min-width rule, we need to sort them from largest to smallest
|
||||
.sort(({ minWidth: widthA }, { minWidth: widthB }) => widthB - widthA),
|
||||
[breakpoints]
|
||||
);
|
||||
const findActiveBreakpoint = useCallback(
|
||||
() => monitoredBreakpoints.find((breakpoint) => windowWidth >= breakpoint.minWidth),
|
||||
[monitoredBreakpoints, windowWidth]
|
||||
);
|
||||
|
||||
const [activeBreakpoint, setActiveBreakpoint] = useState(findActiveBreakpoint());
|
||||
|
||||
useEffect(() => {
|
||||
setActiveBreakpoint(findActiveBreakpoint());
|
||||
}, [findActiveBreakpoint]);
|
||||
|
||||
return activeBreakpoint;
|
||||
}
|
|
@ -4,7 +4,7 @@ import styled from "styled-components";
|
|||
* Accepts all HMTL attributes a `<td>` element does.
|
||||
*/
|
||||
export const TableCell = styled.td.attrs({
|
||||
className: `px-6 py-4 h-tableRow truncate
|
||||
className: `first:pl-6 last:pr-6 px-2 py-4 h-tableRow truncate
|
||||
text-palette-600 even:text-palette-400
|
||||
first:rounded-l-sm last:rounded-r-sm`,
|
||||
})`
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import { useWindowSize } from "react-use";
|
||||
|
||||
const Wrapper = styled.div.attrs({
|
||||
className: "absolute left-0 bottom-0 w-full h-0.5 bg-palette-200",
|
||||
|
@ -15,6 +16,7 @@ const Indicator = styled.div.attrs({
|
|||
export const ActiveTabIndicator = ({ tabRef }) => {
|
||||
const [position, setPosition] = useState(0);
|
||||
const [width, setWidth] = useState(0);
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
if (!tabRef?.current) {
|
||||
|
@ -24,7 +26,7 @@ export const ActiveTabIndicator = ({ tabRef }) => {
|
|||
const { offsetLeft, offsetWidth } = tabRef.current;
|
||||
setPosition(offsetLeft);
|
||||
setWidth(offsetWidth);
|
||||
}, [tabRef]);
|
||||
}, [tabRef, windowWidth]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
@ -33,6 +35,9 @@ export const ActiveTabIndicator = ({ tabRef }) => {
|
|||
);
|
||||
};
|
||||
|
||||
ActiveTabIndicator.propTypes = {
|
||||
// Needed, because we're using an Element constant here which Gatsby doesn't recognize during build time.
|
||||
if (typeof window !== "undefined") {
|
||||
ActiveTabIndicator.propTypes = {
|
||||
tabRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,15 +1,33 @@
|
|||
import PropTypes from "prop-types";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Besides documented props, it accepts all HMTL attributes a `<div>` element does.
|
||||
*/
|
||||
export const TabPanel = ({ children, active, tabId, ...props }) => {
|
||||
if (!active) {
|
||||
const [wasActivated, setWasActivated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setWasActivated(true);
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
// If the TabPanel was never activated, let's not render its children at all.
|
||||
// We'll only render them after the first activation and then won't unmount them
|
||||
// unless the entire TabPanel is unmounted, too.
|
||||
if (!active && !wasActivated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div role="tabpanel" id={`tabpanel-${tabId}`} aria-labelledby={`tab-${tabId}`} {...props}>
|
||||
<div
|
||||
role="tabpanel"
|
||||
id={`tabpanel-${tabId}`}
|
||||
aria-labelledby={`tab-${tabId}`}
|
||||
{...props}
|
||||
style={{ display: active ? "block" : "none" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,11 +6,11 @@ import { ActiveTabIndicator } from "./ActiveTabIndicator";
|
|||
import { usePrefixedTabIds, useTabsChildren } from "./hooks";
|
||||
|
||||
const Container = styled.div.attrs({
|
||||
className: "tabs-container",
|
||||
className: "tabs-container flex flex-col h-full",
|
||||
})``;
|
||||
|
||||
const Header = styled.div.attrs({
|
||||
className: "relative flex justify-start overflow-hidden",
|
||||
className: "relative flex justify-start overflow-hidden grow-0 shrink-0",
|
||||
})``;
|
||||
|
||||
const TabList = styled.div.attrs(({ variant }) => ({
|
||||
|
@ -26,7 +26,7 @@ const Divider = styled.div.attrs({
|
|||
right: calc(-100vw - 2px);
|
||||
`;
|
||||
|
||||
const Body = styled.div``;
|
||||
const Body = styled.div.attrs({ className: "grow min-h-0" })``;
|
||||
|
||||
/**
|
||||
* Besides documented props, it accepts all HMTL attributes a `<div>` element does.
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import cn from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
import styled, { css, keyframes } from "styled-components";
|
||||
|
||||
const moveAnimation = keyframes`
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 15px 0;
|
||||
}
|
||||
`;
|
||||
const Container = styled.div.attrs(({ $status }) => ({
|
||||
className: cn("flex relative rounded-sm h-1", { "bg-palette-200": $status === "uploading" }),
|
||||
}))``;
|
||||
|
||||
const Indicator = styled.div.attrs(({ $status }) => ({
|
||||
className: cn(
|
||||
`
|
||||
rounded-sm bg-[length:15px_10px]
|
||||
`,
|
||||
{
|
||||
"bg-primary": $status === "uploading" || $status === "complete",
|
||||
"text-primary": $status !== "error",
|
||||
"text-error": $status === "error",
|
||||
"bg-dashed": $status === "error" || $status === "enqueued" || $status === "processing",
|
||||
}
|
||||
),
|
||||
}))`
|
||||
width: ${({ $status, $percentage }) => ($status === "uploading" ? $percentage : 100)}%;
|
||||
&.bg-dashed {
|
||||
opacity: 0.4;
|
||||
background-image: linear-gradient(
|
||||
-60deg,
|
||||
transparent,
|
||||
transparent 30%,
|
||||
currentColor 30%,
|
||||
currentColor 70%,
|
||||
transparent 70%,
|
||||
transparent
|
||||
);
|
||||
animation: ${css`
|
||||
${moveAnimation} 1s linear infinite
|
||||
`};
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Primary UI component for indicating progress of a given task
|
||||
*/
|
||||
export const ProgressBar = ({ status, percentage, ...props }) => (
|
||||
<Container $status={status} {...props}>
|
||||
<Indicator $status={status} $percentage={percentage} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
ProgressBar.propTypes = {
|
||||
/**
|
||||
* Status of the task
|
||||
*/
|
||||
status: PropTypes.oneOf(["complete", "enqueued", "error", "uploading", "processing"]),
|
||||
/**
|
||||
* Progress of the task (in case status is "uploading")
|
||||
*/
|
||||
percentage: PropTypes.number,
|
||||
};
|
||||
|
||||
ProgressBar.defaultProps = {
|
||||
status: "enqueued",
|
||||
percentage: 0,
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
import React from "react";
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: "SkynetLibrary/ProgressBar",
|
||||
component: ProgressBar,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
};
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template = (args) => <ProgressBar {...args} />;
|
||||
|
||||
export const Uploading = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Uploading.args = {
|
||||
status: "uploading",
|
||||
percentage: 65,
|
||||
};
|
||||
|
||||
export const Complete = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Complete.args = {
|
||||
status: "complete",
|
||||
};
|
||||
|
||||
export const Enqueued = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Enqueued.args = {
|
||||
status: "enqueued",
|
||||
};
|
||||
|
||||
export const Error = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Error.args = {
|
||||
status: "error",
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cn from "classnames";
|
||||
import path from "path-browserify";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { UploadIcon } from "../Icons/icons/UploadIcon";
|
||||
import { FolderUploadIcon } from "../Icons/icons/FolderUploadIcon";
|
||||
|
||||
import UploaderItem from "./UploaderItem";
|
||||
import { PlusIcon } from "../Icons";
|
||||
|
||||
const MAX_PARALLEL_UPLOADS = 1;
|
||||
|
||||
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;
|
||||
|
||||
const getRootDirectory = (file) => {
|
||||
const filePath = getFilePath(file);
|
||||
const { root, dir } = path.parse(filePath);
|
||||
|
||||
return path.normalize(dir).slice(root.length).split(path.sep)[0];
|
||||
};
|
||||
|
||||
const Uploader = ({ mode }) => {
|
||||
const [uploads, setUploads] = useState([]);
|
||||
|
||||
const onUploadStateChange = useCallback((id, state) => {
|
||||
setUploads((uploads) => {
|
||||
const index = uploads.findIndex((upload) => upload.id === id);
|
||||
|
||||
return [...uploads.slice(0, index), { ...uploads[index], ...state }, ...uploads.slice(index + 1)];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDrop = async (files) => {
|
||||
if (mode === "directory" && files.length) {
|
||||
const name = getRootDirectory(files[0]); // get the file path from the first file
|
||||
const size = files.reduce((acc, file) => acc + file.size, 0);
|
||||
|
||||
files = [{ name, size, files }];
|
||||
}
|
||||
|
||||
setUploads((uploads) => [
|
||||
...files.map((file) => ({ id: nanoid(), file, progress: 0, mode, status: "enqueued" })),
|
||||
...uploads,
|
||||
]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const enqueued = uploads.filter(({ status }) => status === "enqueued");
|
||||
const uploading = uploads.filter(({ status }) => ["uploading", "processing", "retrying"].includes(status));
|
||||
const queue = enqueued.slice(0, MAX_PARALLEL_UPLOADS - uploading.length).map(({ id }) => id);
|
||||
|
||||
if (queue.length && uploading.length < MAX_PARALLEL_UPLOADS) {
|
||||
setUploads((uploads) => {
|
||||
return uploads.map((upload) => {
|
||||
if (queue.includes(upload.id)) return { ...upload, status: "uploading" };
|
||||
return upload;
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [uploads]);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, inputRef } = useDropzone({
|
||||
onDrop: handleDrop,
|
||||
useFsAccessApi: false,
|
||||
});
|
||||
const inputElement = inputRef.current;
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputElement) return;
|
||||
if (mode === "directory") inputElement.setAttribute("webkitdirectory", "true");
|
||||
if (mode === "file") inputElement.removeAttribute("webkitdirectory");
|
||||
}, [inputElement, mode]);
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div
|
||||
className={cn("relative px-8 text-palette-400 text-center flex flex-col justify-center", {
|
||||
"drop-active": isDragActive,
|
||||
"min-h-full": uploads.length === 0,
|
||||
"bg-palette-100/50": !isDragActive,
|
||||
"bg-palette-100": isDragActive,
|
||||
})}
|
||||
{...getRootProps()}
|
||||
disabled={true}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{uploads.length === 0 ? (
|
||||
<div className="flex flex-col items-center">
|
||||
{mode === "file" ? (
|
||||
<>
|
||||
<UploadIcon />
|
||||
<p className="py-4">Add, or drop your files here to pin to Skynet</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FolderUploadIcon />
|
||||
<p className="py-4">Drop any folder with an index.html file to deploy to Skynet</p>
|
||||
</>
|
||||
)}
|
||||
<Button $primary>Add files</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-5">
|
||||
<Button $primary className="w-[40px] h-[40px] !p-0 inline-flex justify-center items-center">
|
||||
<PlusIcon size={12} />
|
||||
</Button>
|
||||
<span className="ml-4">Add, or drop your files here</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{uploads.length > 0 && (
|
||||
<div className="flex flex-col space-y-4 py-10">
|
||||
{uploads.map((upload) => (
|
||||
<UploaderItem key={upload.id} onUploadStateChange={onUploadStateChange} upload={upload} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Uploader.propTypes = {
|
||||
mode: PropTypes.oneOf(["file", "directory"]),
|
||||
};
|
||||
|
||||
Uploader.defaultProps = {
|
||||
mode: "file",
|
||||
};
|
||||
|
||||
export default Uploader;
|
|
@ -0,0 +1,133 @@
|
|||
import * as React from "react";
|
||||
import cn from "classnames";
|
||||
import bytes from "pretty-bytes";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import copy from "copy-text-to-clipboard";
|
||||
import path from "path-browserify";
|
||||
import { useTimeoutFn } from "react-use";
|
||||
import { SkynetClient } from "skynet-js";
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
import UploaderItemIcon from "./UploaderItemIcon";
|
||||
import buildUploadErrorMessage from "./buildUploadErrorMessage";
|
||||
|
||||
const skynetClient = new SkynetClient("https://siasky.net"); //TODO: proper API url
|
||||
|
||||
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;
|
||||
|
||||
const getRelativeFilePath = (file) => {
|
||||
const filePath = getFilePath(file);
|
||||
const { root, dir, base } = path.parse(filePath);
|
||||
const relative = path.normalize(dir).slice(root.length).split(path.sep).slice(1);
|
||||
|
||||
return path.join(...relative, base);
|
||||
};
|
||||
|
||||
export default function UploaderItem({ onUploadStateChange, upload }) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const [, , reset] = useTimeoutFn(() => setCopied(false), 3000);
|
||||
const [retryTimeout, setRetryTimeout] = React.useState(3000); // retry delay after "429: TOO_MANY_REQUESTS"
|
||||
|
||||
const handleCopy = (url) => {
|
||||
copy(url);
|
||||
setCopied(true);
|
||||
reset();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (upload.status === "uploading" && !upload.startedTime) {
|
||||
onUploadStateChange(upload.id, { startedTime: Date.now() });
|
||||
|
||||
(async () => {
|
||||
const onUploadProgress = (progress) => {
|
||||
const status = progress === 1 ? "processing" : "uploading";
|
||||
|
||||
onUploadStateChange(upload.id, { status, progress });
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (upload.mode === "directory") {
|
||||
const files = upload.file.files;
|
||||
const directory = files.reduce((acc, file) => ({ ...acc, [getRelativeFilePath(file)]: file }), {});
|
||||
const name = encodeURIComponent(upload.file.name);
|
||||
|
||||
response = await skynetClient.uploadDirectory(directory, name, { onUploadProgress });
|
||||
} else {
|
||||
response = await skynetClient.uploadFile(upload.file, { onUploadProgress });
|
||||
}
|
||||
|
||||
const url = await skynetClient.getSkylinkUrl(response.skylink, { subdomain: upload.mode === "directory" });
|
||||
|
||||
onUploadStateChange(upload.id, { status: "complete", url });
|
||||
} catch (error) {
|
||||
if (error?.response?.status === StatusCodes.TOO_MANY_REQUESTS) {
|
||||
onUploadStateChange(upload.id, { status: "retrying", progress: 0 });
|
||||
|
||||
setTimeout(() => {
|
||||
onUploadStateChange(upload.id, { status: "enqueued", startedTime: null });
|
||||
setRetryTimeout((timeout) => timeout * 2); // increase timeout on next retry
|
||||
}, retryTimeout);
|
||||
} else {
|
||||
onUploadStateChange(upload.id, { status: "error", error: buildUploadErrorMessage(error) });
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [onUploadStateChange, upload, retryTimeout]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-[40px_1fr_min-content]">
|
||||
<div className="p-1">
|
||||
<UploaderItemIcon status={upload.status} />
|
||||
</div>
|
||||
<div className="flex flex-col ml-3 overflow-hidden">
|
||||
<div className="text-palette-600">
|
||||
<div className="truncate">{upload.file.name}</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-palette-400 text-xs space-x-2 items-end">
|
||||
<div className="font-content truncate">
|
||||
{upload.status === "uploading" && (
|
||||
<span className="tabular-nums">
|
||||
Uploading {bytes(upload.file.size * upload.progress)} of {bytes(upload.file.size)}
|
||||
</span>
|
||||
)}
|
||||
{upload.status === "enqueued" && <span className="text-palette-300">Upload in queue, please wait</span>}
|
||||
{upload.status === "processing" && <span className="text-palette-300">Processing...</span>}
|
||||
{upload.status === "complete" && (
|
||||
<a href={upload.url} className="hover:text-primary transition-colors duration-200">
|
||||
{upload.url}
|
||||
</a>
|
||||
)}
|
||||
{upload.status === "error" && upload.error && <span className="text-error">{upload.error}</span>}
|
||||
{upload.status === "retrying" && (
|
||||
<span>Too many parallel requests, retrying in {retryTimeout / 1000} seconds</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base self-end ml-2">
|
||||
{upload.status === "uploading" && (
|
||||
<span className="uppercase tabular-nums">{Math.floor(upload.progress * 100)}%</span>
|
||||
)}
|
||||
{upload.status === "processing" && <span className="uppercase text-palette-300">Wait</span>}
|
||||
{upload.status === "complete" && (
|
||||
<button
|
||||
className="uppercase hover:text-primary transition-colors duration-200"
|
||||
onClick={() => handleCopy(upload.url)}
|
||||
>
|
||||
{copied ? (
|
||||
<span className={cn({ hidden: !copied })}>Copied</span>
|
||||
) : (
|
||||
<span className={cn({ hidden: copied })}>Copy</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressBar className="mt-1" status={upload.status} percentage={upload.progress * 100} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import cn from "classnames";
|
||||
|
||||
import { CircledCheckmarkIcon, CircledErrorIcon, CircledProgressIcon, CircledArrowUpIcon } from "../Icons";
|
||||
|
||||
export default function UploaderItemIcon({ status }) {
|
||||
switch (status) {
|
||||
case "enqueued":
|
||||
case "retrying":
|
||||
case "uploading":
|
||||
return <CircledArrowUpIcon className={cn({ "text-palette-300": status === "enqueued" })} />;
|
||||
case "processing":
|
||||
return <CircledProgressIcon className="animate-[spin_3s_linear_infinite]" />;
|
||||
case "complete":
|
||||
return <CircledCheckmarkIcon />;
|
||||
case "error":
|
||||
return <CircledErrorIcon className="text-error" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { getReasonPhrase } from "http-status-codes";
|
||||
import bytes from "pretty-bytes";
|
||||
|
||||
export default function buildUploadErrorMessage(error) {
|
||||
// The request was made and the server responded with a status code that falls out of the range of 2xx
|
||||
if (error.response) {
|
||||
if (error.response.data.message) {
|
||||
return `Upload failed with error: ${error.response.data.message}`;
|
||||
}
|
||||
|
||||
const statusCode = error.response.status;
|
||||
const statusText = getReasonPhrase(error.response.status);
|
||||
|
||||
return `Upload failed, our server received your request but failed with status code: ${statusCode} ${statusText}`;
|
||||
}
|
||||
|
||||
// The request was made but no response was received. The best we can do is detect whether browser is online.
|
||||
// This will be triggered mostly if the server is offline or misconfigured and doesn't respond to valid request.
|
||||
if (error.request) {
|
||||
if (!navigator.onLine) {
|
||||
return "You are offline, please connect to the internet and try again";
|
||||
}
|
||||
|
||||
// TODO: We should add a note "our team has been notified" and have some kind of notification with this error.
|
||||
return "Server failed to respond to your request, please try again later.";
|
||||
}
|
||||
|
||||
// Match the error message to a message returned by TUS when upload exceeds max file size
|
||||
const matchTusMaxFileSizeError = error.message.match(/upload exceeds maximum size: \d+ > (?<limit>\d+)/);
|
||||
|
||||
if (matchTusMaxFileSizeError) {
|
||||
return `File exceeds size limit of ${bytes(parseInt(matchTusMaxFileSizeError.groups.limit, 10))}`;
|
||||
}
|
||||
|
||||
// TODO: We should add a note "our team has been notified" and have some kind of notification with this error.
|
||||
return `Critical error, please refresh the application and try again. ${error.message}`;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { createContext } from "react";
|
||||
|
||||
export const UserContext = createContext({
|
||||
user: null,
|
||||
error: null,
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { UserContext } from "./UserContext";
|
||||
|
||||
export const UserProvider = ({ children }) => {
|
||||
const { data: user, error, mutate } = useSWR("user");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (user || error) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user, error]);
|
||||
|
||||
return <UserContext.Provider value={{ user, error, loading, mutate }}>{children}</UserContext.Provider>;
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./UserProvider";
|
||||
export * from "./useUser";
|
|
@ -0,0 +1,5 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { UserContext } from "./UserContext";
|
||||
|
||||
export const useUser = () => useContext(UserContext);
|
|
@ -0,0 +1,28 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import freeTier from "../lib/tiers";
|
||||
|
||||
export default function useSubscriptionPlans(user) {
|
||||
const { data: paidPlans, error, mutate } = useSWR("stripe/prices");
|
||||
const [plans, setPlans] = useState([freeTier]);
|
||||
const [activePlan, setActivePlan] = useState(freeTier);
|
||||
|
||||
useEffect(() => {
|
||||
if (paidPlans) {
|
||||
setPlans((plans) => [...plans, ...paidPlans].sort((planA, planB) => planA.tier - planB.tier));
|
||||
}
|
||||
}, [paidPlans]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setActivePlan(plans.find((plan) => plan.tier === user.tier));
|
||||
}
|
||||
}, [plans, user]);
|
||||
|
||||
return {
|
||||
error,
|
||||
mutate,
|
||||
plans,
|
||||
activePlan,
|
||||
};
|
||||
}
|
|
@ -1,55 +1,52 @@
|
|||
import * as React from "react";
|
||||
import { Link } from "gatsby";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { DropdownMenu, DropdownMenuLink } from "../components/DropdownMenu";
|
||||
import { PageContainer } from "../components/PageContainer";
|
||||
import { CogIcon, SkynetLogoIcon, LockClosedIcon } from "../components/Icons";
|
||||
import { NavBar, NavBarLink, NavBarSection } from "../components/Navbar";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
const Layout = styled.div.attrs({
|
||||
className: "h-screen overflow-hidden",
|
||||
import { authenticatedOnly } from "../lib/swrConfig";
|
||||
|
||||
import { PageContainer } from "../components/PageContainer";
|
||||
import { NavBar } from "../components/Navbar";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { UserProvider, useUser } from "../contexts/user";
|
||||
|
||||
const Wrapper = styled.div.attrs({
|
||||
className: "min-h-screen overflow-hidden",
|
||||
})`
|
||||
background-image: url(/images/dashboard-bg.svg);
|
||||
background-position: -300px -280px;
|
||||
|
||||
.navbar {
|
||||
grid-template-columns: auto max-content 1fr;
|
||||
}
|
||||
background-position: center -280px;
|
||||
background-repeat: no-repeat;
|
||||
`;
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const { user } = useUser();
|
||||
|
||||
// Prevent from flashing the dashboard screen to unauthenticated users.
|
||||
return (
|
||||
<Wrapper>
|
||||
{!user && (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
|
||||
<p>Loading...</p> {/* TODO: Do something nicer here */}
|
||||
</div>
|
||||
)}
|
||||
{user && <>{children}</>}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const DashboardLayout = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<SWRConfig value={authenticatedOnly}>
|
||||
<UserProvider>
|
||||
<Layout>
|
||||
<NavBar className="navbar">
|
||||
<NavBarSection className="w-[110px] justify-start">
|
||||
<SkynetLogoIcon size={48} />
|
||||
</NavBarSection>
|
||||
<NavBarSection>
|
||||
<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="justify-end">
|
||||
<DropdownMenu title="My account">
|
||||
<DropdownMenuLink href="/settings" icon={CogIcon} label="Settings" />
|
||||
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
|
||||
</DropdownMenu>
|
||||
</NavBarSection>
|
||||
</NavBar>
|
||||
<NavBar />
|
||||
<PageContainer>
|
||||
<main className="mt-14">{children}</main>
|
||||
</PageContainer>
|
||||
<Footer />
|
||||
</Layout>
|
||||
</UserProvider>
|
||||
</SWRConfig>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { css } from "styled-components";
|
||||
import theme from "./theme";
|
||||
|
||||
export const screen = (breakpoint, style) => {
|
||||
const { screens } = theme;
|
||||
const minWidth = screens[breakpoint];
|
||||
|
||||
if (typeof minWidth === "undefined") {
|
||||
throw ReferenceError(
|
||||
`Screen "${breakpoint}" is not defined in Tailwind config. Available values are: ${Object.keys(screens).join(
|
||||
", "
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
return css`
|
||||
@media (min-width: ${minWidth}) {
|
||||
${style}
|
||||
}
|
||||
`;
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
import { navigate } from "gatsby";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
// TODO: portal-aware URL
|
||||
const baseUrl = process.env.NODE_ENV !== "production" ? "/api" : "https://account.skynetpro.net/api";
|
||||
|
||||
const redirectUnauthenticated = (key) =>
|
||||
fetch(`${baseUrl}/${key}`).then((response) => {
|
||||
if (response.status === StatusCodes.UNAUTHORIZED) {
|
||||
navigate(`/auth/login?return_to=${encodeURIComponent(window.location.href)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
});
|
||||
|
||||
const redirectAuthenticated = (key) =>
|
||||
fetch(`${baseUrl}/${key}`).then((response) => {
|
||||
if (response.status === StatusCodes.OK) {
|
||||
navigate(`/`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
});
|
||||
|
||||
export const authenticatedOnly = {
|
||||
fetcher: redirectUnauthenticated,
|
||||
};
|
||||
|
||||
export const guestsOnly = {
|
||||
fetcher: redirectAuthenticated,
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
// @preval
|
||||
// This file is pre-evaluated on build-time, so the config is only resolved once
|
||||
// and then included in the bundle for us to use in dynamic styles.
|
||||
import resolveConfig from "tailwindcss/resolveConfig";
|
||||
|
||||
import tailwindConfig from "../../tailwind.config.js";
|
||||
|
||||
export default resolveConfig(tailwindConfig).theme;
|
|
@ -0,0 +1,9 @@
|
|||
const freeTier = {
|
||||
id: "starter",
|
||||
tier: 1,
|
||||
name: "Free",
|
||||
price: 0,
|
||||
description: "100 GB - Casual user with a few files you want to access from around the world? Try the Free tier",
|
||||
};
|
||||
|
||||
export default freeTier;
|
|
@ -1,9 +1,67 @@
|
|||
import * as React from "react";
|
||||
import { useMedia } from "react-use";
|
||||
|
||||
import theme from "../lib/theme";
|
||||
import { ArrowRightIcon } from "../components/Icons";
|
||||
import { Panel } from "../components/Panel";
|
||||
import { Tab, TabPanel, Tabs } from "../components/Tabs";
|
||||
import LatestActivity from "../components/LatestActivity/LatestActivity";
|
||||
import DashboardLayout from "../layouts/DashboardLayout";
|
||||
import Slider from "../components/Slider/Slider";
|
||||
import CurrentUsage from "../components/CurrentUsage";
|
||||
import Uploader from "../components/Uploader/Uploader";
|
||||
import CurrentPlan from "../components/CurrentPlan";
|
||||
|
||||
const IndexPage = () => {
|
||||
return <>Dashboard</>;
|
||||
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full">
|
||||
<Slider
|
||||
slides={[
|
||||
<Panel title="Upload" className="h-[330px]">
|
||||
<Tabs variant="fill">
|
||||
<Tab id="files" title="Files" />
|
||||
<Tab id="directory" title="Directory" />
|
||||
<TabPanel tabId="files" className="h-full overflow-y-auto">
|
||||
<Uploader mode="file" />
|
||||
</TabPanel>
|
||||
<TabPanel tabId="directory" className="h-full overflow-y-auto">
|
||||
<Uploader mode="directory" />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Panel>,
|
||||
<Panel
|
||||
title={
|
||||
<>
|
||||
<ArrowRightIcon /> Usage
|
||||
</>
|
||||
}
|
||||
className="h-[330px]"
|
||||
>
|
||||
<CurrentUsage />
|
||||
</Panel>,
|
||||
<Panel
|
||||
title={
|
||||
<>
|
||||
<ArrowRightIcon /> Current plan
|
||||
</>
|
||||
}
|
||||
className="h-[330px]"
|
||||
>
|
||||
<CurrentPlan />
|
||||
</Panel>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{showRecentActivity && (
|
||||
<div className="mt-10">
|
||||
<LatestActivity />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IndexPage.Layout = DashboardLayout;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 378 146" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<pattern id="diagonalHatch" patternUnits="userSpaceOnUse" width="8" height="8">
|
||||
<path d="M-2,2 l6,-6
|
||||
M0,8 l8,-8
|
||||
M7,10 l6,-6" stroke="#d4dddb" />
|
||||
</pattern>
|
||||
<style>.cls-1{fill:#f5f7f7;}.cls-2{fill:#d4dddb; opacity: 0.5;}.cls-3{fill:url(#diagonalHatch);}.cls-4{fill:#80e3af;}</style>
|
||||
</defs>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<rect class="cls-1" width="2" height="146" rx="1"/>
|
||||
<rect class="cls-1" x="4" width="4" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="10" width="6" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="18" width="8" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="28" width="10" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="40" width="12" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="54" width="14" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="70" width="16" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="88" width="18" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="108" width="20" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="130" width="22" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="154" width="24" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="180" width="26" height="146" rx="2"/>
|
||||
<rect class="cls-1" x="208" width="28" height="146" rx="2"/>
|
||||
<rect class="cls-2" x="238" width="30" height="146" rx="2"/>
|
||||
<rect class="cls-2" x="270" width="32" height="146" rx="2"/>
|
||||
<rect class="cls-2" x="304" width="34" height="146" rx="2"/>
|
||||
<rect class="cls-3" x="340" width="38" height="146" rx="2"/>
|
||||
<path class="cls-4" d="M304,144h34a0,0,0,0,1,0,0v0a2,2,0,0,1-2,2H306a2,2,0,0,1-2-2v0A0,0,0,0,1,304,144Z"/>
|
||||
<path class="cls-4" d="M270,144h32a0,0,0,0,1,0,0v0a2,2,0,0,1-2,2H272a2,2,0,0,1-2-2v0A0,0,0,0,1,270,144Z"/>
|
||||
<path class="cls-4" d="M238,144h30a0,0,0,0,1,0,0v0a2,2,0,0,1-2,2H240a2,2,0,0,1-2-2v0A0,0,0,0,1,238,144Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -338,7 +338,7 @@
|
|||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.15.5", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.7.0":
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.15.5", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.7.0":
|
||||
version "7.17.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0"
|
||||
integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==
|
||||
|
@ -1107,7 +1107,7 @@
|
|||
core-js-pure "^3.20.2"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.17.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
|
||||
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
|
||||
|
@ -1139,7 +1139,7 @@
|
|||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.15.4", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.2.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.15.4", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
|
||||
version "7.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
|
||||
integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
|
||||
|
@ -3156,6 +3156,39 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc"
|
||||
integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==
|
||||
|
||||
"@types/babel__core@^7.1.12":
|
||||
version "7.1.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8"
|
||||
integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.1.0"
|
||||
"@babel/types" "^7.0.0"
|
||||
"@types/babel__generator" "*"
|
||||
"@types/babel__template" "*"
|
||||
"@types/babel__traverse" "*"
|
||||
|
||||
"@types/babel__generator@*":
|
||||
version "7.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7"
|
||||
integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==
|
||||
dependencies:
|
||||
"@babel/types" "^7.0.0"
|
||||
|
||||
"@types/babel__template@*":
|
||||
version "7.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969"
|
||||
integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.1.0"
|
||||
"@babel/types" "^7.0.0"
|
||||
|
||||
"@types/babel__traverse@*":
|
||||
version "7.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43"
|
||||
integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/cacheable-request@^6.0.1":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
|
||||
|
@ -3287,7 +3320,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
||||
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
|
||||
|
||||
"@types/http-proxy@^1.17.7":
|
||||
"@types/http-proxy@^1.17.5", "@types/http-proxy@^1.17.7":
|
||||
version "1.17.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.8.tgz#968c66903e7e42b483608030ee85800f22d03f55"
|
||||
integrity sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==
|
||||
|
@ -4485,6 +4518,11 @@ atob@^2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
attr-accept@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
|
||||
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
|
||||
|
||||
autoprefixer@^10.4.0, autoprefixer@^10.4.2:
|
||||
version "10.4.2"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b"
|
||||
|
@ -4515,7 +4553,7 @@ axe-core@^4.3.5:
|
|||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
|
||||
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
|
||||
|
||||
axios@^0.21.1:
|
||||
axios@^0.21.0, axios@^0.21.1:
|
||||
version "0.21.4"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
||||
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
|
||||
|
@ -4679,6 +4717,16 @@ babel-plugin-polyfill-regenerator@^0.3.0:
|
|||
dependencies:
|
||||
"@babel/helper-define-polyfill-provider" "^0.3.1"
|
||||
|
||||
babel-plugin-preval@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-preval/-/babel-plugin-preval-5.1.0.tgz#6efb89bf6b97af592cd1400c6df49c0e9e6ab027"
|
||||
integrity sha512-G5R+xmo5LS41A4UyZjOjV0mp9AvkuCyUOAJ6TOv/jTZS+VKh7L7HUDRcCSOb0YCM/u0fFarh7Diz0wjY8rFNFg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/babel__core" "^7.1.12"
|
||||
babel-plugin-macros "^3.0.1"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
babel-plugin-react-docgen@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.2.1.tgz#7cc8e2f94e8dc057a06e953162f0810e4e72257b"
|
||||
|
@ -4774,6 +4822,13 @@ balanced-match@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base32-encode@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/base32-encode/-/base32-encode-1.2.0.tgz#e150573a5e431af0a998e32bdfde7045725ca453"
|
||||
integrity sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==
|
||||
dependencies:
|
||||
to-data-view "^1.1.0"
|
||||
|
||||
base64-arraybuffer@0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
|
||||
|
@ -4845,6 +4900,11 @@ bl@^4.0.0, bl@^4.0.3:
|
|||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
blakejs@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.1.tgz#bf313053978b2cd4c444a48795710be05c785702"
|
||||
integrity sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==
|
||||
|
||||
bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.2:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
|
@ -5071,6 +5131,14 @@ buffer@^5.2.0, buffer@^5.5.0, buffer@^5.7.0:
|
|||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
buffer@^6.0.1:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
builtin-status-codes@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||
|
@ -5412,6 +5480,11 @@ class-utils@^0.3.5:
|
|||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classnames@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||
|
||||
clean-css@^4.2.3:
|
||||
version "4.2.4"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
|
||||
|
@ -5777,6 +5850,11 @@ copy-descriptor@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copy-text-to-clipboard@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz#8cbf8f90e0a47f12e4a24743736265d157bce69c"
|
||||
integrity sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==
|
||||
|
||||
copy-to-clipboard@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
||||
|
@ -6218,6 +6296,11 @@ date-fns@^2.25.0:
|
|||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
||||
|
||||
dayjs@^1.10.8:
|
||||
version "1.10.8"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
||||
integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
|
||||
|
||||
debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
|
@ -7632,6 +7715,13 @@ file-loader@^6.2.0:
|
|||
loader-utils "^2.0.0"
|
||||
schema-utils "^3.0.0"
|
||||
|
||||
file-selector@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17"
|
||||
integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
file-system-cache@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"
|
||||
|
@ -8159,6 +8249,11 @@ gatsby-plugin-postcss@^5.7.0:
|
|||
"@babel/runtime" "^7.15.4"
|
||||
postcss-loader "^4.3.0"
|
||||
|
||||
gatsby-plugin-preval@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-preval/-/gatsby-plugin-preval-1.0.0.tgz#b0e9dcc9ef568cb6ca998f7211b5365824b97201"
|
||||
integrity sha512-HpPp2bdA4nZsuD3R++GRhM9BPlFp8+ilkXIo53hNr14OlHXxrgimh9lqVVvP3q4JriHh+bYcvmfqm6msIsxxLg==
|
||||
|
||||
gatsby-plugin-provide-react@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-provide-react/-/gatsby-plugin-provide-react-1.0.2.tgz#e50bb311cd8ef5855c6d94f708266ab117c77a15"
|
||||
|
@ -8196,10 +8291,10 @@ gatsby-plugin-sharp@^4.6.0:
|
|||
svgo "1.3.2"
|
||||
uuid "3.4.0"
|
||||
|
||||
gatsby-plugin-styled-components@^5.7.0:
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-styled-components/-/gatsby-plugin-styled-components-5.7.0.tgz#8ba7b4ddb1722dcd0efd4fc6f1a8e62f47be012c"
|
||||
integrity sha512-mX8N4nqIX0Ow/pUSORUb8WlKvgX7foCoWZ0AifyBOFnhBCbRWYTsXFWwiea6jCnST5V61b2TOFpjIHcvHvc9aQ==
|
||||
gatsby-plugin-styled-components@^5.8.0:
|
||||
version "5.8.0"
|
||||
resolved "https://registry.yarnpkg.com/gatsby-plugin-styled-components/-/gatsby-plugin-styled-components-5.8.0.tgz#5d8c81802ed9266435aa1145451bfb3ac582ad37"
|
||||
integrity sha512-4ma9PgOr3U5TUX6uwAqFW+VX+fDxmt1y4oM3ArfZufaiQvZJ52cuf/uiyI+Tx1DJebcypEpR5dXYVl9ZX1bUHg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
|
||||
|
@ -9196,6 +9291,17 @@ http-errors@1.8.1:
|
|||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-proxy-middleware@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz#43700d6d9eecb7419bf086a128d0f7205d9eb665"
|
||||
integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==
|
||||
dependencies:
|
||||
"@types/http-proxy" "^1.17.5"
|
||||
http-proxy "^1.18.1"
|
||||
is-glob "^4.0.1"
|
||||
is-plain-obj "^3.0.0"
|
||||
micromatch "^4.0.2"
|
||||
|
||||
http-proxy@^1.18.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
|
@ -9205,6 +9311,11 @@ http-proxy@^1.18.1:
|
|||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-status-codes@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be"
|
||||
integrity sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==
|
||||
|
||||
http2-wrapper@^1.0.0-beta.5.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
|
||||
|
@ -9718,6 +9829,11 @@ is-plain-obj@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
|
||||
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
|
||||
|
||||
is-plain-obj@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
|
||||
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
|
||||
|
@ -11067,7 +11183,7 @@ nano-css@^5.3.1:
|
|||
stacktrace-js "^2.0.2"
|
||||
stylis "^4.0.6"
|
||||
|
||||
nanoid@^3.1.23, nanoid@^3.2.0:
|
||||
nanoid@^3.1.23, nanoid@^3.2.0, nanoid@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
|
||||
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
|
||||
|
@ -12402,6 +12518,11 @@ pretty-bytes@^5.4.1:
|
|||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||
|
||||
pretty-bytes@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.0.0.tgz#928be2ad1f51a2e336add8ba764739f9776a8140"
|
||||
integrity sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==
|
||||
|
||||
pretty-error@^2.1.1, pretty-error@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"
|
||||
|
@ -12508,7 +12629,7 @@ prompts@^2.4.0, prompts@^2.4.2:
|
|||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.7.2:
|
||||
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
|
@ -12657,6 +12778,11 @@ querystring@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd"
|
||||
integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==
|
||||
|
||||
querystringify@^2.1.1:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
|
||||
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
|
@ -12793,6 +12919,15 @@ react-draggable@^4.4.3:
|
|||
clsx "^1.1.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-dropzone@^12.0.4:
|
||||
version "12.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-12.0.4.tgz#b88eeaa2c7118f7fd042404682b17a1d466f2fcf"
|
||||
integrity sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==
|
||||
dependencies:
|
||||
attr-accept "^2.2.2"
|
||||
file-selector "^0.4.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-element-to-jsx-string@^14.3.4:
|
||||
version "14.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8"
|
||||
|
@ -13769,6 +13904,30 @@ sisteransi@^1.0.5:
|
|||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
||||
|
||||
sjcl@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a"
|
||||
integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==
|
||||
|
||||
skynet-js@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-3.0.2.tgz#d08a33066ee85b86e4ffc7c31591239a88da6fbe"
|
||||
integrity sha512-rbmpOGbDwg2FcsZ7HkmGhVaUwWO6kaysRFKTBC3yGiV+b6fbnpPPNCskvh8kWwbTsj+koWkSRUFYqG7cc+eTuA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
axios "^0.21.0"
|
||||
base32-encode "^1.1.1"
|
||||
base64-js "^1.3.1"
|
||||
blakejs "^1.1.0"
|
||||
buffer "^6.0.1"
|
||||
mime "^2.5.2"
|
||||
path-browserify "^1.0.1"
|
||||
randombytes "^2.1.0"
|
||||
sjcl "^1.0.8"
|
||||
tweetnacl "^1.0.3"
|
||||
url-join "^4.0.1"
|
||||
url-parse "^1.4.7"
|
||||
|
||||
slash@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
|
||||
|
@ -14061,6 +14220,11 @@ store2@^2.12.0:
|
|||
resolved "https://registry.yarnpkg.com/store2/-/store2-2.13.1.tgz#fae7b5bb9d35fc53dc61cd262df3abb2f6e59022"
|
||||
integrity sha512-iJtHSGmNgAUx0b/MCS6ASGxb//hGrHHRgzvN+K5bvkBTN7A9RTpPSf1WSp+nPGvWCJ1jRnvY7MKnuqfoi3OEqg==
|
||||
|
||||
storybook-addon-gatsby@^0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/storybook-addon-gatsby/-/storybook-addon-gatsby-0.0.5.tgz#94f5b67bab8659d0248b65e60dabc3702818ce8b"
|
||||
integrity sha512-18f8Kc6mx8mEFfqY2DgF9ayDfmM58+9IjJqIxGV4bA4r2EtB/Q1LDNELIJmpLLyA5NrSvECxCqzLu7jNBlWgmA==
|
||||
|
||||
stream-browserify@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
|
||||
|
@ -14417,6 +14581,11 @@ svgo@^2.7.0:
|
|||
picocolors "^1.0.0"
|
||||
stable "^0.1.8"
|
||||
|
||||
swr@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/swr/-/swr-1.2.2.tgz#6cae09928d30593a7980d80f85823e57468fac5d"
|
||||
integrity sha512-ky0BskS/V47GpW8d6RU7CPsr6J8cr7mQD6+do5eky3bM0IyJaoi3vO8UhvrzJaObuTlGhPl2szodeB2dUd76Xw==
|
||||
|
||||
symbol-observable@^1.0.4:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
|
@ -14690,6 +14859,11 @@ to-arraybuffer@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
|
||||
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
|
||||
|
||||
to-data-view@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/to-data-view/-/to-data-view-1.1.0.tgz#08d6492b0b8deb9b29bdf1f61c23eadfa8994d00"
|
||||
integrity sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==
|
||||
|
||||
to-fast-properties@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
||||
|
@ -14868,6 +15042,11 @@ tunnel-agent@^0.6.0:
|
|||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tweetnacl@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
|
||||
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
|
@ -15161,6 +15340,11 @@ urix@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
|
||||
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
|
||||
|
||||
url-join@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
|
||||
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
|
||||
|
||||
url-loader@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2"
|
||||
|
@ -15177,6 +15361,14 @@ url-parse-lax@^3.0.0:
|
|||
dependencies:
|
||||
prepend-http "^2.0.0"
|
||||
|
||||
url-parse@^1.4.7:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
|
||||
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
|
||||
dependencies:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
url@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"@stripe/stripe-js": "1.23.0",
|
||||
"classnames": "2.3.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"dayjs": "1.10.7",
|
||||
"dayjs": "1.10.8",
|
||||
"express-jwt": "6.1.1",
|
||||
"fast-levenshtein": "3.0.0",
|
||||
"formik": "2.2.9",
|
||||
|
|
|
@ -619,10 +619,10 @@ damerau-levenshtein@^1.0.7:
|
|||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d"
|
||||
integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==
|
||||
|
||||
dayjs@1.10.7:
|
||||
version "1.10.7"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
||||
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
||||
dayjs@1.10.8:
|
||||
version "1.10.8"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
||||
integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
|
||||
|
||||
debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
|
|
|
@ -12,30 +12,30 @@
|
|||
"classnames": "2.3.1",
|
||||
"copy-text-to-clipboard": "3.0.1",
|
||||
"crypto-browserify": "3.12.0",
|
||||
"framer-motion": "6.2.6",
|
||||
"gatsby": "4.8.1",
|
||||
"framer-motion": "6.2.8",
|
||||
"gatsby": "4.9.0",
|
||||
"gatsby-background-image": "1.6.0",
|
||||
"gatsby-plugin-image": "2.7.0",
|
||||
"gatsby-plugin-manifest": "4.7.0",
|
||||
"gatsby-plugin-image": "2.9.0",
|
||||
"gatsby-plugin-manifest": "4.9.0",
|
||||
"gatsby-plugin-matomo": "0.11.0",
|
||||
"gatsby-plugin-offline": "5.7.0",
|
||||
"gatsby-plugin-postcss": "5.8.0",
|
||||
"gatsby-plugin-purgecss": "6.1.0",
|
||||
"gatsby-plugin-react-helmet": "5.7.0",
|
||||
"gatsby-plugin-offline": "5.9.0",
|
||||
"gatsby-plugin-postcss": "5.9.0",
|
||||
"gatsby-plugin-purgecss": "6.1.1",
|
||||
"gatsby-plugin-react-helmet": "5.9.0",
|
||||
"gatsby-plugin-robots-txt": "1.7.0",
|
||||
"gatsby-plugin-sharp": "4.8.1",
|
||||
"gatsby-plugin-sitemap": "5.8.0",
|
||||
"gatsby-plugin-sharp": "4.9.0",
|
||||
"gatsby-plugin-sitemap": "5.9.0",
|
||||
"gatsby-plugin-svgr": "3.0.0-beta.0",
|
||||
"gatsby-remark-classes": "1.0.2",
|
||||
"gatsby-remark-copy-linked-files": "5.7.0",
|
||||
"gatsby-remark-images": "6.7.0",
|
||||
"gatsby-remark-prismjs": "6.7.0",
|
||||
"gatsby-remark-responsive-iframe": "5.8.0",
|
||||
"gatsby-remark-smartypants": "5.7.0",
|
||||
"gatsby-source-filesystem": "4.7.0",
|
||||
"gatsby-transformer-remark": "5.8.1",
|
||||
"gatsby-transformer-sharp": "4.8.0",
|
||||
"gatsby-transformer-yaml": "4.8.0",
|
||||
"gatsby-remark-copy-linked-files": "5.9.0",
|
||||
"gatsby-remark-images": "6.9.0",
|
||||
"gatsby-remark-prismjs": "6.9.0",
|
||||
"gatsby-remark-responsive-iframe": "5.9.0",
|
||||
"gatsby-remark-smartypants": "5.9.0",
|
||||
"gatsby-source-filesystem": "4.9.0",
|
||||
"gatsby-transformer-remark": "5.9.0",
|
||||
"gatsby-transformer-sharp": "4.9.0",
|
||||
"gatsby-transformer-yaml": "4.9.0",
|
||||
"gbimage-bridge": "0.2.1",
|
||||
"http-status-codes": "2.2.0",
|
||||
"jsonp": "0.2.1",
|
||||
|
@ -64,7 +64,7 @@
|
|||
"@tailwindcss/typography": "0.5.2",
|
||||
"autoprefixer": "10.4.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "9.5.0",
|
||||
"cypress": "9.5.1",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"prettier": "2.5.1",
|
||||
"tailwindcss": "3.0.23"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue