This commit is contained in:
Karol Wypchlo 2021-03-25 22:44:24 +01:00
parent 7c228c3f4b
commit 4980a99ef2
14 changed files with 407 additions and 47 deletions

View File

@ -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": {

View File

@ -0,0 +1 @@
export { default } from "./HeroStartPage";

View File

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66">
<defs>
<filter id="add-a">
<feColorMatrix in="SourceGraphic" values="0 0 0 0 0.050980 0 0 0 0 0.050980 0 0 0 0 0.050980 0 0 0 1.000000 0"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(-.097 .903)">
<circle cx="33.097" cy="33.097" r="32" fill="#00C65E"/>
<path stroke="#0D0D0D" stroke-width="2" d="M59.7279532,50.4620828 C62.9634424,45.4615529 64.8418319,39.5010119 64.8418319,33.1017494 C64.8418319,15.4286374 50.5149439,1.1017494 32.8418319,1.1017494 C15.1687199,1.1017494 0.84183186,15.4286374 0.84183186,33.1017494" transform="rotate(-2 32.842 25.782)"/>
<g filter="url(#add-a)" transform="translate(13.097 13.097)">
<path fill="#FFF" d="M21,12 L21,19 L28,19 L28,21 L21,21 L21,28 L19,28 L19,21 L12,21 L12,19 L19,19 L19,12 L21,12 Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 912 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
<path fill="#0D0D0D" d="M20,4 C28.836,4 36,11.164 36,20 C36,28.836 28.836,36 20,36 C11.164,36 4,28.836 4,20 C4,11.164 11.164,4 20,4 Z M20,6 C12.2685695,6 6,12.2685695 6,20 C6,27.7314305 12.2685695,34 20,34 C27.7314305,34 34,27.7314305 34,20 C34,12.2685695 27.7314305,6 20,6 Z M19.4363719,13.4966307 C19.8299265,13.1934645 20.3968364,13.2236414 20.7557217,13.5855059 L20.7557217,13.5855059 L25.7100234,18.5809324 L24.2899766,19.9892886 L21,16.673 L21,26 L19,26 L19,16.733 L15.7038828,20.0000001 L14.2961172,18.5793679 L19.3418155,13.5793679 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 638 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
<path fill="#0D0D0D" d="M20,4 C28.836,4 36,11.164 36,20 C36,28.836 28.836,36 20,36 C11.164,36 4,28.836 4,20 C4,11.164 11.164,4 20,4 Z M20,6 C12.2685695,6 6,12.2685695 6,20 C6,27.7314305 12.2685695,34 20,34 C27.7314305,34 34,27.7314305 34,20 C34,12.2685695 27.7314305,6 20,6 Z M26.0574126,15.2928932 L27.4716262,16.7071068 L18.0464454,26.1322875 L13.2928932,21.3787353 L14.7071068,19.9645218 L18.046,23.303 L26.0574126,15.2928932 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="62" height="54" viewBox="0 0 62 54">
<defs>
<filter id="cloud-a">
<feColorMatrix in="SourceGraphic" values="0 0 0 0 0.050980 0 0 0 0 0.050980 0 0 0 0 0.050980 0 0 0 1.000000 0"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<polygon fill="#00C65E" points="25.097 46.291 25.097 32 19.354 32 30 21 40.343 32 35 32 35 50"/>
<g filter="url(#cloud-a)">
<path stroke="#222829" stroke-width="2" d="M41 40L50 40C56.627 40 62 34.628 62 28 62 22.053 58 17 52 17 52 12 49 8 43 8L42 8C39.272 3.076 34.028 0 28 0 19.164 0 12 7.164 12 16 5.373 16 0 21.373 0 28 0 34.628 5.373 40 12 40L21 40M18 17C18 10.925 23 6 28 6"/>
<polyline stroke="#222829" stroke-linejoin="round" stroke-width="2" points="25 54 25 32 19 32 30 21 41 32 35 32 35 54"/>
<line x1="30" x2="30" y1="31" y2="33" stroke="#222829" stroke-linejoin="round" stroke-width="2"/>
<line x1="30" x2="30" y1="35" y2="37" stroke="#222829" stroke-linejoin="round" stroke-width="2"/>
<line x1="30" x2="30" y1="39" y2="41" stroke="#222829" stroke-linejoin="round" stroke-width="2"/>
<line x1="30" x2="30" y1="43" y2="45" stroke="#222829" stroke-linejoin="round" stroke-width="2"/>
<line x1="30" x2="30" y1="47" y2="49" stroke="#222829" stroke-linejoin="round" stroke-width="2"/>
<line x1="30" x2="30" y1="51" y2="53" stroke="#222829" stroke-linejoin="round" stroke-width="2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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";

View File

@ -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 (
<div className={classnames("sticky top-0", { "bg-white border-b border-palette-200": false })}>
<div ref={ref} className={classnames("sticky top-0 z-50", { "bg-white border-b border-palette-200": false })}>
<NewsHeader />
<Navigation mode={false ? "light" : "dark"} />
<Navigation mode={mode} />
</div>
);
};

View File

@ -0,0 +1 @@
export { default } from "./Layout";

View File

@ -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 (
<nav className={classnames("relative px-8 py-12", { "bg-palette-600": open }, "desktop:bg-transparent")}>
<div className="max-w-layout mx-auto">
<motion.nav
className={classnames("relative px-8 transition-colors duration-500", {
"bg-white border-b border-palette-200": mode === "light",
"bg-palette-600 bg-opacity-50": mode === "dark",
})}
initial={false}
animate={{
paddingTop: offsetY ? "24px" : "48px",
paddingBottom: offsetY ? "24px" : "48px",
}}
ref={navRef}
>
<div className={classnames("max-w-layout mx-auto")}>
<div className="flex justify-between">
<Link to="/" className={classnames("flex flex-shrink-0 items-center", { hidden: open }, "desktop:flex")}>
{mode === "dark" && <LogoWhiteText className="h-8 w-auto" />}
{mode === "light" && <LogoBlackText className="h-8 w-auto" />}
</Link>
<div className="ml-auto flex items-center desktop:hidden">
<div className="ml-auto flex items-center desktop:hidden z-10">
<button
type="button"
className="inline-flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
@ -71,12 +101,13 @@ const Navigation = ({ mode }) => {
<MenuMobileClose className={classnames({ hidden: !open })} aria-hidden="true" />
</button>
</div>
<div className="hidden desktop:ml-6 desktop:flex desktop:items-center desktop:space-x-12">
{routes.map(({ title, route }) => (
<Link
key={route}
to={route}
className={classnames("text-sm font-light", {
className={classnames("text-sm font-light transition-colors duration-500", {
"text-white": mode === "dark",
"text-palette-600": mode === "light",
})}
@ -88,42 +119,43 @@ const Navigation = ({ mode }) => {
<SignUpButton />
</div>
</div>
</div>
<div
className={classnames("absolute bg-palette-600 inset-x-0 px-8 pb-12 desktop:hidden", {
block: open,
hidden: !open,
})}
id="mobile-menu"
>
<ul className="pt-4 pb-10 space-y-2">
{routes.map(({ title, route }) => (
<li key={title}>
<Link key={route} to={route} className="text-xl leading-7 font-semibold text-white">
{title}
</Link>
</li>
))}
</ul>
<div className="border-t border-palette-500 py-7">
<a
href="https://discordapp.com/invite/sia"
className="text-palette-300 text-m font-content flex items-center"
target="_blank"
rel="noopener noreferrer"
>
<DiscordSmall className="mr-2 fill-current text-white" />
<span>Join our Discord</span>
</a>
</div>
<div className="pt-12 pb-8 border-t border-palette-500">
<div className="flex items-center px-4 space-x-6">
<LogInButton className="flex-grow" />
<SignUpButton className="flex-grow" />
<div
className={classnames("fixed bg-palette-600 inset-0 px-8 py-12 desktop:hidden", {
block: open,
hidden: !open,
})}
style={{ marginTop: mobileMenuOffset }}
>
<ul className="py-10 space-y-2">
{routes.map(({ title, route }) => (
<li key={title}>
<Link key={route} to={route} className="text-xl leading-7 font-semibold text-white">
{title}
</Link>
</li>
))}
</ul>
<div className="border-t border-palette-500 py-7">
<a
href="https://discordapp.com/invite/sia"
className="text-palette-300 text-m font-content flex items-center"
target="_blank"
rel="noopener noreferrer"
>
<DiscordSmall className="mr-2 fill-current text-white" />
<span>Join our Discord</span>
</a>
</div>
<div className="pt-12 pb-8 border-t border-palette-500">
<div className="flex items-center justify-center px-4 space-x-6">
<LogInButton className="px-10" />
<SignUpButton className="px-10" />
</div>
</div>
</div>
</div>
</nav>
</motion.nav>
);
};

View File

@ -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 (
<div>
<div className="flex items-center">
{status === "uploading" && <ArrowUpCircle />}
{status === "processing" && <ArrowUpCircle />}
{status === "complete" && <CheckCircle />}
<div className="flex flex-col flex-grow ml-3">
<div className="text-palette-600 text-sm font-light">{file.name}</div>
<div className="flex justify-between text-palette-400 text-xs">
<div className="font-content">
{status === "uploading" && (
<span>
Uploading {bytes(file.size * progress)} of {bytes(file.size)}
</span>
)}
{status === "processing" && <span>Processing...</span>}
{status === "complete" && (
<a href={url} target="_blank" rel="noopener noreferrer">
{url}
</a>
)}
</div>
<div>
{status === "uploading" && <span className="uppercase">{Math.floor(progress * 100)}% completed</span>}
{status === "complete" && (
<button className="uppercase" onClick={() => handleCopy(url)}>
Copy link
</button>
)}
</div>
</div>
</div>
</div>
<div className="flex bg-palette-200 mt-1" style={{ height: "5px" }}>
<div style={{ width: `${Math.floor(progress * 100)}%` }} className="bg-primary" />
</div>
</div>
);
};
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 (
<div className="px-8 py-12">
<div className="max-w-content mx-auto rounded-lg shadow bg-white z-0 relative">
<div className="flex">
<button
className={classnames("uppercase text-xxs desktop:text-xs w-1/2 p-3 rounded-tl-lg leading-8", {
"bg-primary": mode === "file",
"bg-palette-200": mode === "directory",
})}
onClick={() => setMode("file")}
>
<span className="hidden desktop:inline">Try it now and upload your files</span>
<span className="inline desktop:hidden">Upload files</span>
</button>
<button
className={classnames("uppercase text-xxs desktop:text-xs w-1/2 p-3 rounded-tr-lg leading-8", {
"bg-primary": mode === "directory",
"bg-palette-200": mode === "file",
})}
onClick={() => setMode("directory")}
>
<span className="hidden desktop:inline">Do you want to upload an entire directory?</span>
<span className="inline desktop:hidden">Upload directory</span>
</button>
</div>
<div
className={classNames("p-4 relative home-upload-dropzone", {
"drop-active": isDragActive,
})}
{...getRootProps()}
>
<input {...getInputProps()} />
<div
className={classnames(
"p-8 border-2 border-dashed border-palette-200 rounded-lg flex flex-col items-center",
{
"bg-palette-100": isDragActive,
}
)}
>
<Cloud />
<h4 className="font-light text-palette-600 text-lg mt-2 text-center">
Add or drop your files here to pin to Skynet
</h4>
</div>
<div className="absolute left-1/2 -bottom-4 desktop:-bottom-8">
<div className="relative -left-1/2" role="button">
<Add />
</div>
</div>
</div>
{files.length > 0 && (
<div className="flex flex-col space-y-5 p-14">
{files.map((file, index) => (
<UploadElement key={index} {...file} />
))}
</div>
)}
</div>
</div>
);
};
Uploader.propTypes = {};
Uploader.defaultProps = {};
export default Uploader;

View File

@ -0,0 +1 @@
export { default } from "./Uploader";

View File

@ -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 }) => (
<div className={classnames("px-8 p-3", className)}>
const Section = ({ children, className, ...props }) => (
<div className={classnames("p-8", className)} {...props}>
<div className="max-w-content mx-auto">{children}</div>
</div>
);
@ -57,10 +58,15 @@ const IndexPage = () => (
<Layout>
<SEO title="Home" />
<Section className="py-24">
<Section>
<HeroStartPage />
</Section>
<Section className="relative">
{/* <div className="absolute inset-x-0 bg-white bottom-0" style={{ top: 256 }}></div> */}
<Uploader />
</Section>
<Section className="bg-white py-32">
<SectionTitle className="text-center mb-11">The new decentralized internet is here</SectionTitle>

View File

@ -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"],