diff --git a/packages/website/package.json b/packages/website/package.json index 75e8c802..e1039e93 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -9,6 +9,7 @@ "@fontsource/source-sans-pro": "^4.2.2", "@svgr/webpack": "^5.5.0", "autoprefixer": "^10.2.5", + "bytes": "^3.1.0", "classnames": "^2.2.6", "framer-motion": "^4.0.3", "gatsby": "^3.0.1", @@ -26,16 +27,21 @@ "gatsby-transformer-json": "^3.1.0", "gatsby-transformer-sharp": "^3.0.0", "gbimage-bridge": "^0.1.1", + "http-status-codes": "^2.1.4", "normalize.css": "^8.0.1", + "path-browserify": "^0.0.1", + "polished": "^4.1.1", "popmotion": "^9.3.4", "postcss": "^8.2.8", "preact-svg-loader": "^0.2.1", "prop-types": "^15.7.2", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-dropzone": "^11.3.1", "react-helmet": "^6.1.0", "react-svg-loader": "^3.0.3", "react-use": "^17.2.1", + "skynet-js": "^3.0.0", "tailwindcss": "^2.0.3" }, "devDependencies": { diff --git a/packages/website/src/components/HeroStartPage/index.js b/packages/website/src/components/HeroStartPage/index.js new file mode 100644 index 00000000..b323fd8c --- /dev/null +++ b/packages/website/src/components/HeroStartPage/index.js @@ -0,0 +1 @@ +export { default } from "./HeroStartPage"; diff --git a/packages/website/src/components/Icons/Add.svg b/packages/website/src/components/Icons/Add.svg new file mode 100644 index 00000000..7331a3d1 --- /dev/null +++ b/packages/website/src/components/Icons/Add.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/website/src/components/Icons/ArrowUpCircle.svg b/packages/website/src/components/Icons/ArrowUpCircle.svg new file mode 100644 index 00000000..96b076f7 --- /dev/null +++ b/packages/website/src/components/Icons/ArrowUpCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/components/Icons/CheckCircle.svg b/packages/website/src/components/Icons/CheckCircle.svg new file mode 100644 index 00000000..f6b316aa --- /dev/null +++ b/packages/website/src/components/Icons/CheckCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/website/src/components/Icons/Cloud.svg b/packages/website/src/components/Icons/Cloud.svg new file mode 100644 index 00000000..cdebc934 --- /dev/null +++ b/packages/website/src/components/Icons/Cloud.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/website/src/components/Icons/index.js b/packages/website/src/components/Icons/index.js index 941c77cd..d184f4c7 100644 --- a/packages/website/src/components/Icons/index.js +++ b/packages/website/src/components/Icons/index.js @@ -1,5 +1,9 @@ +export { default as Add } from "./Add.svg"; export { default as ArrowRight } from "./ArrowRight.svg"; +export { default as ArrowUpCircle } from "./ArrowUpCircle.svg"; export { default as CheckActive } from "./CheckActive.svg"; +export { default as CheckCircle } from "./CheckCircle.svg"; +export { default as Cloud } from "./Cloud.svg"; export { default as DiscordSmall } from "./DiscordSmall.svg"; export { default as DiscordSmallWhite } from "./DiscordSmallWhite.svg"; export { default as LogoBlackText } from "./LogoBlackText.svg"; diff --git a/packages/website/src/components/Layout/Layout.js b/packages/website/src/components/Layout/Layout.js index 52b3ca13..19b3e9c0 100644 --- a/packages/website/src/components/Layout/Layout.js +++ b/packages/website/src/components/Layout/Layout.js @@ -15,16 +15,26 @@ import Navigation from "../Navigation/Navigation"; import NewsHeader from "../NewsHeader/NewsHeader"; import Footer from "../Footer/Footer"; import FooterNavigation from "../FooterNavigation/FooterNavigation"; -// import { useWindowScroll } from "react-use"; +import { useWindowScroll } from "react-use"; import classnames from "classnames"; +import { readableColor } from "polished"; + +const modeMap = { "#fff": "dark", "#000": "light" }; const StickyHeader = () => { - // const { y } = useWindowScroll(); + useWindowScroll(); + + const ref = React.useRef(null); + const element = document.elementFromPoint(0, ref.current?.offsetHeight ?? 0); + + const backgroundColor = window.getComputedStyle(element, null).getPropertyValue("background-color"); + const color = React.useMemo(() => readableColor(backgroundColor), [backgroundColor]); + const mode = modeMap[color]; return ( -
+
- +
); }; diff --git a/packages/website/src/components/Layout/index.js b/packages/website/src/components/Layout/index.js new file mode 100644 index 00000000..d4dca0dc --- /dev/null +++ b/packages/website/src/components/Layout/index.js @@ -0,0 +1 @@ +export { default } from "./Layout"; diff --git a/packages/website/src/components/Navigation/Navigation.js b/packages/website/src/components/Navigation/Navigation.js index 40db2633..991777e1 100644 --- a/packages/website/src/components/Navigation/Navigation.js +++ b/packages/website/src/components/Navigation/Navigation.js @@ -7,6 +7,8 @@ import LogoBlackText from "../Icons/LogoBlackText.svg"; import MenuMobile from "../Icons/MenuMobile.svg"; import MenuMobileClose from "../Icons/MenuMobileClose.svg"; import DiscordSmall from "../Icons/DiscordSmall.svg"; +import { motion } from "framer-motion"; +import { useWindowSize, useWindowScroll } from "react-use"; const routes = [ { title: "Home", route: "/" }, @@ -48,17 +50,45 @@ const SignUpButton = ({ className, ...props }) => ( ); const Navigation = ({ mode }) => { + const navRef = React.useRef(null); const [open, setOpen] = React.useState(false); + const windowSize = useWindowSize(); + const { y: offsetY } = useWindowScroll(); + + React.useEffect(() => { + setOpen(false); + }, [windowSize, setOpen]); + + React.useEffect(() => { + if (open && document.body.style.overflow !== "hidden") { + document.body.style.overflow = "hidden"; + } else if (document.body.style.overflow === "hidden") { + document.body.style.overflow = "unset"; + } + }, [open]); + + const mobileMenuOffset = navRef.current ? navRef.current.offsetTop : 0; return ( - + ); }; diff --git a/packages/website/src/components/Uploader/Uploader.js b/packages/website/src/components/Uploader/Uploader.js new file mode 100644 index 00000000..738610a4 --- /dev/null +++ b/packages/website/src/components/Uploader/Uploader.js @@ -0,0 +1,255 @@ +import * as React from "react"; +import classnames from "classnames"; +import { Add, Cloud, ArrowUpCircle, CheckCircle } from "../Icons"; +import bytes from "bytes"; +import classNames from "classnames"; +import { getReasonPhrase, StatusCodes } from "http-status-codes"; +import path from "path-browserify"; +import { useDropzone } from "react-dropzone"; +import { SkynetClient } from "skynet-js"; + +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); +}; + +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 createUploadErrorMessage = (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."; + } + + // 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}`; +}; + +const client = new SkynetClient("https://siasky.net"); + +const UploadElement = ({ file, progress, status, url }) => { + const handleCopy = (url) => { + console.log(url); + }; + + return ( +
+
+ {status === "uploading" && } + {status === "processing" && } + {status === "complete" && } +
+
{file.name}
+
+
+ {status === "uploading" && ( + + Uploading {bytes(file.size * progress)} of {bytes(file.size)} + + )} + + {status === "processing" && Processing...} + + {status === "complete" && ( + + {url} + + )} +
+
+ {status === "uploading" && {Math.floor(progress * 100)}% completed} + {status === "complete" && ( + + )} +
+
+
+
+ +
+
+
+
+ ); +}; + +const Uploader = () => { + const [mode, setMode] = React.useState("file"); + const [files, setFiles] = React.useState([]); + + const handleDrop = async (acceptedFiles) => { + if (mode === "directory" && acceptedFiles.length) { + const rootDir = getRootDirectory(acceptedFiles[0]); // get the file path from the first file + + acceptedFiles = [{ name: rootDir, directory: true, files: acceptedFiles }]; + } + + setFiles((previousFiles) => [...acceptedFiles.map((file) => ({ file, status: "uploading" })), ...previousFiles]); + + const onFileStateChange = (file, state) => { + setFiles((previousFiles) => { + const index = previousFiles.findIndex((f) => f.file === file); + + return [ + ...previousFiles.slice(0, index), + { + ...previousFiles[index], + ...state, + }, + ...previousFiles.slice(index + 1), + ]; + }); + }; + + acceptedFiles.forEach((file) => { + const onUploadProgress = (progress) => { + const status = progress === 1 ? "processing" : "uploading"; + + onFileStateChange(file, { status, progress }); + }; + + // Reject files larger than our hard limit of 1 GB with proper message + if (file.size > bytes("1 GB")) { + onFileStateChange(file, { status: "error", error: "This file size exceeds the maximum allowed size of 1 GB." }); + + return; + } + + const upload = async () => { + try { + let response; + + if (file.directory) { + const directory = file.files.reduce((acc, file) => ({ ...acc, [getRelativeFilePath(file)]: file }), {}); + + response = await client.uploadDirectory(directory, encodeURIComponent(file.name), { onUploadProgress }); + } else { + response = await client.uploadFile(file, { onUploadProgress }); + } + + onFileStateChange(file, { status: "complete", url: client.getSkylinkUrl(response.skylink) }); + } catch (error) { + if (error.response && error.response.status === StatusCodes.TOO_MANY_REQUESTS) { + onFileStateChange(file, { progress: -1 }); + + return new Promise((resolve) => setTimeout(() => resolve(upload()), 3000)); + } + + onFileStateChange(file, { status: "error", error: createUploadErrorMessage(error) }); + } + }; + + upload(); + }); + }; + + const { getRootProps, getInputProps, isDragActive, inputRef } = useDropzone({ onDrop: handleDrop }); + const inputElement = inputRef.current; + + React.useEffect(() => { + if (mode === "directory") { + inputElement.setAttribute("webkitdirectory", "true"); + } else { + inputElement.removeAttribute("webkitdirectory"); + } + }, [mode, inputElement]); + + return ( +
+
+
+ + +
+
+ +
+ +

+ Add or drop your files here to pin to Skynet +

+
+
+
+ +
+
+
+ + {files.length > 0 && ( +
+ {files.map((file, index) => ( + + ))} +
+ )} +
+
+ ); +}; + +Uploader.propTypes = {}; + +Uploader.defaultProps = {}; + +export default Uploader; diff --git a/packages/website/src/components/Uploader/index.js b/packages/website/src/components/Uploader/index.js new file mode 100644 index 00000000..044a94cb --- /dev/null +++ b/packages/website/src/components/Uploader/index.js @@ -0,0 +1 @@ +export { default } from "./Uploader"; diff --git a/packages/website/src/pages/index.js b/packages/website/src/pages/index.js index 4e2a2458..ba42ffdb 100644 --- a/packages/website/src/pages/index.js +++ b/packages/website/src/pages/index.js @@ -1,9 +1,10 @@ import * as React from "react"; // import { StaticImage } from "gatsby-plugin-image"; -import Layout from "../components/Layout/Layout"; +import Layout from "../components/Layout"; import SEO from "../components/seo"; -import HeroStartPage from "../components/HeroStartPage/HeroStartPage"; +import HeroStartPage from "../components/HeroStartPage"; import CommunitySection from "../components/CommunitySection"; +import Uploader from "../components/Uploader"; import { ArrowRight, SkynetToolBig, @@ -17,8 +18,8 @@ import { } from "../components/Icons"; import classnames from "classnames"; -const Section = ({ children, className }) => ( -
+const Section = ({ children, className, ...props }) => ( +
{children}
); @@ -57,10 +58,15 @@ const IndexPage = () => ( -
+
+
+ {/*
*/} + +
+
The new decentralized internet is here diff --git a/packages/website/tailwind.config.js b/packages/website/tailwind.config.js index bcf1d352..35f33f75 100644 --- a/packages/website/tailwind.config.js +++ b/packages/website/tailwind.config.js @@ -18,7 +18,7 @@ module.exports = { tablet: "640px", md: "768px", lg: "1024px", - desktop: "1088px", + desktop: "1024px", xl: "1280px", hires: "1408px", "2xl": "1536px", @@ -32,6 +32,9 @@ module.exports = { sans: ["Sora", ...defaultTheme.fontFamily.sans], content: ["Source\\ Sans\\ Pro", ...defaultTheme.fontFamily.sans], }, + fontSize: { + xxs: ["0.625rem", "0.75rem"], + }, colors: { primary: { DEFAULT: "#00c65e", @@ -51,6 +54,7 @@ module.exports = { }, variants: { extend: { + animation: ["hover"], backgroundColor: ["disabled"], textColor: ["disabled"], margin: ["first"],