diff --git a/.github/workflows/deploy-dashboard-v2-storybook.yml b/.github/workflows/deploy-dashboard-v2-storybook.yml new file mode 100644 index 00000000..b4591b59 --- /dev/null +++ b/.github/workflows/deploy-dashboard-v2-storybook.yml @@ -0,0 +1,31 @@ +name: Build Storybook - packages/dashboard-v2 + +on: + push: + branches: + - master + paths: + - "packages/dashboard-v2/**" + pull_request: + paths: + - "packages/dashboard-v2/**" + +defaults: + run: + working-directory: packages/dashboard-v2 + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 16.x + - run: yarn install + - run: yarn build-storybook + - name: "Deploy to Skynet" + uses: skynetlabs/deploy-to-skynet-action@v2 + with: + upload-dir: packages/dashboard-v2/storybook-build + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-packages-dashboard-v2.yml b/.github/workflows/lint-packages-dashboard-v2.yml new file mode 100644 index 00000000..a8577562 --- /dev/null +++ b/.github/workflows/lint-packages-dashboard-v2.yml @@ -0,0 +1,24 @@ +name: Lint - packages/dashboard-v2 + +on: + pull_request: + paths: + - packages/dashboard-v2/** + +defaults: + run: + working-directory: packages/dashboard-v2 + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 16.x + + - run: yarn + - run: yarn prettier --check + - run: yarn lint diff --git a/.github/workflows/test-packages-health-check.yml b/.github/workflows/test-packages-health-check.yml new file mode 100644 index 00000000..79cfe324 --- /dev/null +++ b/.github/workflows/test-packages-health-check.yml @@ -0,0 +1,23 @@ +name: Test - packages/health-check + +on: + pull_request: + paths: + - packages/health-check/** + +defaults: + run: + working-directory: packages/health-check + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 16.x + + - run: yarn + - run: yarn jest diff --git a/changelog/items/bugs-fixed/escape-uri-on-subdomain-skylink-requests.md b/changelog/items/bugs-fixed/escape-uri-on-subdomain-skylink-requests.md new file mode 100644 index 00000000..3beabc7e --- /dev/null +++ b/changelog/items/bugs-fixed/escape-uri-on-subdomain-skylink-requests.md @@ -0,0 +1 @@ +- fixed a bug when accessing file from skylink via subdomain with a filename that had escaped characters diff --git a/docker-compose.abuse.yml b/docker-compose.abuse.yml index b7d0d022..e3f32750 100644 --- a/docker-compose.abuse.yml +++ b/docker-compose.abuse.yml @@ -23,7 +23,6 @@ services: - ABUSE_SPONSOR=${ABUSE_SPONSOR} - BLOCKER_HOST=10.10.10.110 - BLOCKER_PORT=4000 - - BLOCKER_AUTH_HEADER=${BLOCKER_AUTH_HEADER} - EMAIL_SERVER=${EMAIL_SERVER} - EMAIL_USERNAME=${EMAIL_USERNAME} - EMAIL_PASSWORD=${EMAIL_PASSWORD} diff --git a/docker-compose.accounts.yml b/docker-compose.accounts.yml index e8159040..a3941f6b 100644 --- a/docker-compose.accounts.yml +++ b/docker-compose.accounts.yml @@ -66,8 +66,7 @@ services: env_file: - .env environment: - - NEXT_PUBLIC_SKYNET_PORTAL_API=${SKYNET_PORTAL_API} - - NEXT_PUBLIC_SKYNET_DASHBOARD_URL=${SKYNET_DASHBOARD_URL} + - NEXT_PUBLIC_PORTAL_DOMAIN=${PORTAL_DOMAIN} - NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY} volumes: - ./docker/data/dashboard/.next:/usr/app/.next diff --git a/docker-compose.jaeger.yml b/docker-compose.jaeger.yml index 297d466a..0e23c251 100644 --- a/docker-compose.jaeger.yml +++ b/docker-compose.jaeger.yml @@ -10,7 +10,7 @@ services: sia: environment: - JAEGER_DISABLED=${JAEGER_DISABLED:-false} # Enable/Disable tracing - - JAEGER_SERVICE_NAME=${PORTAL_NAME:-Skyd} # change to e.g. eu-ger-1 + - JAEGER_SERVICE_NAME=${SERVER_DOMAIN:-Skyd} # change to e.g. eu-ger-1 # Configuration # See https://github.com/jaegertracing/jaeger-client-go#environment-variables # for all options. diff --git a/docker-compose.yml b/docker-compose.yml index e63df946..f511eec5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,6 @@ services: - SKYD_DISK_CACHE_SIZE=${SKYD_DISK_CACHE_SIZE:-53690000000} # 50GB - SKYD_DISK_CACHE_MIN_HITS=${SKYD_DISK_CACHE_MIN_HITS:-3} - SKYD_DISK_CACHE_HIT_PERIOD=${SKYD_DISK_CACHE_HIT_PERIOD:-3600} # 1h - env_file: - .env volumes: @@ -65,6 +64,8 @@ services: logging: *default-logging env_file: - .env + environment: + - SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-false} volumes: - ./docker/data/nginx/cache:/data/nginx/cache - ./docker/data/nginx/blocker:/data/nginx/blocker diff --git a/docker/handshake/Dockerfile b/docker/handshake/Dockerfile index 7a1fd0eb..fba07334 100644 --- a/docker/handshake/Dockerfile +++ b/docker/handshake/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16.13.2-alpine +FROM node:16.14.0-alpine WORKDIR /opt/hsd diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile index 43c4b7f9..c8ef7baf 100644 --- a/docker/nginx/Dockerfile +++ b/docker/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM openresty/openresty:1.19.9.1-bionic +FROM openresty/openresty:1.19.9.1-focal RUN luarocks install lua-resty-http && \ luarocks install hasher && \ diff --git a/docker/nginx/conf.d/include/cors-headers b/docker/nginx/conf.d/include/cors-headers index 58369b65..0f0bb328 100644 --- a/docker/nginx/conf.d/include/cors-headers +++ b/docker/nginx/conf.d/include/cors-headers @@ -1,5 +1,5 @@ more_set_headers 'Access-Control-Allow-Origin: $http_origin'; more_set_headers 'Access-Control-Allow-Credentials: true'; more_set_headers 'Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE'; -more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,If-None-Match,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location'; +more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,If-None-Match,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location,Skynet-API-Key'; more_set_headers 'Access-Control-Expose-Headers: Content-Length,Content-Range,ETag,Skynet-File-Metadata,Skynet-Skylink,Skynet-Proof,Skynet-Portal-Api,Skynet-Server-Api,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location'; diff --git a/docker/nginx/conf.d/include/location-hns b/docker/nginx/conf.d/include/location-hns index 22e50317..b0c7322d 100644 --- a/docker/nginx/conf.d/include/location-hns +++ b/docker/nginx/conf.d/include/location-hns @@ -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"] = os.getenv("SKYNET_PORTAL_API") - ngx.header["Skynet-Server-Api"] = os.getenv("SKYNET_SERVER_API") + ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN") + ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN") if ngx.header.location then -- match location redirect part after the skylink diff --git a/docker/nginx/conf.d/include/location-skylink b/docker/nginx/conf.d/include/location-skylink index b0d2066e..cf250cea 100644 --- a/docker/nginx/conf.d/include/location-skylink +++ b/docker/nginx/conf.d/include/location-skylink @@ -82,8 +82,8 @@ access_by_lua_block { } header_filter_by_lua_block { - ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API") - ngx.header["Skynet-Server-Api"] = os.getenv("SKYNET_SERVER_API") + ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN") + ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN") -- not empty skynet_proof means this is a skylink v2 request -- so we should replace the Skynet-Proof header with the one diff --git a/docker/nginx/conf.d/include/proxy-cache-downloads b/docker/nginx/conf.d/include/proxy-cache-downloads index 8481ebb9..85aeeb9e 100644 --- a/docker/nginx/conf.d/include/proxy-cache-downloads +++ b/docker/nginx/conf.d/include/proxy-cache-downloads @@ -4,11 +4,18 @@ proxy_cache_min_uses 3; # cache after 3 uses proxy_cache_valid 200 206 307 308 48h; # keep 200, 206, 307 and 308 responses valid for up to 2 days add_header X-Proxy-Cache $upstream_cache_status; # add response header to indicate cache hits and misses +# map skyd env variable value to "1" for true and "0" for false (expected by proxy_no_cache) +set_by_lua_block $skyd_disk_cache_enabled { + return os.getenv("SKYD_DISK_CACHE_ENABLED") == "true" and "1" or "0" +} + # bypass - this will bypass cache hit on request (status BYPASS) # but still stores file in cache if cache conditions are met -proxy_cache_bypass $cookie_nocache $arg_nocache; +proxy_cache_bypass $cookie_nocache $arg_nocache $skyd_disk_cache_enabled; # no cache - this will ignore cache on request (status MISS) # and does not store file in cache under no condition set_if_empty $nocache "0"; -proxy_no_cache $nocache; + +# disable cache when nocache is set or skyd cache is enabled +proxy_no_cache $nocache $skyd_disk_cache_enabled; diff --git a/docker/nginx/conf.d/server/server.api b/docker/nginx/conf.d/server/server.api index bcdd3705..e8fc0743 100644 --- a/docker/nginx/conf.d/server/server.api +++ b/docker/nginx/conf.d/server/server.api @@ -248,8 +248,8 @@ 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 $SKYNET_SERVER_API 'return os.getenv("SKYNET_SERVER_API")'; - proxy_redirect $scheme://$host $SKYNET_SERVER_API; + set_by_lua_block $server_domain { return os.getenv("SERVER_DOMAIN") } + proxy_redirect $scheme://$host $scheme://$server_domain; # proxy /skynet/tus requests to siad endpoint with all arguments proxy_pass http://sia:9980; @@ -276,8 +276,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"] = os.getenv("SKYNET_PORTAL_API") - ngx.header["Skynet-Server-Api"] = os.getenv("SKYNET_SERVER_API") + ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN") + ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN") if ngx.header["Upload-Metadata"] then local encodedSkylink = string.match(ngx.header["Upload-Metadata"], "Skylink ([^,?]+)") @@ -311,8 +311,8 @@ location /skynet/metadata { include /etc/nginx/conf.d/include/portal-access-check; header_filter_by_lua_block { - ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API") - ngx.header["Skynet-Server-Api"] = os.getenv("SKYNET_SERVER_API") + ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN") + ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN") } proxy_set_header User-Agent: Sia-Agent; @@ -324,8 +324,8 @@ location /skynet/resolve { include /etc/nginx/conf.d/include/portal-access-check; header_filter_by_lua_block { - ngx.header["Skynet-Portal-Api"] = os.getenv("SKYNET_PORTAL_API") - ngx.header["Skynet-Server-Api"] = os.getenv("SKYNET_SERVER_API") + ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN") + ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN") } proxy_set_header User-Agent: Sia-Agent; @@ -348,6 +348,43 @@ location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" { include /etc/nginx/conf.d/include/location-skylink; } +location /skynet/trustless/basesector { + include /etc/nginx/conf.d/include/cors; + include /etc/nginx/conf.d/include/proxy-buffer; + include /etc/nginx/conf.d/include/track-download; + + limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time + + # default download rate to unlimited + set $limit_rate 0; + + access_by_lua_block { + if require("skynet.account").accounts_enabled() then + -- check if portal is in authenticated only mode + if require("skynet.account").is_access_unauthorized() then + return require("skynet.account").exit_access_unauthorized() + end + + -- check if portal is in subscription only mode + if require("skynet.account").is_access_forbidden() then + return require("skynet.account").exit_access_forbidden() + end + + -- get account limits of currently authenticated user + local limits = require("skynet.account").get_account_limits() + + -- apply download speed limit + ngx.var.limit_rate = limits.download + end + } + + limit_rate_after 512k; + limit_rate $limit_rate; + + proxy_set_header User-Agent: Sia-Agent; + proxy_pass http://sia:9980; +} + location /__internal/do/not/use/accounts { include /etc/nginx/conf.d/include/cors; diff --git a/docker/nginx/conf.d/server/server.skylink b/docker/nginx/conf.d/server/server.skylink index 14c0870e..a8f659f1 100644 --- a/docker/nginx/conf.d/server/server.skylink +++ b/docker/nginx/conf.d/server/server.skylink @@ -6,7 +6,12 @@ include /etc/nginx/conf.d/include/init-optional-variables; location / { set_by_lua_block $skylink { return string.match(ngx.var.host, "%w+") } - set $path $uri; + set_by_lua_block $path { + -- strip ngx.var.request_uri from query params - this is basically the same as ngx.var.uri but + -- do not use ngx.var.uri because it will already be unescaped and we need to use escaped path + -- examples: escaped uri "/b%20r56+7" and unescaped uri "/b r56 7" + return string.gsub(ngx.var.request_uri, "?.*", "") + } include /etc/nginx/conf.d/include/location-skylink; } diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index eb5494c9..64397630 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -26,11 +26,12 @@ worker_processes auto; #pid logs/nginx.pid; # declare env variables to use it in config -env SKYNET_PORTAL_API; -env SKYNET_SERVER_API; +env PORTAL_DOMAIN; +env SERVER_DOMAIN; env PORTAL_MODULES; env ACCOUNTS_LIMIT_ACCESS; env SIA_API_PASSWORD; +env SKYD_DISK_CACHE_ENABLED; events { worker_connections 8192; @@ -49,7 +50,7 @@ http { '"$upstream_http_content_type" "$upstream_cache_status" ' '"$server_alias" "$sent_http_skynet_skylink" ' '$upstream_connect_time $upstream_header_time ' - '$request_time "$hns_domain" "$skylink"'; + '$request_time "$hns_domain" "$skylink" $upstream_http_skynet_cache_ratio'; access_log logs/access.log main; @@ -94,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"] = os.getenv("SKYNET_PORTAL_API") - ngx.header["Skynet-Server-Api"] = os.getenv("SKYNET_SERVER_API") + ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN") + ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN") } # ratelimit specified IPs diff --git a/packages/dashboard-v2/.eslintignore b/packages/dashboard-v2/.eslintignore new file mode 100644 index 00000000..65ea287b --- /dev/null +++ b/packages/dashboard-v2/.eslintignore @@ -0,0 +1,4 @@ +node_modules/ +.cache/ +public/ +storybook-build/ diff --git a/packages/dashboard-v2/.eslintrc.js b/packages/dashboard-v2/.eslintrc.js new file mode 100644 index 00000000..51d8f9b5 --- /dev/null +++ b/packages/dashboard-v2/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + globals: { + __PATH_PREFIX__: true, + }, + extends: ["react-app", "plugin:storybook/recommended"], +}; diff --git a/packages/dashboard-v2/.gitignore b/packages/dashboard-v2/.gitignore new file mode 100644 index 00000000..65ea287b --- /dev/null +++ b/packages/dashboard-v2/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.cache/ +public/ +storybook-build/ diff --git a/packages/dashboard-v2/.prettierignore b/packages/dashboard-v2/.prettierignore new file mode 100644 index 00000000..65ea287b --- /dev/null +++ b/packages/dashboard-v2/.prettierignore @@ -0,0 +1,4 @@ +node_modules/ +.cache/ +public/ +storybook-build/ diff --git a/packages/dashboard-v2/.prettierrc.json b/packages/dashboard-v2/.prettierrc.json new file mode 100644 index 00000000..963354f2 --- /dev/null +++ b/packages/dashboard-v2/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} diff --git a/packages/dashboard-v2/.storybook/main.js b/packages/dashboard-v2/.storybook/main.js new file mode 100644 index 00000000..09e2ce48 --- /dev/null +++ b/packages/dashboard-v2/.storybook/main.js @@ -0,0 +1,18 @@ +module.exports = { + stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + { + name: "@storybook/addon-postcss", + options: { + postcssLoaderOptions: { + implementation: require("postcss"), + }, + }, + }, + ], + core: { + builder: "webpack5", + }, +}; diff --git a/packages/dashboard-v2/.storybook/preview.js b/packages/dashboard-v2/.storybook/preview.js new file mode 100644 index 00000000..de9fb8cb --- /dev/null +++ b/packages/dashboard-v2/.storybook/preview.js @@ -0,0 +1,20 @@ +import "tailwindcss/tailwind.css"; +import "@fontsource/sora/300.css"; // light +import "@fontsource/sora/400.css"; // normal +import "@fontsource/sora/500.css"; // medium +import "@fontsource/sora/600.css"; // semibold +import "@fontsource/source-sans-pro/400.css"; // normal +import "@fontsource/source-sans-pro/600.css"; // semibold + +import "../src/styles/global.css"; + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + layout: "fullscreen", +}; diff --git a/packages/dashboard-v2/README.md b/packages/dashboard-v2/README.md new file mode 100644 index 00000000..ba9db568 --- /dev/null +++ b/packages/dashboard-v2/README.md @@ -0,0 +1,10 @@ +# Skynet Account Dashboard + +Code behind [account.skynetpro.net](https://account.skynetpro.net/) + +## Development + +This is a Gatsby application. To run it locally, all you need is: + +- `yarn install` +- `yarn start` diff --git a/packages/dashboard-v2/gatsby-browser.js b/packages/dashboard-v2/gatsby-browser.js new file mode 100644 index 00000000..a71e49c3 --- /dev/null +++ b/packages/dashboard-v2/gatsby-browser.js @@ -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 {element}; +} diff --git a/packages/dashboard-v2/gatsby-config.js b/packages/dashboard-v2/gatsby-config.js new file mode 100644 index 00000000..b82a34b1 --- /dev/null +++ b/packages/dashboard-v2/gatsby-config.js @@ -0,0 +1,22 @@ +module.exports = { + siteMetadata: { + title: `Accounts Dashboard`, + siteUrl: `https://www.yourdomain.tld`, + }, + plugins: [ + "gatsby-plugin-image", + "gatsby-plugin-provide-react", + "gatsby-plugin-react-helmet", + "gatsby-plugin-sharp", + "gatsby-transformer-sharp", + "gatsby-plugin-postcss", + { + resolve: "gatsby-source-filesystem", + options: { + name: "images", + path: "./src/images/", + }, + __key: "images", + }, + ], +}; diff --git a/packages/dashboard-v2/package.json b/packages/dashboard-v2/package.json new file mode 100644 index 00000000..c5bb6214 --- /dev/null +++ b/packages/dashboard-v2/package.json @@ -0,0 +1,63 @@ +{ + "name": "accounts-dashboard", + "version": "1.0.0", + "private": true, + "description": "Accounts Dashboard", + "author": "Skynet Labs", + "keywords": [ + "gatsby" + ], + "scripts": { + "develop": "gatsby develop", + "start": "gatsby develop", + "build": "gatsby build", + "serve": "gatsby serve", + "clean": "gatsby clean", + "lint": "eslint .", + "prettier": "prettier .", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook -o storybook-build" + }, + "dependencies": { + "@fontsource/sora": "^4.5.3", + "@fontsource/source-sans-pro": "^4.5.3", + "gatsby": "^4.6.2", + "gatsby-plugin-postcss": "^5.7.0", + "postcss": "^8.4.6", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-helmet": "^6.1.0", + "react-use": "^17.3.2", + "tailwindcss": "^3.0.23" + }, + "devDependencies": { + "@babel/core": "^7.17.4", + "@storybook/addon-actions": "^6.4.19", + "@storybook/addon-essentials": "^6.4.19", + "@storybook/addon-interactions": "^6.4.19", + "@storybook/addon-links": "^6.4.19", + "@storybook/addon-postcss": "^2.0.0", + "@storybook/builder-webpack5": "^6.4.19", + "@storybook/manager-webpack5": "^6.4.19", + "@storybook/react": "^6.4.19", + "@storybook/testing-library": "^0.0.9", + "autoprefixer": "^10.4.2", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.2.3", + "babel-plugin-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-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-source-filesystem": "^4.6.0", + "gatsby-transformer-sharp": "^4.6.0", + "prettier": "2.5.1", + "react-is": "^17.0.2", + "styled-components": "^5.3.3" + } +} diff --git a/packages/dashboard-v2/postcss.config.js b/packages/dashboard-v2/postcss.config.js new file mode 100644 index 00000000..3b35b010 --- /dev/null +++ b/packages/dashboard-v2/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require("tailwindcss/nesting"), require("tailwindcss"), require("autoprefixer")], +}; diff --git a/packages/dashboard-v2/src/components/Button/Button.js b/packages/dashboard-v2/src/components/Button/Button.js new file mode 100644 index 00000000..230a5a93 --- /dev/null +++ b/packages/dashboard-v2/src/components/Button/Button.js @@ -0,0 +1,41 @@ +import PropTypes from "prop-types"; + +/** + * Primary UI component for user interaction + */ +export const Button = ({ primary, label, ...props }) => { + return ( + + ); +}; + +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, +}; + +Button.defaultProps = { + primary: false, + onClick: undefined, +}; diff --git a/packages/dashboard-v2/src/components/Button/Button.stories.js b/packages/dashboard-v2/src/components/Button/Button.stories.js new file mode 100644 index 00000000..74f2ca90 --- /dev/null +++ b/packages/dashboard-v2/src/components/Button/Button.stories.js @@ -0,0 +1,38 @@ +import { Button } from "./Button"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/Button", + component: Button, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: { + backgroundColor: { control: "color" }, + }, +}; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template = (args) => + ); +}; + +IconButton.propTypes = { + /** + * Is this the principal call to action on the page? + */ + primary: PropTypes.bool, + /** + * How large should the button be? + */ + size: PropTypes.oneOf(["small", "medium", "large"]), + /** + * Icon component + */ + icon: PropTypes.element.isRequired, + /** + * Optional click handler + */ + onClick: PropTypes.func, +}; + +IconButton.defaultProps = { + backgroundColor: null, + primary: false, + size: "medium", + onClick: undefined, +}; diff --git a/packages/dashboard-v2/src/components/IconButton/IconButton.stories.js b/packages/dashboard-v2/src/components/IconButton/IconButton.stories.js new file mode 100644 index 00000000..71ab054c --- /dev/null +++ b/packages/dashboard-v2/src/components/IconButton/IconButton.stories.js @@ -0,0 +1,39 @@ +import { IconButton } from "./IconButton"; +import { ArrowRightIcon } from "../Icons"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/IconButton", + component: IconButton, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: { + backgroundColor: { control: "color" }, + }, +}; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template = (args) => ; + +export const Primary = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Primary.args = { + primary: true, + icon: , +}; + +export const Secondary = Template.bind({}); +Secondary.args = { + icon: , +}; + +export const Large = Template.bind({}); +Large.args = { + size: "large", + icon: , +}; + +export const Small = Template.bind({}); +Small.args = { + size: "small", + icon: , +}; diff --git a/packages/dashboard-v2/src/components/IconButton/index.js b/packages/dashboard-v2/src/components/IconButton/index.js new file mode 100644 index 00000000..53185101 --- /dev/null +++ b/packages/dashboard-v2/src/components/IconButton/index.js @@ -0,0 +1 @@ +export * from "./IconButton"; diff --git a/packages/dashboard-v2/src/components/IconButtonText/IconButtonText.js b/packages/dashboard-v2/src/components/IconButtonText/IconButtonText.js new file mode 100644 index 00000000..e1bf4d55 --- /dev/null +++ b/packages/dashboard-v2/src/components/IconButtonText/IconButtonText.js @@ -0,0 +1,46 @@ +import PropTypes from "prop-types"; + +/** + * Primary UI component for user interaction + */ +export const IconButtonText = ({ primary, label, icon, ...props }) => { + return ( + + ); +}; + +IconButtonText.propTypes = { + /** + * Is this the principal call to action on the page? + */ + primary: PropTypes.bool, + /** + * Button Label + */ + label: PropTypes.string.isRequired, + /** + * Icon component + */ + icon: PropTypes.element.isRequired, + /** + * Optional click handler + */ + onClick: PropTypes.func, +}; + +IconButtonText.defaultProps = { + primary: false, + label: "", + onClick: undefined, +}; diff --git a/packages/dashboard-v2/src/components/IconButtonText/IconButtonText.stories.js b/packages/dashboard-v2/src/components/IconButtonText/IconButtonText.stories.js new file mode 100644 index 00000000..28f43542 --- /dev/null +++ b/packages/dashboard-v2/src/components/IconButtonText/IconButtonText.stories.js @@ -0,0 +1,29 @@ +import { IconButtonText } from "./IconButtonText"; +import { CogIcon } from "../Icons"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/IconButtonText", + component: IconButtonText, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: { + backgroundColor: { control: "color" }, + }, +}; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Template = (args) => ; + +export const Primary = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Primary.args = { + primary: true, + label: "Settings", + icon: , +}; + +export const Secondary = Template.bind({}); +Secondary.args = { + label: "Settings", + icon: , +}; diff --git a/packages/dashboard-v2/src/components/IconButtonText/index.js b/packages/dashboard-v2/src/components/IconButtonText/index.js new file mode 100644 index 00000000..ee1afc81 --- /dev/null +++ b/packages/dashboard-v2/src/components/IconButtonText/index.js @@ -0,0 +1 @@ +export * from "./IconButtonText"; diff --git a/packages/dashboard-v2/src/components/Icons/Icons.stories.js b/packages/dashboard-v2/src/components/Icons/Icons.stories.js new file mode 100644 index 00000000..de95188d --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/Icons.stories.js @@ -0,0 +1,37 @@ +import { Panel } from "../Panel"; +import * as icons from "."; + +export default { + title: "SkynetLibrary/Icons", + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export const DefaultSizeIcon = () => ; + +export const LargeIcon = () => ; + +export const AllIcons = () => { + const sizes = [24, 32, 60]; + + return ( + <> + {Object.entries(icons).map(([iconName, IconComponent]) => ( + +
{iconName}
+ +
+ {sizes.map((size) => ( + + ))} +
+
+ ))} + + ); +}; diff --git a/packages/dashboard-v2/src/components/Icons/icons/ArrowRightIcon.js b/packages/dashboard-v2/src/components/Icons/icons/ArrowRightIcon.js new file mode 100644 index 00000000..3e3dea95 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/ArrowRightIcon.js @@ -0,0 +1,11 @@ +import { withIconProps } from "../withIconProps"; + +export const ArrowRightIcon = withIconProps(({ size, ...props }) => ( + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/ChevronDownIcon.js b/packages/dashboard-v2/src/components/Icons/icons/ChevronDownIcon.js new file mode 100644 index 00000000..3aa411cf --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/ChevronDownIcon.js @@ -0,0 +1,14 @@ +import { withIconProps } from "../withIconProps"; + +export const ChevronDownIcon = withIconProps(({ size, ...props }) => ( + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/CogIcon.js b/packages/dashboard-v2/src/components/Icons/icons/CogIcon.js new file mode 100644 index 00000000..9b846047 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/CogIcon.js @@ -0,0 +1,11 @@ +import { withIconProps } from "../withIconProps"; + +export const CogIcon = withIconProps(({ size, ...props }) => ( + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/InfoIcon.js b/packages/dashboard-v2/src/components/Icons/icons/InfoIcon.js new file mode 100644 index 00000000..75a0a169 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/InfoIcon.js @@ -0,0 +1,11 @@ +import { withIconProps } from "../withIconProps"; + +export const InfoIcon = withIconProps(({ size, ...props }) => ( + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/LockClosedIcon.js b/packages/dashboard-v2/src/components/Icons/icons/LockClosedIcon.js new file mode 100644 index 00000000..d2559983 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/LockClosedIcon.js @@ -0,0 +1,11 @@ +import { withIconProps } from "../withIconProps"; + +export const LockClosedIcon = withIconProps(({ size, ...props }) => ( + + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/icons/SkynetLogoIcon.js b/packages/dashboard-v2/src/components/Icons/icons/SkynetLogoIcon.js new file mode 100644 index 00000000..ad45af62 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/icons/SkynetLogoIcon.js @@ -0,0 +1,8 @@ +import { withIconProps } from "../withIconProps"; + +export const SkynetLogoIcon = withIconProps(({ size, ...props }) => ( + + Skynet + + +)); diff --git a/packages/dashboard-v2/src/components/Icons/index.js b/packages/dashboard-v2/src/components/Icons/index.js new file mode 100644 index 00000000..28f87b15 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/index.js @@ -0,0 +1,6 @@ +export * from "./icons/ChevronDownIcon"; +export * from "./icons/CogIcon"; +export * from "./icons/LockClosedIcon"; +export * from "./icons/SkynetLogoIcon"; +export * from "./icons/ArrowRightIcon"; +export * from "./icons/InfoIcon"; diff --git a/packages/dashboard-v2/src/components/Icons/withIconProps.js b/packages/dashboard-v2/src/components/Icons/withIconProps.js new file mode 100644 index 00000000..d4267318 --- /dev/null +++ b/packages/dashboard-v2/src/components/Icons/withIconProps.js @@ -0,0 +1,19 @@ +import PropTypes from "prop-types"; + +const propTypes = { + /** + * Size of the icon's bounding box. + */ + size: PropTypes.number, +}; + +const defaultProps = { + size: 32, +}; + +export const withIconProps = (IconComponent) => { + IconComponent.propTypes = propTypes; + IconComponent.defaultProps = defaultProps; + + return IconComponent; +}; diff --git a/packages/dashboard-v2/src/components/NavBar/NavBar.js b/packages/dashboard-v2/src/components/NavBar/NavBar.js new file mode 100644 index 00000000..257b8a0d --- /dev/null +++ b/packages/dashboard-v2/src/components/NavBar/NavBar.js @@ -0,0 +1,21 @@ +import styled from "styled-components"; + +import { PageContainer } from "../PageContainer"; + +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", +})` + grid-template-columns: auto max-content 1fr; +`; + +export const NavBar = (props) => ( + + + + + +); diff --git a/packages/dashboard-v2/src/components/NavBar/NavBar.stories.js b/packages/dashboard-v2/src/components/NavBar/NavBar.stories.js new file mode 100644 index 00000000..e1022faa --- /dev/null +++ b/packages/dashboard-v2/src/components/NavBar/NavBar.stories.js @@ -0,0 +1,25 @@ +import { NavBar, NavBarLink, NavBarSection } from "."; + +export default { + title: "SkynetLibrary/NavBar", + component: NavBar, + subcomponents: { + NavBarSection, + NavBarLink, + }, +}; + +const Template = (props) => ( + + + + Dashboard + + Files + Payments + + +); + +export const DashboardTopNavigation = Template.bind({}); +DashboardTopNavigation.args = {}; diff --git a/packages/dashboard-v2/src/components/NavBar/NavBarLink.js b/packages/dashboard-v2/src/components/NavBar/NavBarLink.js new file mode 100644 index 00000000..772933c0 --- /dev/null +++ b/packages/dashboard-v2/src/components/NavBar/NavBarLink.js @@ -0,0 +1,19 @@ +import PropTypes from "prop-types"; +import styled from "styled-components"; + +export const NavBarLink = styled.a.attrs(({ active }) => ({ + className: ` + min-w-[168px] + flex h-full items-center justify-center + border-x border-x-palette-100 border-b-2 + text-palette-600 transition-colors hover:bg-palette-100/50 + ${active ? "border-b-primary" : "border-b-palette-200/50"} + `, +}))``; + +NavBarLink.propTypes = { + /** + * When set to true, an additional indicator will be rendered showing the item as active. + */ + active: PropTypes.bool, +}; diff --git a/packages/dashboard-v2/src/components/NavBar/NavBarSection.js b/packages/dashboard-v2/src/components/NavBar/NavBarSection.js new file mode 100644 index 00000000..773b2fa4 --- /dev/null +++ b/packages/dashboard-v2/src/components/NavBar/NavBarSection.js @@ -0,0 +1,3 @@ +import styled from "styled-components"; + +export const NavBarSection = styled.div.attrs({ className: "flex items-center" })``; diff --git a/packages/dashboard-v2/src/components/NavBar/index.js b/packages/dashboard-v2/src/components/NavBar/index.js new file mode 100644 index 00000000..afb300a6 --- /dev/null +++ b/packages/dashboard-v2/src/components/NavBar/index.js @@ -0,0 +1,3 @@ +export * from "./NavBar"; +export * from "./NavBarSection"; +export * from "./NavBarLink"; diff --git a/packages/dashboard-v2/src/components/PageContainer/PageContainer.js b/packages/dashboard-v2/src/components/PageContainer/PageContainer.js new file mode 100644 index 00000000..9c95b59c --- /dev/null +++ b/packages/dashboard-v2/src/components/PageContainer/PageContainer.js @@ -0,0 +1,13 @@ +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`, +})``; + +PageContainer.propTypes = { + /** + * Optional `class` attribute. + */ + className: PropTypes.string, +}; diff --git a/packages/dashboard-v2/src/components/PageContainer/index.js b/packages/dashboard-v2/src/components/PageContainer/index.js new file mode 100644 index 00000000..a40c7c89 --- /dev/null +++ b/packages/dashboard-v2/src/components/PageContainer/index.js @@ -0,0 +1 @@ +export * from "./PageContainer"; diff --git a/packages/dashboard-v2/src/components/Panel/Panel.js b/packages/dashboard-v2/src/components/Panel/Panel.js new file mode 100644 index 00000000..ea79b4d1 --- /dev/null +++ b/packages/dashboard-v2/src/components/Panel/Panel.js @@ -0,0 +1,33 @@ +import PropTypes from "prop-types"; +import styled from "styled-components"; + +const PanelBody = styled.div.attrs({ + className: "p-6 bg-white rounded", +})``; + +const PanelTitle = styled.h6.attrs({ + className: "uppercase text-xs text-palette-400 h-8 flex items-center", +})``; + +/** + * Besides documented props, it accepts all HMTL attributes a `
` element does. + * + * These additional props will be rendered onto the panel's body element. + */ +export const Panel = ({ title, ...props }) => ( +
+ {title && {title}} + +
+); + +Panel.propTypes = { + /** + * Label of the panel + */ + title: PropTypes.string, +}; + +Panel.defaultProps = { + title: "", +}; diff --git a/packages/dashboard-v2/src/components/Panel/Panel.stories.js b/packages/dashboard-v2/src/components/Panel/Panel.stories.js new file mode 100644 index 00000000..ac270119 --- /dev/null +++ b/packages/dashboard-v2/src/components/Panel/Panel.stories.js @@ -0,0 +1,65 @@ +import { Panel } from "./Panel"; + +export default { + title: "SkynetLibrary/Panel", + component: Panel, + decorators: [ + (Story) => ( +
+
+ +
+
+ ), + ], +}; + +const SampleContent = () => ( + <> +

This is the first paragraph

+

This is the second paragraph

+

This is the third paragraph

+ +); + +const Template = (args) => ( + + + +); + +export const RawPanel = Template.bind({}); +RawPanel.args = {}; + +export const TitledPanel = Template.bind({}); +TitledPanel.args = { + title: "Latest activity", +}; + +export const InlinePanelsExample = () => ( +
+ + + + + + +
+); + +export const FullWidthPanelsExample = () => ( + <> + + + + + + + +); + +export const CustomPanelBackground = Template.bind({}); +CustomPanelBackground.args = { + className: "bg-red-500", + title: "Background below should be red", +}; diff --git a/packages/dashboard-v2/src/components/Panel/index.js b/packages/dashboard-v2/src/components/Panel/index.js new file mode 100644 index 00000000..c9401816 --- /dev/null +++ b/packages/dashboard-v2/src/components/Panel/index.js @@ -0,0 +1 @@ +export * from "./Panel"; diff --git a/packages/dashboard-v2/src/components/PopoverMenu/PopoverMenu.js b/packages/dashboard-v2/src/components/PopoverMenu/PopoverMenu.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/dashboard-v2/src/components/PopoverMenu/index.js b/packages/dashboard-v2/src/components/PopoverMenu/index.js new file mode 100644 index 00000000..23131d37 --- /dev/null +++ b/packages/dashboard-v2/src/components/PopoverMenu/index.js @@ -0,0 +1 @@ +export * from "./PopoverMenu"; diff --git a/packages/dashboard-v2/src/components/Select/Select.js b/packages/dashboard-v2/src/components/Select/Select.js new file mode 100644 index 00000000..97d3d73b --- /dev/null +++ b/packages/dashboard-v2/src/components/Select/Select.js @@ -0,0 +1,102 @@ +import { Children, cloneElement, useEffect, useMemo, useRef } from "react"; +import PropTypes from "prop-types"; +import { useClickAway } from "react-use"; +import styled, { css, keyframes } from "styled-components"; + +import { ChevronDownIcon } from "../Icons"; +import { useCallbacks, useSelectReducer } from "./hooks"; +import { SelectOption } from "./SelectOption"; + +const dropDown = keyframes` + 0% { + transform: scaleY(0); + } + 80% { + transform: scaleY(1.1); + } + 100% { + transform: scaleY(1); + } +`; + +const Container = styled.div.attrs({ className: "relative inline-flex" })``; + +const Trigger = styled.button.attrs(({ placeholder }) => ({ + className: `flex items-center cursor-pointer ${placeholder ? "text-palette-300" : ""}`, +}))``; + +const TriggerIcon = styled(ChevronDownIcon).attrs({ + className: "transition-transform text-primary", +})` + transform: ${({ open }) => (open ? "rotateX(180deg)" : "none")}; +`; + +const Flyout = styled.ul.attrs(({ open }) => ({ + className: `absolute top-[20px] right-0 + p-0 h-0 border rounded bg-white + overflow-hidden pointer-events-none + shadow-md shadow-palette-200/50 + ${open ? "pointer-events-auto h-auto overflow-visible border-primary" : ""} + ${open ? "visible" : "invisible"}`, +}))` + animation: ${({ open }) => + open + ? css` + ${dropDown} 0.1s ease-in-out + ` + : "none"}; +`; + +export const Select = ({ defaultValue, children, onChange, placeholder }) => { + const selectRef = useRef(); + const options = useMemo(() => Children.toArray(children).filter(({ type }) => type === SelectOption), [children]); + const [state, dispatch] = useSelectReducer({ defaultValue, placeholder, options }); + const { close, toggle, selectOption } = useCallbacks(state, dispatch); + + useClickAway(selectRef, close); + + useEffect(() => { + if (state.selectedOptionIndex > -1) { + onChange(options[state.selectedOptionIndex].props.value); + } + }, [onChange, options, state.selectedOptionIndex]); + + const activeOption = options[state.selectedOptionIndex]; + const activeLabel = activeOption?.props?.label ?? null; + + return ( + + + {activeLabel ?? placeholder} + + + {options.map((item, index) => + cloneElement(item, { + ...item.props, + onClick: () => selectOption(index), + selected: state.selectedOptionIndex === index, + }) + )} + + + ); +}; + +Select.propTypes = { + /** + * `` elements. + */ + children: PropTypes.node.isRequired, + /** + * Default value to be selected upon rendering. + */ + defaultValue: PropTypes.string, + /** + * Callback for every change. + */ + onChange: PropTypes.func, + /** + * Placeholder to be displayed when no option is selected. + */ + placeholder: PropTypes.string, +}; diff --git a/packages/dashboard-v2/src/components/Select/Select.stories.js b/packages/dashboard-v2/src/components/Select/Select.stories.js new file mode 100644 index 00000000..9c8ddc1c --- /dev/null +++ b/packages/dashboard-v2/src/components/Select/Select.stories.js @@ -0,0 +1,47 @@ +import { Panel } from "../Panel"; +import { Select, SelectOption } from "."; + +export default { + title: "SkynetLibrary/Select", + component: Select, + subcomponents: { + SelectOption, + }, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +const Template = (props) => ( + +); +Template.args = {}; + +export const NoDefaultNoPlaceholder = Template.bind({}); +NoDefaultNoPlaceholder.args = { + onChange: console.info.bind(console, "onChange"), +}; + +export const WithPlaceholder = Template.bind({}); +WithPlaceholder.args = { + placeholder: "Select...", + onChange: console.info.bind(console, "onChange"), +}; + +export const WithDefautValue = Template.bind({}); +WithDefautValue.args = { + defaultValue: "size-desc", + placeholder: "Select...", + onChange: console.info.bind(console, "onChange"), +}; diff --git a/packages/dashboard-v2/src/components/Select/SelectOption.js b/packages/dashboard-v2/src/components/Select/SelectOption.js new file mode 100644 index 00000000..31c60cf5 --- /dev/null +++ b/packages/dashboard-v2/src/components/Select/SelectOption.js @@ -0,0 +1,29 @@ +import PropTypes from "prop-types"; +import styled from "styled-components"; + +const Option = styled.li.attrs(({ selected }) => ({ + className: `m-0 px-4 whitespace-nowrap py-1 px-4 cursor-pointer + transition-colors hover:bg-palette-100/50 + ${selected ? "pl-3.5 border-l-2 border-l-primary" : ""}`, +}))``; + +export const SelectOption = ({ selected, label, ...props }) => ( + +); + +SelectOption.propTypes = { + /** + * Label for the option + */ + label: PropTypes.string.isRequired, + + /** Value represented by the option */ + value: PropTypes.string.isRequired, + + /** + * Indicates an option is currently selected. **Controlled by parent ` + + + ); +}; + +Switch.propTypes = { + /** + * Switch's current value + */ + isOn: PropTypes.bool, + /** + * Function to execute on change + */ + handleToggle: PropTypes.func, +}; + +Switch.defaultProps = { + isOn: false, +}; diff --git a/packages/dashboard-v2/src/components/Switch/Switch.stories.js b/packages/dashboard-v2/src/components/Switch/Switch.stories.js new file mode 100644 index 00000000..41d64f6b --- /dev/null +++ b/packages/dashboard-v2/src/components/Switch/Switch.stories.js @@ -0,0 +1,22 @@ +import { Switch } from "./Switch"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/Switch", + component: Switch, + // 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) => ; + +export const SwitchTrue = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +SwitchTrue.args = { + isOn: true, +}; +export const SwitchFalse = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +SwitchFalse.args = { + isOn: false, +}; diff --git a/packages/dashboard-v2/src/components/Switch/index.js b/packages/dashboard-v2/src/components/Switch/index.js new file mode 100644 index 00000000..3b82ff05 --- /dev/null +++ b/packages/dashboard-v2/src/components/Switch/index.js @@ -0,0 +1 @@ +export * from "./Switch"; diff --git a/packages/dashboard-v2/src/components/Table/Table.js b/packages/dashboard-v2/src/components/Table/Table.js new file mode 100644 index 00000000..8d55e119 --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/Table.js @@ -0,0 +1,20 @@ +import styled from "styled-components"; + +const Container = styled.div.attrs({ + className: "p-1 max-w-full overflow-x-auto", +})``; + +const StyledTable = styled.table.attrs({ + className: "table-auto w-full border-separate", +})` + border-spacing: 0; +`; + +/** + * Accepts all HMTL attributes a `` element does. + */ +export const Table = (props) => ( + + + +); diff --git a/packages/dashboard-v2/src/components/Table/Table.stories.js b/packages/dashboard-v2/src/components/Table/Table.stories.js new file mode 100644 index 00000000..a6aba006 --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/Table.stories.js @@ -0,0 +1,93 @@ +import { CogIcon } from "../Icons"; + +import { IconButton } from "../IconButton"; +import { Table, TableBody, TableHead, TableCell, TableRow, TableHeadCell } from "./"; + +export default { + title: "SkynetLibrary/Table", + component: Table, + subcomponents: { + TableBody, + TableHead, + TableCell, + TableRow, + TableHeadCell, + }, +}; + +const DATA = [ + { + name: "At_vereo_eos_censes", + type: ".mp4", + size: "2.45 MB", + uploaded: "a few seconds ago", + skylink: "_HyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5P1izriu", + }, + { + name: "Miriam Klein IV", + type: ".pdf", + size: "7.52 MB", + uploaded: "01/04/2021; 17:11", + skylink: "_izriuHyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5P1", + }, + { + name: "tmp/QmWR6eVDVkwhAYq7X99w4xT9KNKBzwK39Fj1PDmr4ZnzMm/QmWR6eVDVkwhAYq7X99w4xT9KNKBzwK39Fj1PDmr4ZnzMm", + type: ".doc", + size: "8.15 MB", + uploaded: "10/26/2020; 7:21", + skylink: "_VXeRDgaDAAWg6Bmm5P1izriuHyFqH632Rmy99c93idTtB", + }, + { + name: "Perm_London", + type: ".avi", + size: "225.6 MB", + uploaded: "09/12/2020; 19:28", + skylink: "_eRDgaDAAWg6Bmm5P1izriuHyFqH632Rmy99c93idTtBVX", + }, + { + name: "Santa_Clara", + type: ".pdf", + size: "7.52 MB", + uploaded: "09/12/2020; 19:23", + skylink: "_AWg6Bmm5P1izriuHyFqH632Rmy99c93idTtBVXeRDgaDA", + }, + { + name: "Marysa_Labrone", + type: ".doc", + size: "8.15 MB", + uploaded: "09/12/2020; 19:21", + skylink: "_P1izriuHyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5", + }, +]; + +const Template = (args) => ( +
+ + + Name + Type + Size + Uploaded + Skylink + Activity + + + + {DATA.map(({ name, type, size, uploaded, skylink }) => ( + + {name} + {type} + {size} + {uploaded} + {skylink} + + }> + + + ))} + +
+); + +export const RegularTable = Template.bind({}); +RegularTable.args = {}; diff --git a/packages/dashboard-v2/src/components/Table/TableBody.js b/packages/dashboard-v2/src/components/Table/TableBody.js new file mode 100644 index 00000000..80a0b0b1 --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/TableBody.js @@ -0,0 +1,6 @@ +import styled from "styled-components"; + +/** + * Accepts all HMTL attributes a `` element does. + */ +export const TableBody = styled.tbody``; diff --git a/packages/dashboard-v2/src/components/Table/TableCell.js b/packages/dashboard-v2/src/components/Table/TableCell.js new file mode 100644 index 00000000..98f2bd3d --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/TableCell.js @@ -0,0 +1,13 @@ +import styled from "styled-components"; + +/** + * Accepts all HMTL attributes a `` element does. + */ +export const TableCell = styled.td.attrs({ + className: `px-6 py-4 h-tableRow truncate + text-palette-600 even:text-palette-400 + first:rounded-l-sm last:rounded-r-sm`, +})` + text-align: ${({ align }) => align ?? "left"}; + max-width: ${({ maxWidth }) => maxWidth ?? "none"}; +`; diff --git a/packages/dashboard-v2/src/components/Table/TableHead.js b/packages/dashboard-v2/src/components/Table/TableHead.js new file mode 100644 index 00000000..ad5ab8b0 --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/TableHead.js @@ -0,0 +1,6 @@ +import styled from "styled-components"; + +/** + * Accepts all HMTL attributes a `` element does. + */ +export const TableHead = styled.thead``; diff --git a/packages/dashboard-v2/src/components/Table/TableHeadCell.js b/packages/dashboard-v2/src/components/Table/TableHeadCell.js new file mode 100644 index 00000000..aeb65670 --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/TableHeadCell.js @@ -0,0 +1,13 @@ +import styled from "styled-components"; + +/** + * Accepts all HMTL attributes a `` element does. + */ +export const TableHeadCell = styled.th.attrs({ + className: `px-6 py-2.5 truncate h-tableRow + text-palette-600 font-sans font-light text-xs + first:rounded-l-sm last:rounded-r-sm`, +})` + text-align: ${({ align }) => align ?? "left"}; + max-width: ${({ maxWidth }) => maxWidth ?? "none"}; +`; diff --git a/packages/dashboard-v2/src/components/Table/TableRow.js b/packages/dashboard-v2/src/components/Table/TableRow.js new file mode 100644 index 00000000..3534a72b --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/TableRow.js @@ -0,0 +1,20 @@ +import PropTypes from "prop-types"; +import styled from "styled-components"; + +/** + * Besides documented props, it accepts all HMTL attributes a `` element does. + */ +export const TableRow = styled.tr.attrs(({ noHoverEffect }) => ({ + className: `bg-palette-100/50 odd:bg-white ${noHoverEffect ? "" : "hover:bg-palette-200/20"}`, +}))``; + +/** + * Allows disabling `hover` effect on a row. Useful for `` row. + */ +TableRow.propTypes = { + noHoverEffect: PropTypes.bool, +}; + +TableRow.defaultProps = { + noHoverEffect: false, +}; diff --git a/packages/dashboard-v2/src/components/Table/index.js b/packages/dashboard-v2/src/components/Table/index.js new file mode 100644 index 00000000..5368a1d9 --- /dev/null +++ b/packages/dashboard-v2/src/components/Table/index.js @@ -0,0 +1,6 @@ +export * from "./Table"; +export * from "./TableHead"; +export * from "./TableHeadCell"; +export * from "./TableBody"; +export * from "./TableRow"; +export * from "./TableCell"; diff --git a/packages/dashboard-v2/src/components/Tabs/ActiveTabIndicator.js b/packages/dashboard-v2/src/components/Tabs/ActiveTabIndicator.js new file mode 100644 index 00000000..f6f89266 --- /dev/null +++ b/packages/dashboard-v2/src/components/Tabs/ActiveTabIndicator.js @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; + +const Wrapper = styled.div.attrs({ + className: "absolute left-0 bottom-0 w-full h-0.5 bg-palette-200", +})``; + +const Indicator = styled.div.attrs({ + className: "absolute h-0.5 bottom-0 bg-primary duration-200 ease-in-out", +})` + will-change: left, width; +`; + +export const ActiveTabIndicator = ({ tabRef }) => { + const [position, setPosition] = useState(0); + const [width, setWidth] = useState(0); + + useEffect(() => { + if (!tabRef?.current) { + return; + } + + const { offsetLeft, offsetWidth } = tabRef.current; + setPosition(offsetLeft); + setWidth(offsetWidth); + }, [tabRef]); + + return ( + + + + ); +}; + +ActiveTabIndicator.propTypes = { + tabRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]), +}; diff --git a/packages/dashboard-v2/src/components/Tabs/Tab.js b/packages/dashboard-v2/src/components/Tabs/Tab.js new file mode 100644 index 00000000..1e7bd074 --- /dev/null +++ b/packages/dashboard-v2/src/components/Tabs/Tab.js @@ -0,0 +1,55 @@ +import { forwardRef } from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; + +export const StyledTab = styled.button.attrs(({ active, variant }) => ({ + className: `m-0 pr-2 pb-2 + text-tab text-left font-sans + transition-colors hover:text-palette-500 + ${active ? "font-semibold text-palette-600" : "font-light text-palette-300"} + ${variant === "regular" ? "sm:min-w-[180px]" : ""}`, +}))``; + +export const Tab = forwardRef(({ active, title, id, variant, ...props }, ref) => ( + + {title} + +)); + +Tab.displayName = "Tab"; + +Tab.propTypes = { + /** + * Used by `Tabs` component to control the `active` property, and also + * in the HTML markup for accessibility purposes. + * + * Should be set to the same value as related `TabPanel`'s `tabId` prop. + */ + id: PropTypes.string.isRequired, + /** + * Used as a label. + */ + title: PropTypes.string.isRequired, + /** + * Controlled by `Tabs` component. + */ + active: PropTypes.bool, + /** + * Controlled by `Tabs` component. + */ + variant: PropTypes.string, +}; + +Tab.defaultProps = { + variant: "regular", +}; diff --git a/packages/dashboard-v2/src/components/Tabs/TabPanel.js b/packages/dashboard-v2/src/components/Tabs/TabPanel.js new file mode 100644 index 00000000..a22becc6 --- /dev/null +++ b/packages/dashboard-v2/src/components/Tabs/TabPanel.js @@ -0,0 +1,31 @@ +import PropTypes from "prop-types"; + +/** + * Besides documented props, it accepts all HMTL attributes a `
` element does. + */ +export const TabPanel = ({ children, active, tabId, ...props }) => { + if (!active) { + return null; + } + + return ( +
+ {children} +
+ ); +}; + +TabPanel.propTypes = { + /** + * Used by `Tabs` component to control the `active` property, and also + * in the HTML markup for accessibility purposes. + * + * Should be set to the same value as related `Tab`'s `id` prop. + */ + tabId: PropTypes.string.isRequired, + children: PropTypes.node, + /** + * Controlled by `Tabs` component. + */ + active: PropTypes.bool, +}; diff --git a/packages/dashboard-v2/src/components/Tabs/Tabs.js b/packages/dashboard-v2/src/components/Tabs/Tabs.js new file mode 100644 index 00000000..14356466 --- /dev/null +++ b/packages/dashboard-v2/src/components/Tabs/Tabs.js @@ -0,0 +1,106 @@ +import { cloneElement, useCallback, useEffect, useMemo, useState } from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; + +import { ActiveTabIndicator } from "./ActiveTabIndicator"; +import { usePrefixedTabIds, useTabsChildren } from "./hooks"; + +const Container = styled.div.attrs({ + className: "tabs-container", +})``; + +const Header = styled.div.attrs({ + className: "relative flex justify-start overflow-hidden", +})``; + +const TabList = styled.div.attrs(({ variant }) => ({ + role: "tablist", + className: `relative inline-grid grid-flow-col auto-cols-fr + ${variant === "regular" ? "w-full sm:w-auto" : "w-full"}`, +}))``; + +const Divider = styled.div.attrs({ + "aria-hidden": "true", + className: "absolute bottom-0 w-screen border-b border-b-palette-200", +})` + right: calc(-100vw - 2px); +`; + +const Body = styled.div``; + +/** + * Besides documented props, it accepts all HMTL attributes a `
` element does. + */ +export const Tabs = ({ defaultTab, children, variant }) => { + const getTabId = usePrefixedTabIds(); + const { tabs, panels, tabsRefs } = useTabsChildren(children, getTabId); + const defaultTabId = useMemo(() => getTabId(defaultTab || tabs[0].props.id), [getTabId, defaultTab, tabs]); + const [activeTabId, setActiveTabId] = useState(defaultTabId); + const [activeTabRef, setActiveTabRef] = useState(tabsRefs[activeTabId]); + const isActive = (id) => id === activeTabId; + const onTabChange = useCallback( + (id) => { + setActiveTabId(id); + }, + [setActiveTabId] + ); + + useEffect(() => { + // Refresh active tab indicator whenever active tab changes. + setActiveTabRef(tabsRefs[activeTabId]); + }, [setActiveTabRef, tabsRefs, activeTabId]); + + return ( + +
+ + {tabs.map((tab) => { + const tabId = getTabId(tab.props.id); + + return cloneElement(tab, { + ref: tabsRefs[tabId], + id: tabId, + variant, + active: isActive(tabId), + onClick: () => onTabChange(tabId), + }); + })} + + + +
+ + {panels.map((panel) => { + const tabId = getTabId(panel.props.tabId); + + return cloneElement(panel, { + ...panel.props, + tabId, + active: isActive(tabId), + }); + })} + +
+ ); +}; + +Tabs.propTypes = { + /** + * Should include `` and `` components. + */ + children: PropTypes.node.isRequired, + /** + * ID of the `` which should be open by default + */ + defaultTab: PropTypes.string, + /** + * `regular` (default) will make the tabs only take as much space as they need + * + * `fill` will make the tabs spread throughout the available width + */ + variant: PropTypes.oneOf(["regular", "fill"]), +}; + +Tabs.defaultProps = { + variant: "regular", +}; diff --git a/packages/dashboard-v2/src/components/Tabs/Tabs.stories.js b/packages/dashboard-v2/src/components/Tabs/Tabs.stories.js new file mode 100644 index 00000000..812fec30 --- /dev/null +++ b/packages/dashboard-v2/src/components/Tabs/Tabs.stories.js @@ -0,0 +1,67 @@ +import { Tab, TabPanel, Tabs } from "./"; + +export default { + title: "SkynetLibrary/Tabs", + component: Tabs, + subcomponents: { Tab, TabPanel }, +}; + +const Template = (props) => ( + <> + + + + +
    +
  • Upload #1
  • +
  • Upload #2
  • +
  • Upload #3
  • +
  • Upload #4
  • +
+
+ +
    +
  • Download #1
  • +
  • Download #2
  • +
  • Download #3
  • +
  • Download #4
  • +
+
+
+ +); + +const RegularTabs = Template.bind({}); + +const FillingTabs = Template.bind({}); +FillingTabs.args = { + variant: "fill", +}; + +const FillingTabsInNarrowContainer = Template.bind({}); +FillingTabsInNarrowContainer.args = { + variant: "fill", + defaultTab: "downloads", +}; +FillingTabsInNarrowContainer.decorators = [ + (Story) => ( +
+ +
+ ), +]; + +const MultipleTabsComponents = Template.bind({}); +MultipleTabsComponents.args = { + variant: "fill", +}; +MultipleTabsComponents.decorators = [ + (Story) => ( +
+ + +
+ ), +]; + +export { RegularTabs, FillingTabs, FillingTabsInNarrowContainer, MultipleTabsComponents }; diff --git a/packages/dashboard-v2/src/components/Tabs/hooks.js b/packages/dashboard-v2/src/components/Tabs/hooks.js new file mode 100644 index 00000000..5bc0d19b --- /dev/null +++ b/packages/dashboard-v2/src/components/Tabs/hooks.js @@ -0,0 +1,33 @@ +import { Children, createRef, useCallback, useMemo } from "react"; + +import { Tab } from "./Tab"; +import { TabPanel } from "./TabPanel"; + +export const usePrefixedTabIds = () => { + const seed = useMemo(() => Math.random().toString().split(".")[1], []); + + return useCallback((id) => `${seed}-${id}`, [seed]); +}; + +export const useTabsChildren = (children, prefixId) => { + const childrenArray = useMemo(() => Children.toArray(children), [children]); + const tabs = useMemo(() => childrenArray.filter(({ type }) => type === Tab), [childrenArray]); + const panels = useMemo(() => childrenArray.filter(({ type }) => type === TabPanel), [childrenArray]); + const tabsRefs = useMemo( + () => + tabs.reduce( + (refs, tab) => ({ + ...refs, + [prefixId(tab.props.id)]: createRef(), + }), + {} + ), + [tabs, prefixId] + ); + + return { + tabs, + panels, + tabsRefs, + }; +}; diff --git a/packages/dashboard-v2/src/components/Tabs/index.js b/packages/dashboard-v2/src/components/Tabs/index.js new file mode 100644 index 00000000..ef181f43 --- /dev/null +++ b/packages/dashboard-v2/src/components/Tabs/index.js @@ -0,0 +1,3 @@ +export * from "./Tab"; +export * from "./Tabs"; +export * from "./TabPanel"; diff --git a/packages/dashboard-v2/src/components/TextIndicator/TextIndicator.js b/packages/dashboard-v2/src/components/TextIndicator/TextIndicator.js new file mode 100644 index 00000000..cc8c5ff3 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextIndicator/TextIndicator.js @@ -0,0 +1,31 @@ +import PropTypes from "prop-types"; + +/** + * Primary UI component for user interaction + */ +export const TextIndicator = ({ variant }) => { + return ( +
+ {variant === "success" ? "success" : variant === "next" ? "next" : "error"} +
+ ); +}; + +TextIndicator.propTypes = { + /** + * Variant of text indicator + */ + variant: PropTypes.oneOf(["success", "next", "error"]), +}; + +TextIndicator.defaultProps = { + variant: "success", +}; diff --git a/packages/dashboard-v2/src/components/TextIndicator/TextIndicator.stories.js b/packages/dashboard-v2/src/components/TextIndicator/TextIndicator.stories.js new file mode 100644 index 00000000..65242354 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextIndicator/TextIndicator.stories.js @@ -0,0 +1,27 @@ +import { TextIndicator } from "./TextIndicator"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/TextIndicator", + component: TextIndicator, + // 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) => ; + +export const Success = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Success.args = { + variant: "success", +}; + +export const Next = Template.bind({}); +Next.args = { + variant: "next", +}; + +export const Error = Template.bind({}); +Error.args = { + variant: "error", +}; diff --git a/packages/dashboard-v2/src/components/TextIndicator/index.js b/packages/dashboard-v2/src/components/TextIndicator/index.js new file mode 100644 index 00000000..03e9f194 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextIndicator/index.js @@ -0,0 +1 @@ +export * from "./TextIndicator"; diff --git a/packages/dashboard-v2/src/components/TextInputBasic/TextInputBasic.js b/packages/dashboard-v2/src/components/TextInputBasic/TextInputBasic.js new file mode 100644 index 00000000..9fa7f670 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextInputBasic/TextInputBasic.js @@ -0,0 +1,30 @@ +import PropTypes from "prop-types"; + +/** + * Primary UI component for user interaction + */ +export const TextInputBasic = ({ label, placeholder }) => { + return ( +
+

{label}

+ +
+ ); +}; + +TextInputBasic.propTypes = { + /** + * Icon to place in text input + */ + label: PropTypes.string, + /** + * Input placeholder + */ + placeholder: PropTypes.string, +}; diff --git a/packages/dashboard-v2/src/components/TextInputBasic/TextInputBasic.stories.js b/packages/dashboard-v2/src/components/TextInputBasic/TextInputBasic.stories.js new file mode 100644 index 00000000..8a179802 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextInputBasic/TextInputBasic.stories.js @@ -0,0 +1,18 @@ +import { TextInputBasic } from "./TextInputBasic"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/TextInputBasic", + component: TextInputBasic, + // 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) => ; + +export const Input = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +Input.args = { + label: "Display Name", + placeholder: "Your Name", +}; diff --git a/packages/dashboard-v2/src/components/TextInputBasic/index.js b/packages/dashboard-v2/src/components/TextInputBasic/index.js new file mode 100644 index 00000000..11506954 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextInputBasic/index.js @@ -0,0 +1 @@ +export * from "./TextInputBasic"; diff --git a/packages/dashboard-v2/src/components/TextInputIcon/TextInputIcon.js b/packages/dashboard-v2/src/components/TextInputIcon/TextInputIcon.js new file mode 100644 index 00000000..892da996 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextInputIcon/TextInputIcon.js @@ -0,0 +1,35 @@ +import PropTypes from "prop-types"; + +/** + * Primary UI component for user interaction + */ +export const TextInputIcon = ({ icon, position, placeholder }) => { + return ( +
+ {position === "left" ?
{icon}
: null} + + {position === "right" ?
{icon}
: null} +
+ ); +}; + +TextInputIcon.propTypes = { + /** + * Icon to place in text input + */ + icon: PropTypes.element, + /** + * Side to place icon + */ + position: PropTypes.oneOf(["left", "right"]), + /** + * Input placeholder + */ + placeholder: PropTypes.string, +}; diff --git a/packages/dashboard-v2/src/components/TextInputIcon/TextInputIcon.stories.js b/packages/dashboard-v2/src/components/TextInputIcon/TextInputIcon.stories.js new file mode 100644 index 00000000..676ca9cf --- /dev/null +++ b/packages/dashboard-v2/src/components/TextInputIcon/TextInputIcon.stories.js @@ -0,0 +1,27 @@ +import { TextInputIcon } from "./TextInputIcon"; +import { CogIcon } from "../Icons"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "SkynetLibrary/TextInputIcon", + component: TextInputIcon, + // 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) => ; + +export const IconLeft = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +IconLeft.args = { + icon: , + position: "left", + placeholder: "Search", +}; + +export const IconRight = Template.bind({}); +IconRight.args = { + icon: , + position: "right", + placeholder: "Search", +}; diff --git a/packages/dashboard-v2/src/components/TextInputIcon/index.js b/packages/dashboard-v2/src/components/TextInputIcon/index.js new file mode 100644 index 00000000..7043c232 --- /dev/null +++ b/packages/dashboard-v2/src/components/TextInputIcon/index.js @@ -0,0 +1 @@ +export * from "./TextInputIcon"; diff --git a/packages/dashboard-v2/src/layouts/DashboardLayout.js b/packages/dashboard-v2/src/layouts/DashboardLayout.js new file mode 100644 index 00000000..61d57870 --- /dev/null +++ b/packages/dashboard-v2/src/layouts/DashboardLayout.js @@ -0,0 +1,57 @@ +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"; + +const Layout = styled.div.attrs({ + className: "h-screen overflow-hidden", +})` + background-image: url(/images/dashboard-bg.svg); + background-position: -300px -280px; + + .navbar { + grid-template-columns: auto max-content 1fr; + } +`; + +const DashboardLayout = ({ children }) => { + return ( + <> + + + + + + + + Dashboard + + + Files + + + Payments + + + + + + + + + + +
{children}
+
+