diff --git a/.gitignore b/.gitignore index b9d93c2..96c2ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -113,7 +113,7 @@ dist /coverage # production -/build +/dist # misc .DS_Store @@ -128,3 +128,4 @@ yarn-error.log* .npmrc index.generated.css +.wrangler diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..27be00e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +yarn run test +yarn run lint +yarn lint-staged diff --git a/README.md b/README.md index c8ff4f6..9be2b18 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ See the section about [running tests](https://facebook.github.io/create-react-ap ### `npm run build` -Builds the app for production to the `build` folder.\ +Builds the app for production to the `dist` folder.\ It correctly bundles React in production mode and optimizes the build for the best performance. -The build is minified and the filenames include the hashes.\ +The build is minified and obfuscated, and the filenames include the hashes.\ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. diff --git a/cloudflare/worker.ts b/cloudflare/worker.ts new file mode 100644 index 0000000..d6d040a --- /dev/null +++ b/cloudflare/worker.ts @@ -0,0 +1,5 @@ +export default { + async fetch(request, env) { + return env.ASSETS.fetch(request); + }, +} satisfies ExportedHandler; diff --git a/eslint.config.mjs b/eslint.config.mjs index f3cf7c8..2478e8c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,7 +2,7 @@ import eslintConfigInternxt from '@internxt/eslint-config-internxt'; export default [ { - ignores: ['build', 'coverage', 'node_modules'], + ignores: ['dist', 'coverage', 'node_modules'], }, ...eslintConfigInternxt, ]; diff --git a/package.json b/package.json index bfba73d..4627386 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "add:npmrc": "cp .npmrc.template .npmrc", "vercel:install": "yarn run add:npmrc && yarn install", "lint": "eslint .", - "pretty": "prettier --write **/*.{js,jsx,tsx,ts}" + "pretty": "prettier --write **/*.{js,jsx,tsx,ts}", + "prepare": "husky", + "cloudflare:dev": "yarn build && wrangler dev" }, "dependencies": { "@headlessui/react": "2.2.9", @@ -20,7 +22,7 @@ "@internxt/sdk": "1.11.12", "@sentry/react": "10.22.0", "async": "3.2.6", - "axios": "1.13.0", + "axios": "1.13.1", "bip39": "3.1.0", "browserify-zlib": "0.2.0", "buffer": "6.0.3", @@ -42,7 +44,7 @@ "react-dropzone": "14.3.8", "react-hot-toast": "2.6.0", "react-markdown": "10.1.0", - "react-router-dom": "7.9.4", + "react-router-dom": "7.9.5", "stream-browserify": "3.0.0", "stream-http": "3.2.0", "streamsaver": "2.0.6", @@ -65,22 +67,24 @@ ] }, "devDependencies": { + "@cloudflare/vite-plugin": "1.13.17", "@internxt/eslint-config-internxt": "2.0.1", "@internxt/prettier-config": "internxt/prettier-config#v1.0.2", - "@rollup/plugin-replace": "6.0.2", + "@rollup/plugin-replace": "6.0.3", "@tailwindcss/vite": "4.1.16", "@testing-library/jest-dom": "6.9.1", "@types/async": "3.2.25", "@types/lodash.throttle": "4.1.9", - "@types/node": "24.9.1", + "@types/node": "24.9.2", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "@types/streamsaver": "2.0.5", "@vitejs/plugin-react": "5.1.0", - "@vitest/coverage-istanbul": "4.0.4", + "@vitest/coverage-istanbul": "4.0.5", "autoprefixer": "10.4.21", "dotenv": "17.2.3", "eslint": "9.38.0", + "husky": "9.1.7", "jsdom": "27.0.1", "lint-staged": "16.2.6", "postcss": "8.5.6", @@ -90,7 +94,8 @@ "vite-plugin-bundle-obfuscator": "1.8.0", "vite-plugin-node-polyfills": "0.24.0", "vite-plugin-svgr": "4.5.0", - "vitest": "4.0.4" + "vitest": "4.0.5", + "wrangler": "4.45.2" }, "lint-staged": { "*.{js,jsx,tsx,ts}": [ diff --git a/src/App.tsx b/src/App.tsx index ab4b70a..90358e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,15 @@ -import { Toaster } from "react-hot-toast"; -import { BrowserRouter, Navigate, Route, Routes, useParams, useSearchParams } from "react-router-dom"; -import RootDropzone from "./components/RootDropzone"; -import { FilesProvider } from "./contexts/Files"; -import DownloadView from "./views/DownloadView"; -import HomeView from "./views/HomeView"; -import NotFoundView from "./views/NotFoundView"; - +import { Toaster } from 'react-hot-toast'; +import { BrowserRouter, Navigate, Route, Routes, useParams, useSearchParams } from 'react-router-dom'; +import RootDropzone from './components/RootDropzone'; +import { FilesProvider } from './contexts/Files'; +import DownloadView from './views/DownloadView'; +import HomeView from './views/HomeView'; +import NotFoundView from './views/NotFoundView'; function DownloadRedirectWrapper() { const { sendId } = useParams(); const [searchParams] = useSearchParams(); - const code = searchParams.get("code") ?? ''; + const code = searchParams.get('code') ?? ''; return ; } diff --git a/src/Layout.tsx b/src/Layout.tsx index 7abe74c..eb52b5b 100644 --- a/src/Layout.tsx +++ b/src/Layout.tsx @@ -1,15 +1,34 @@ -import { ReactNode, RefObject, useEffect, useRef, useState } from "react"; - -import Card from "./components/Card"; -import Navbar from "./components/Navbar/Navbar"; -import { ArrowCircleDown, CaretRight } from "phosphor-react"; -import lang from "./assets/lang/en/send.json"; -import Privacy from "./assets/images/HeroSectionImages/Privacy.webp"; -import Blog from "./assets/images/HeroSectionImages/Blog.webp"; -import Pricing from "./assets/images/HeroSectionImages/Pricing.webp"; -import PrivacyLaptop from "./assets/images/HeroSectionImages/Privacy.svg"; -import BlogLaptop from "./assets/images/HeroSectionImages/Blog.svg"; -import PricingLaptop from "./assets/images/HeroSectionImages/Pricing.svg"; +import { ReactNode, RefObject, useEffect, useRef, useState } from 'react'; + +import Card from './components/Card'; +import Navbar from './components/Navbar/Navbar'; +import { ArrowCircleDown, CaretRight } from 'phosphor-react'; +import lang from './assets/lang/en/send.json'; +import Privacy from './assets/images/HeroSectionImages/Privacy.webp'; +import Blog from './assets/images/HeroSectionImages/Blog.webp'; +import Pricing from './assets/images/HeroSectionImages/Pricing.webp'; +import PrivacyLaptop from './assets/images/HeroSectionImages/Privacy.svg'; +import BlogLaptop from './assets/images/HeroSectionImages/Blog.svg'; +import PricingLaptop from './assets/images/HeroSectionImages/Pricing.svg'; + +interface Item { + text: { + title: string; + description: string; + cta?: string; + ctaLink?: string; + }; + image?: string; + background: + | { + backgroundImage: string; + background?: undefined; + } + | { + background: string; + backgroundImage?: undefined; + }; +} const heroSectionTextPaths = [ lang.HeroSection.index, @@ -19,59 +38,37 @@ const heroSectionTextPaths = [ lang.HeroSection.pricing, ]; -const heroSectionImages = [ - `${window.origin}/Drive-1.webp`, - Privacy, - Blog, - Pricing, -]; -const heroSectionImagesForLaptop = [ - `${window.origin}/Drive-1.webp`, - PrivacyLaptop, - BlogLaptop, - PricingLaptop, -]; +const heroSectionImages = [`${window.origin}/Drive-1.webp`, Privacy, Blog, Pricing]; +const heroSectionImagesForLaptop = [`${window.origin}/Drive-1.webp`, PrivacyLaptop, BlogLaptop, PricingLaptop]; const backgroundColor = [ { backgroundImage: `url(${window.origin}/bg.png)` }, { - background: "radial-gradient(50% 50% at 50% 50%, #00A4C8 0%, #161616 100%)", + background: 'radial-gradient(50% 50% at 50% 50%, #00A4C8 0%, #161616 100%)', }, { - background: "radial-gradient(50% 50% at 50% 50%, #905CFF 0%, #161616 100%)", + background: 'radial-gradient(50% 50% at 50% 50%, #905CFF 0%, #161616 100%)', }, { - background: "radial-gradient(50% 50% at 50% 50%, #0058DB 0%, #161616 100%)", + background: 'radial-gradient(50% 50% at 50% 50%, #0058DB 0%, #161616 100%)', }, { - background: "radial-gradient(50% 50% at 50% 50%, #0058DB 0%, #161616 100%)", + background: 'radial-gradient(50% 50% at 50% 50%, #0058DB 0%, #161616 100%)', }, ]; -const BgLoop = (text: any, ctaRef: RefObject) => { +const BgLoop = (text: Item['text'], ctaRef: RefObject) => { return ( -
+
-
-

+
+

{text.title}

-

- {text.description} -

+

{text.description}

{text.cta ? (
{ - window.open(text.ctaLink, "_blank"); + window.open(text.ctaLink, '_blank'); }} className="mt-5 flex cursor-pointer flex-row items-center space-x-1 hover:underline" > @@ -96,10 +93,10 @@ export default function Layout({ const ctaRef = useRef(null); const imageRef = useRef(null); const [imagesLoaded, setImagesLoaded] = useState([]); - let height = useRef(window.innerHeight); - const [item, setItem] = useState>({ + const height = useRef(window.innerHeight); + const [item, setItem] = useState({ text: heroSectionTextPaths[0], - image: "", + image: '', background: backgroundColor[0], }); @@ -123,13 +120,13 @@ export default function Layout({ const background = new Image(); background.src = `${window.origin}/bg.png`; - background.addEventListener("load", () => { - backgroundRef.current?.classList.remove("opacity-0"); - backgroundRef.current?.classList.add("opacity-100"); + background.addEventListener('load', () => { + backgroundRef.current?.classList.remove('opacity-0'); + backgroundRef.current?.classList.add('opacity-100'); }); }, []); - ctaRef.current?.classList.add("opacity-100"); + ctaRef.current?.classList.add('opacity-100'); useEffect(() => { let currentIndex = 0; @@ -145,12 +142,12 @@ export default function Layout({ const newBg = backgroundColor[currentIndex]; // Fade out - ctaRef.current?.classList.remove("opacity-100"); - ctaRef.current?.classList.add("opacity-0"); - backgroundRef.current?.classList.remove("opacity-100"); - backgroundRef.current?.classList.add("opacity-0"); - imageRef.current?.classList.remove("opacity-100"); - imageRef.current?.classList.add("opacity-0"); + ctaRef.current?.classList.remove('opacity-100'); + ctaRef.current?.classList.add('opacity-0'); + backgroundRef.current?.classList.remove('opacity-100'); + backgroundRef.current?.classList.add('opacity-0'); + imageRef.current?.classList.remove('opacity-100'); + imageRef.current?.classList.add('opacity-0'); // Update text and image setTimeout(() => { @@ -170,12 +167,12 @@ export default function Layout({ setTimeout(() => { // Fade in - ctaRef.current?.classList.remove("opacity-0"); - ctaRef.current?.classList.add("opacity-100"); - backgroundRef.current?.classList.remove("opacity-0"); - backgroundRef.current?.classList.add("opacity-100"); - imageRef.current?.classList.remove("opacity-0"); - imageRef.current?.classList.add("opacity-100"); + ctaRef.current?.classList.remove('opacity-0'); + ctaRef.current?.classList.add('opacity-100'); + backgroundRef.current?.classList.remove('opacity-0'); + backgroundRef.current?.classList.add('opacity-100'); + imageRef.current?.classList.remove('opacity-0'); + imageRef.current?.classList.add('opacity-100'); }, 1000); // Wait for fade out animation to complete }, 7000); // 10 seconds @@ -183,12 +180,12 @@ export default function Layout({ }, [imagesLoaded]); useEffect(() => { - window.addEventListener("resize", () => { + window.addEventListener('resize', () => { height.current = window.innerHeight; }); return () => { - window.removeEventListener("resize", () => {}); + window.removeEventListener('resize', () => {}); }; }, []); @@ -196,7 +193,7 @@ export default function Layout({ <>
-
-
+
+
- - {children} - -
-
- {BgLoop(item.text, ctaRef)} + {children}
+
{BgLoop(item.text, ctaRef)}
- {hasContentBelow && + {hasContentBelow && (
- +
- } + )}
{item.image && ( )}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 3b15b9e..8faa60a 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,7 +1,7 @@ -import { ReactNode } from "react"; +import { ReactNode } from 'react'; export default function Button({ - className = "", + className = '', children, disabled, outline, @@ -13,17 +13,11 @@ export default function Button({ outline?: boolean; onClick?: () => void; }) { - const background = outline - ? "bg-transparent" - : "bg-primary active:bg-primary-dark disabled:bg-gray-40"; + const background = outline ? 'bg-transparent' : 'bg-primary active:bg-primary-dark disabled:bg-gray-40'; - const textColor = outline - ? "text-primary active:text-primary-dark disabled:text-gray-40" - : "text-white"; + const textColor = outline ? 'text-primary active:text-primary-dark disabled:text-gray-40' : 'text-white'; - const border = outline - ? "border border-primary active:border-primary-dark disabled:border-gray-40" - : ""; + const border = outline ? 'border border-primary active:border-primary-dark disabled:border-gray-40' : ''; return (
+
{children}
); } diff --git a/src/components/CardBotton.tsx b/src/components/CardBotton.tsx index e462c55..439fe7c 100644 --- a/src/components/CardBotton.tsx +++ b/src/components/CardBotton.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode } from 'react'; export default function CardBottom({ children }: { children: ReactNode }) { return ( diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index cf73048..266fc9e 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -7,7 +7,7 @@ export default function Dropdown({ menuItems, classMenuItems, openDirection, - buttonInputRef + buttonInputRef, }: { children: ReactNode; classButton?: string; @@ -19,7 +19,9 @@ export default function Dropdown({ return (
- {children} + + {children} +
-

- {progress <= 99 ? progress : 99} -

+

{progress <= 99 ? progress : 99}

%

- + { setActive(!active); }} - className="my-5 flex cursor-pointer flex-row items-center justify-between space-x-6 text-left hover:text-primary" + className="my-5 flex cursor-pointer flex-row items-center justify-between space-x-6 text-left + hover:text-primary" > - + {question} - + - {answer.map((text: any) => { + {answer.map((text: string) => { return {text}; })} diff --git a/src/components/FileArea.tsx b/src/components/FileArea.tsx index 76f6674..90ff32e 100644 --- a/src/components/FileArea.tsx +++ b/src/components/FileArea.tsx @@ -1,26 +1,12 @@ -import { File, Folder, PlusCircle } from "phosphor-react"; -import { - ChangeEvent, - forwardRef, - MouseEvent, - ReactNode, - useContext, - useRef, - useState, -} from "react"; -import { FilesContext } from "../contexts/Files"; -import { format } from "bytes"; -import { MAX_BYTES_PER_SEND, MAX_ITEMS_PER_LINK } from "../constants"; -import ItemsList from "./ItemList"; -import Dropdown from "./Dropdown"; +import { File, Folder, PlusCircle } from 'phosphor-react'; +import { ChangeEvent, forwardRef, MouseEvent, ReactNode, useContext, useRef, useState } from 'react'; +import { FilesContext } from '../contexts/Files'; +import { format } from 'bytes'; +import { MAX_BYTES_PER_SEND, MAX_ITEMS_PER_LINK } from '../constants'; +import ItemsList from './ItemList'; +import Dropdown from './Dropdown'; -export default function FileArea({ - className = "", - scroll, -}: { - className?: string; - scroll: boolean; -}) { +export default function FileArea({ className = '', scroll }: { className?: string; scroll: boolean }) { const fileInputRef = useRef(null); const folderInputRef = useRef(null); const dropdownMenuButtonRef = useRef(null); @@ -43,31 +29,26 @@ export default function FileArea({ if (event.target.files) { fileContext.addFiles(Array.from(event.target.files)); } - if (dropdownMenuButtonRef.current?.ariaExpanded === "true") { + if (dropdownMenuButtonRef.current?.ariaExpanded === 'true') { dropdownMenuButtonRef.current?.click(); } setFileInputKey(Date.now()); setFolderInputKey(Date.now()); }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const MenuItem = forwardRef( - ( - { - children, - onClick, - }: { children: ReactNode; onClick: (e: MouseEvent) => void }, - ref - ) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ children, onClick }: { children: ReactNode; onClick: (e: MouseEvent) => void }, ref) => { return (
{children}
); - } + }, ); const spaceRemaining = MAX_BYTES_PER_SEND - fileContext.totalFilesSize; @@ -93,29 +74,20 @@ export default function FileArea({ webkitdirectory="" /> {fileContext.itemList.length === 0 ? ( - + ) : ( <> @@ -132,8 +104,8 @@ export default function FileArea({

Add more items

- {fileContext.totalFilesCount} / {MAX_ITEMS_PER_LINK}{" "} - {fileContext.totalFilesCount > 1 ? "files" : "file"} added + {fileContext.totalFilesCount} / {MAX_ITEMS_PER_LINK}{' '} + {fileContext.totalFilesCount > 1 ? 'files' : 'file'} added

·

{format(spaceRemaining)} remaining

@@ -164,12 +136,7 @@ function Empty({ e.stopPropagation(); }} > - +

Upload files

@@ -183,7 +150,7 @@ function Empty({ ); } -declare module "react" { +declare module 'react' { interface HTMLAttributes extends AriaAttributes, DOMAttributes { // extends React's HTMLAttributes directory?: string; diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 4a80475..f54f7ba 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,10 +1,10 @@ -import { WarningOctagon, Warning, CheckCircle } from "phosphor-react"; -import { JSX } from "react"; +import { WarningOctagon, Warning, CheckCircle } from 'phosphor-react'; +import { JSX } from 'react'; export default function Input({ - className = "", + className = '', label, - type = "text", + type = 'text', accent, disabled, placeholder, @@ -21,8 +21,8 @@ export default function Input({ }: { className?: string; label?: string; - variant?: "text" | "email"; - accent?: "error" | "warning" | "success"; + variant?: 'text' | 'email'; + accent?: 'error' | 'warning' | 'success'; disabled?: boolean; placeholder?: string; value?: string; @@ -33,35 +33,34 @@ export default function Input({ onBlur?: () => void; message?: string; type?: string; - refForInput?: React.RefObject; + refForInput?: React.RefObject; onMouseEnter?: React.MouseEventHandler; onMouseLeave?: React.MouseEventHandler; }): JSX.Element { let focusColor: string; switch (accent) { - case "error": - focusColor = "focus:border-red-std ring-red-std"; + case 'error': + focusColor = 'focus:border-red-std ring-red-std'; break; - case "warning": - focusColor = "focus:border-orange ring-orange"; + case 'warning': + focusColor = 'focus:border-orange ring-orange'; break; - case "success": - focusColor = "focus:border-green ring-green"; + case 'success': + focusColor = 'focus:border-green ring-green'; break; default: - focusColor = "focus:border-primary ring-primary"; + focusColor = 'focus:border-primary ring-primary'; break; } - const borderColor = - "border-gray-20 disabled:border-gray-10 hover:border-gray-30"; + const borderColor = 'border-gray-20 disabled:border-gray-10 hover:border-gray-30'; - const backgroundColor = "bg-white disabled:bg-white"; + const backgroundColor = 'bg-white disabled:bg-white'; - const placeholderColor = "placeholder-gray-30"; + const placeholderColor = 'placeholder-gray-30'; - const padding = "px-3"; + const padding = 'px-3'; const input = (
@@ -70,17 +69,17 @@ export default function Input({ onMouseLeave={onMouseLeave} ref={refForInput} disabled={disabled} - className={`inxt-input h-11 w-full rounded-md border text-lg font-normal text-gray-80 outline-hidden ring-opacity-10 disabled:text-gray-40 disabled:placeholder-gray-20 md:h-9 lg:text-base - ${borderColor} ${focusColor} ${placeholderColor} ${backgroundColor} ${padding}` - } - type={type ?? "text"} + className={`inxt-input h-11 w-full rounded-md border text-lg font-normal text-gray-80 outline-hidden + ring-opacity-10 disabled:text-gray-40 disabled:placeholder-gray-20 md:h-9 lg:text-base + ${borderColor} ${focusColor} ${placeholderColor} ${backgroundColor} ${padding}`} + type={type ?? 'text'} placeholder={placeholder} onChange={(e) => onChange && onChange(e.target.value)} onFocus={() => { - onFocus && onFocus(); + onFocus?.(); }} onBlur={() => { - onBlur && onBlur(); + onBlur?.(); }} onPaste={onPaste} value={value} @@ -93,29 +92,26 @@ export default function Input({ let MessageIcon: typeof WarningOctagon | undefined; switch (accent) { - case "success": - messageColor = "text-green"; + case 'success': + messageColor = 'text-green'; MessageIcon = CheckCircle; break; - case "warning": - messageColor = "text-orange"; + case 'warning': + messageColor = 'text-orange'; MessageIcon = Warning; break; - case "error": - messageColor = "text-red-std"; + case 'error': + messageColor = 'text-red-std'; MessageIcon = WarningOctagon; break; default: - messageColor = "text-gray-80"; + messageColor = 'text-gray-80'; } return (
{label ? ( -
diff --git a/src/components/NotificationToast.tsx b/src/components/NotificationToast.tsx index f66ac40..efa0b06 100644 --- a/src/components/NotificationToast.tsx +++ b/src/components/NotificationToast.tsx @@ -1,7 +1,7 @@ -import { Transition } from "@headlessui/react"; -import { CheckCircle, Info, Warning, WarningOctagon, X } from "phosphor-react"; -import { ToastShowProps, ToastType } from "../services/notifications.service"; -import { JSX } from "react"; +import { Transition } from '@headlessui/react'; +import { CheckCircle, Info, Warning, WarningOctagon, X } from 'phosphor-react'; +import { ToastShowProps, ToastType } from '../services/notifications.service'; +import { JSX } from 'react'; const NotificationToast = ({ text, @@ -10,7 +10,7 @@ const NotificationToast = ({ visible, closable, onClose, -}: Omit & { +}: Omit & { visible: boolean; onClose: () => void; }): JSX.Element => { @@ -20,19 +20,19 @@ const NotificationToast = ({ switch (type) { case ToastType.Success: Icon = CheckCircle; - IconColor = "text-green"; + IconColor = 'text-green'; break; case ToastType.Error: Icon = WarningOctagon; - IconColor = "text-red-50"; + IconColor = 'text-red-50'; break; case ToastType.Info: Icon = Info; - IconColor = "text-primary"; + IconColor = 'text-primary'; break; case ToastType.Warning: Icon = Warning; - IconColor = "text-yellow"; + IconColor = 'text-yellow'; break; } @@ -49,18 +49,13 @@ const NotificationToast = ({ >
- {Icon && ( - - )} + {Icon && }

{text}

{action && ( - )} diff --git a/src/components/RevealX.tsx b/src/components/RevealX.tsx index 39b8912..3e29e7d 100644 --- a/src/components/RevealX.tsx +++ b/src/components/RevealX.tsx @@ -1,17 +1,15 @@ -import React, { useEffect } from "react"; +import React, { useEffect } from 'react'; interface RevealProps { children: React.ReactNode; className?: string; - direction?: "left" | "right"; + direction?: 'left' | 'right'; } const RevealX = ({ children, className, direction }: RevealProps) => { useEffect(() => { function reveal() { - const reveals = document.querySelectorAll( - direction === "left" ? ".revealXLeft" : ".revealXRight" - ); + const reveals = document.querySelectorAll(direction === 'left' ? '.revealXLeft' : '.revealXRight'); for (let i = 0; i < reveals.length; i++) { const windowHeight = window.innerHeight; @@ -19,26 +17,18 @@ const RevealX = ({ children, className, direction }: RevealProps) => { const elementVisible = 150; if (elementTop < windowHeight - elementVisible) { - reveals[i].classList.add("active"); + reveals[i].classList.add('active'); } } } - window.addEventListener("scroll", reveal); + window.addEventListener('scroll', reveal); return () => { - window.removeEventListener("scroll", reveal); + window.removeEventListener('scroll', reveal); }; }, []); - return ( -
- {children} -
- ); + return
{children}
; }; export default RevealX; diff --git a/src/components/RootDropzone.tsx b/src/components/RootDropzone.tsx index 079b4fb..0151512 100644 --- a/src/components/RootDropzone.tsx +++ b/src/components/RootDropzone.tsx @@ -1,23 +1,17 @@ -import { format } from "bytes"; -import { ReactNode, useCallback, useContext } from "react"; -import { useDropzone } from "react-dropzone"; -import { MAX_BYTES_PER_SEND } from "../constants"; -import { FilesContext } from "../contexts/Files"; +import { format } from 'bytes'; +import { ReactNode, useCallback, useContext } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { MAX_BYTES_PER_SEND } from '../constants'; +import { FilesContext } from '../contexts/Files'; -export default function RootDropzone({ - className = "", - children, -}: { - className?: string; - children: ReactNode; -}) { +export default function RootDropzone({ className = '', children }: { className?: string; children: ReactNode }) { const filesContext = useContext(FilesContext); const onDrop = useCallback( (acceptedFiles: File[]) => { filesContext.addFiles(acceptedFiles); }, - [filesContext] + [filesContext], ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ @@ -34,7 +28,10 @@ export default function RootDropzone({ {isDragActive && (
-
+

Drag and drop your files here diff --git a/src/components/SendBanner.tsx b/src/components/SendBanner.tsx index 51246be..ea63e33 100644 --- a/src/components/SendBanner.tsx +++ b/src/components/SendBanner.tsx @@ -1,6 +1,6 @@ -import { X } from "phosphor-react"; -import SendIcon from "../assets/images/share-file/visual.svg?react"; -import BackgroundImage from "../assets/images/share-file/banner-bg.png"; +import { X } from 'phosphor-react'; +import SendIcon from '../assets/images/share-file/visual.svg?react'; +import BackgroundImage from '../assets/images/share-file/banner-bg.png'; export interface Props { sendBannerVisible: boolean; @@ -15,40 +15,36 @@ const SendBanner = (props: Props) => { return (
-
-

- Try out Internxt -

+

Try out Internxt

Encrypt, save and share large files with Internxt.

)} - {state.status === "downloading" && ( + {state.status === 'downloading' && (
- +

Downloading

- {format(state.downloadedBytes)} of {format(state.totalBytes)}{" "} - downloaded + {format(state.downloadedBytes)} of {format(state.totalBytes)} downloaded

)} - {state.status === "done" && ( + {state.status === 'done' && (
-

- Download successful -

+

Download successful

You can now access your files.

)} - {state.status === "error" && ( + {state.status === 'error' && (
@@ -201,16 +168,14 @@ export default function DownloadView() {

Invalid link

- {state.reason === "not_found" - ? "Sorry, this link has expired." - : "Something went wrong..."} + {state.reason === 'not_found' ? 'Sorry, this link has expired.' : 'Something went wrong...'}

)} - + ); } diff --git a/src/views/HomeView.tsx b/src/views/HomeView.tsx index 7a9f1e0..6191741 100644 --- a/src/views/HomeView.tsx +++ b/src/views/HomeView.tsx @@ -1,35 +1,33 @@ -import isValidEmail from "@internxt/lib/dist/src/auth/isValidEmail"; -import { format } from "bytes"; -import copy from "copy-to-clipboard"; -import throttle from "lodash.throttle"; -import { CheckCircle, Copy, WarningCircle, X } from "phosphor-react"; -import { RefObject, useContext, useEffect, useRef, useState } from "react"; -import Button from "../components/Button"; -import CardBottom from "../components/CardBotton"; -import FancySpinner from "../components/FancySpinner"; -import FileArea from "../components/FileArea"; -import Input from "../components/Input"; -import Switch from "../components/Switch"; -import { MAX_RECIPIENTS } from "../constants"; -import { FilesContext } from "../contexts/Files"; -import Layout from "../Layout"; -import notificationsService, { - ToastType, -} from "../services/notifications.service"; -import { UploadService } from "../services/upload.service"; -import * as Sentry from "@sentry/react"; -import { getAllItemsArray } from "../services/items.service"; -import FeatureSection from "../components/send/FeatureSection"; -import InfoSection from "../components/send/InfoSection"; -import FaqSection from "../components/send/FaqSection"; -import Footer from "../components/footer/Footer"; - -import schemaMarkup from "../assets/lang/en/send.json"; -import { sm_faq } from "../components/utils/schema-markup-generator"; -import CtaSection from "../components/send/CtaSection"; -import moment from "moment"; -import Tooltip from "../components/Tooltip"; -import analyticsService from "../services/analytics.service"; +import isValidEmail from '@internxt/lib/dist/src/auth/isValidEmail'; +import { format } from 'bytes'; +import copy from 'copy-to-clipboard'; +import throttle from 'lodash.throttle'; +import { CheckCircle, Copy, WarningCircle, X } from 'phosphor-react'; +import { RefObject, useContext, useEffect, useRef, useState } from 'react'; +import Button from '../components/Button'; +import CardBottom from '../components/CardBotton'; +import FancySpinner from '../components/FancySpinner'; +import FileArea from '../components/FileArea'; +import Input from '../components/Input'; +import Switch from '../components/Switch'; +import { MAX_RECIPIENTS } from '../constants'; +import { FilesContext } from '../contexts/Files'; +import Layout from '../Layout'; +import notificationsService, { ToastType } from '../services/notifications.service'; +import { UploadService } from '../services/upload.service'; +import * as Sentry from '@sentry/react'; +import { getAllItemsArray } from '../services/items.service'; +import FeatureSection from '../components/send/FeatureSection'; +import InfoSection from '../components/send/InfoSection'; +import FaqSection from '../components/send/FaqSection'; +import Footer from '../components/footer/Footer'; + +import schemaMarkup from '../assets/lang/en/send.json'; +import { sm_faq } from '../components/utils/schema-markup-generator'; +import CtaSection from '../components/send/CtaSection'; +import moment from 'moment'; +import Tooltip from '../components/Tooltip'; +import analyticsService from '../services/analytics.service'; type EmailFormState = { sendTo: string[]; @@ -40,44 +38,37 @@ type EmailFormState = { }; export default function HomeView() { - const options = ["Send link", "Send email"] as const; + const options = ['Send link', 'Send email'] as const; const uploadAbortController = useRef(null); - const [switchValue, setSwitchValue] = useState( - options[0] - ); + const [switchValue, setSwitchValue] = useState<(typeof options)[number]>(options[0]); - const formattedDate = moment() - .add(1, "day") - .add(2, "weeks") - .format("MMMM DD[,] YYYY"); + const formattedDate = moment().add(1, 'day').add(2, 'weeks').format('MMMM DD[,] YYYY'); const [phase, setPhase] = useState< - | { name: "standby" } - | { name: "loading" | "confirm_cancel"; uploadedBytes: number } - | { name: "done"; link: string } - | { name: "error" } - >({ name: "standby" }); + | { name: 'standby' } + | { name: 'loading' | 'confirm_cancel'; uploadedBytes: number } + | { name: 'done'; link: string } + | { name: 'error' } + >({ name: 'standby' }); const [formState, setFormState] = useState({ sendTo: [], - sender: "", - title: "", - message: "", - sendToField: "", + sender: '', + title: '', + message: '', + sendToField: '', }); const filesContext = useContext(FilesContext); useEffect(() => { - filesContext.setEnabled(phase.name === "standby"); + filesContext.setEnabled(phase.name === 'standby'); }, [phase, filesContext]); const disableButton = filesContext.totalFilesCount === 0 || - (switchValue === "Send email" && - ((formState.sendTo.length === 0 && - !isValidEmail(formState.sendToField)) || - !isValidEmail(formState.sender))); + (switchValue === 'Send email' && + ((formState.sendTo.length === 0 && !isValidEmail(formState.sendToField)) || !isValidEmail(formState.sender))); async function uploadFiles(cb: (progress: number) => void): Promise { const abortController = new AbortController(); @@ -90,13 +81,10 @@ export default function HomeView() { const link = await UploadService.uploadFilesAndGetLink( items, - switchValue === "Send email" + switchValue === 'Send email' ? { sender: formState.sender, - receivers: - formState.sendTo.length === 0 - ? [formState.sendToField] - : formState.sendTo, + receivers: formState.sendTo.length === 0 ? [formState.sendToField] : formState.sendTo, subject: formState.message, title: formState.title, } @@ -104,31 +92,32 @@ export default function HomeView() { { progress: (_, uploadedBytes) => cb(uploadedBytes), abortController, - } + }, ); return link; } catch (error) { const err = error as Error; + // eslint-disable-next-line no-console console.error(err); throw err; } } function cancelUpload() { - if (phase.name !== "loading") { + if (phase.name !== 'loading') { return; } setPhase({ - name: "confirm_cancel", + name: 'confirm_cancel', uploadedBytes: phase.uploadedBytes, }); } async function onSubmit() { - const isEmailOption = switchValue === "Send email"; - const trackName = isEmailOption ? "Email Sent" : "Link Created"; + const isEmailOption = switchValue === 'Send email'; + const trackName = isEmailOption ? 'Email Sent' : 'Link Created'; const emailData = isEmailOption ? { @@ -136,18 +125,17 @@ export default function HomeView() { } : undefined; - setPhase({ name: "loading", uploadedBytes: 0 }); + setPhase({ name: 'loading', uploadedBytes: 0 }); try { const link = await uploadFiles((uploadedBytes) => { setPhase((phase) => { - if (phase.name === "loading" || phase.name === "confirm_cancel") - return { name: phase.name, uploadedBytes }; + if (phase.name === 'loading' || phase.name === 'confirm_cancel') return { name: phase.name, uploadedBytes }; else return phase; }); }); - setPhase({ name: "done", link }); + setPhase({ name: 'done', link }); - window.gtag("event", trackName); + window.gtag('event', trackName); analyticsService.track(trackName, { totalFilesCount: filesContext.totalFilesCount, @@ -155,56 +143,46 @@ export default function HomeView() { ...emailData, }); } catch (err) { + // eslint-disable-next-line no-console console.error(err); if (!uploadAbortController.current?.signal.aborted) { - setPhase({ name: "error" }); + setPhase({ name: 'error' }); Sentry.captureException(err); } } } function copyLink() { - if (phase.name === "done") { + if (phase.name === 'done') { copy(phase.link); notificationsService.show({ type: ToastType.Success, - text: "Link copied to your clipboard", + text: 'Link copied to your clipboard', }); } } const copyLinkThrottled = throttle(copyLink, 5000); - const titleRef = useRef(null); + const titleRef = useRef(null); const [showTooltip, setShowTooltip] = useState(false); - const [titleInputPosition, setTitleInputPosition] = useState( - titleRef.current?.getBoundingClientRect() - ); + const [titleInputPosition, setTitleInputPosition] = useState(titleRef.current?.getBoundingClientRect()); return (
- {phase.name === "standby" && ( + {phase.name === 'standby' && ( <>
{ - setTitleInputPosition( - titleRef.current?.getBoundingClientRect() - ); + setTitleInputPosition(titleRef.current?.getBoundingClientRect()); }} > - - {switchValue === "Send email" && ( + + {switchValue === 'Send email' && (
- -
)} - {(phase.name === "loading" || phase.name === "confirm_cancel") && ( + {(phase.name === 'loading' || phase.name === 'confirm_cancel') && (
- {phase.name === "loading" ? ( + {phase.name === 'loading' ? ( <>

- {switchValue === "Send email" ? "Sending" : "Uploading"}{" "} - {filesContext.totalFilesCount}{" "} - {filesContext.totalFilesCount > 1 ? "files" : "file"} + {switchValue === 'Send email' ? 'Sending' : 'Uploading'} {filesContext.totalFilesCount}{' '} + {filesContext.totalFilesCount > 1 ? 'files' : 'file'}

- {format(phase.uploadedBytes)} of{" "} - {format(filesContext.totalFilesSize)} uploaded + {format(phase.uploadedBytes)} of {format(filesContext.totalFilesSize)} uploaded

) : ( -

- Are you sure you want to cancel this upload? -

+

Are you sure you want to cancel this upload?

)}
- {phase.name === "loading" ? ( + {phase.name === 'loading' ? ( @@ -290,7 +251,7 @@ export default function HomeView() { outline onClick={() => setPhase({ - name: "loading", + name: 'loading', uploadedBytes: phase.uploadedBytes, }) } @@ -301,7 +262,7 @@ export default function HomeView() { className="ml-4" onClick={() => { uploadAbortController.current?.abort(); - setPhase({ name: "standby" }); + setPhase({ name: 'standby' }); }} > Yes @@ -311,7 +272,7 @@ export default function HomeView() {
)} - {phase.name === "done" && ( + {phase.name === 'done' && (
@@ -319,18 +280,18 @@ export default function HomeView() {

- {switchValue === "Send email" - ? "Files successfully sent via email" + {switchValue === 'Send email' + ? 'Files successfully sent via email' : `${filesContext.totalFilesCount} ${ - filesContext.totalFilesCount > 1 ? "files" : "file" + filesContext.totalFilesCount > 1 ? 'files' : 'file' } uploaded successfully`}

- {switchValue === "Send email" + {switchValue === 'Send email' ? `Access will expire on ${formattedDate}` : `This link will expire on ${formattedDate}`}

- {switchValue === "Send link" && ( + {switchValue === 'Send link' && (
)} - {phase.name === "error" && ( + {phase.name === 'error' && (
-

- Something went wrong... -

+

Something went wrong...

- We were unable to{" "} - {switchValue === "Send email" ? "send" : "upload"} your files. + We were unable to {switchValue === 'Send email' ? 'send' : 'upload'} your files.
Please try again later.

@@ -387,7 +345,7 @@ export default function HomeView() { outline onClick={() => setPhase({ - name: "standby", + name: 'standby', }) } > @@ -423,16 +381,13 @@ function EmailForm({ value: EmailFormState; onChange: (v: EmailFormState) => void; setShowTooltip: (v: boolean) => void; - inputTitleRef: RefObject; + inputTitleRef: RefObject; }) { const { sendTo, sendToField } = value; return (
- onChange({ ...value, ...v })} - /> + onChange({ ...value, ...v })} />
onChange({ ...value, sender: v })} value={value.sender} onKeyDown={(e) => { - if (e.key === " ") e.preventDefault(); + if (e.key === ' ') e.preventDefault(); }} />
-
+

Transfer info (Optional) @@ -462,8 +415,9 @@ function EmailForm({ />