├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── assets
├── download.gif
├── thumbnail.svg
└── upload.gif
├── index.html
├── package.json
├── public
└── icons
│ ├── icon-128x128.png
│ ├── icon-144x144.png
│ ├── icon-152x152.png
│ ├── icon-192x192.png
│ ├── icon-384x384.png
│ ├── icon-512x512.png
│ ├── icon-72x72.png
│ └── icon-96x96.png
├── src
├── App.tsx
├── ViewController.tsx
├── assets
│ ├── apple-touch-icon.png
│ └── favicon.png
├── components
│ ├── Footer.tsx
│ ├── Hero.tsx
│ ├── HostedFileItem.tsx
│ ├── LoaderScreen.tsx
│ ├── Navbar.tsx
│ └── icons
│ │ ├── apple.tsx
│ │ ├── download.tsx
│ │ ├── file-magnifying-glass.tsx
│ │ ├── github.tsx
│ │ ├── google.tsx
│ │ ├── heart.tsx
│ │ ├── logo-color.tsx
│ │ ├── logo-white.tsx
│ │ ├── moon-stars.tsx
│ │ ├── server.tsx
│ │ ├── sun.tsx
│ │ ├── upload.tsx
│ │ └── user.tsx
├── main.tsx
├── preact.d.ts
├── services
│ ├── auth.ts
│ ├── error-handling.ts
│ ├── file.ts
│ ├── firebase.ts
│ └── user-data.ts
├── util
│ ├── base.css
│ ├── file-info.ts
│ ├── firebase-config.ts
│ ├── theme.ts
│ └── toast.ts
├── views
│ ├── auth.tsx
│ ├── download.tsx
│ ├── file.tsx
│ ├── server.tsx
│ └── upload.tsx
└── vite-env.d.ts
├── tsconfig.json
├── vite.config.ts
├── webmanifest.config.ts
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ['https://www.buymeacoffee.com/colegawin']
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | .idea
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": true,
4 | "embeddedLanguageFormatting": "auto",
5 | "htmlWhitespaceSensitivity": "css",
6 | "insertPragma": false,
7 | "jsxBracketSameLine": false,
8 | "jsxSingleQuote": false,
9 | "printWidth": 120,
10 | "proseWrap": "always",
11 | "quoteProps": "as-needed",
12 | "requirePragma": false,
13 | "semi": true,
14 | "singleQuote": false,
15 | "tabWidth": 2,
16 | "trailingComma": "es5",
17 | "useTabs": false,
18 | "vueIndentScriptAndStyle": false,
19 | "importOrder": [
20 | "(^(preact\\/))(.*)",
21 | "^[^~.]",
22 | "^(~|[.])(.*)$",
23 | "^[./]"
24 | ],
25 | "importOrderSeparation": true
26 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Cole Gawin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | # ⚡️ LIGHTNING SHARE
31 |
32 | **a project created by [Cole Gawin](https://github.com/chroline)**
33 |
34 |
35 |
36 | ### [Check it out](https://lightning-share.vercel.app) | [Learn how it's made](/#) | [Support the project](https://github.com/chroline/lightning-share#%EF%B8%8F-support-this-project)
37 |
38 |
39 |
40 |
41 |
42 | ---
43 |
44 |
45 |
46 | # 👋 Introduction
47 |
48 | LIGHTNING SHARE is a file hosting and sharing service powered by [Firebase](https://firebase.google.com). Users can upload files under 20MB and download hosted files using a "share code" (a short 3 word code provided by [words-aas](https://github.com/chroline/words-aas)). After uploading a file, the file uploader can retrieve the share code and share it with others or delete the file at any time. Files will automatically be deleted after 21 days (WIP).
49 |
50 | # 🚀 Usage
51 |
52 | |**Upload** | **Download** |
53 | |---|---|
54 | |||
55 |
56 | # 🧑💻 Development
57 |
58 | 1. 📂 Clone this repo
59 |
60 | 2. 📦 Install dependencies with `yarn`
61 |
62 | 3. 🏃 Start the Vite dev server with `yarn dev`
63 |
64 | 4. 🌎 Visit the provided link in your browser
65 |
66 | ## Firebase
67 |
68 | Firebase is used to power the authentication, database, and file storage for LIGHTNING SHARE. For security reasons, the production LIGHTING SHARE Firebase project is not available for local development on `localhost`. As such, you will need to create your own Firebase project for local development purposes.
69 |
70 | On your development Firebase project:
71 |
72 | 1. Enable anonymous authentication.
73 | 2. Ensure `localhost` is an authorized domain for authentication.
74 | 3. Enable Firestore Database and Storage features.
75 |
76 | Replace the [`firebaseConfig` variable](https://github.com/chroline/lightning-share/blob/main/src/util/firebase-config.ts#L1) in [`src/util/firebase-config.ts`](https://github.com/chroline/lightning-share/blob/main/src/util/firebase-config.ts) with the config for your Firebase project.
77 |
78 | # ❤️ Support this project
79 |
80 | If you want to say thank you and/or support active development of LIGHTNING SHARE:
81 |
82 | - Add a GitHub Star to the project!
83 | - Tweet about the project on your Twitter!
84 | - Tag [@colegawin_](https://twitter.com/colegawin_) and mention "LIGHTNING SHARE"
85 | - Leave a comment or a reaction on the tutorial of how this project was built!
86 |
87 | Thanks so much for your interest in supporting LIGHTNING SHARE!
88 |
89 | _**PS:** consider sponsoring me ([Cole Gawin](https://colegaw.in)) to continue the development of this project on [BuyMeACoffee](https://buymeacoffee.com/colegawin) :)_
90 |
--------------------------------------------------------------------------------
/assets/download.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/assets/download.gif
--------------------------------------------------------------------------------
/assets/thumbnail.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/assets/upload.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/assets/upload.gif
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | LIGHTNING SHARE
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lighting-share-vite",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc && vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "@chakra-ui/react": "^1.6.5",
11 | "@emotion/react": "^11.4.0",
12 | "@emotion/styled": "^11.3.0",
13 | "@heroicons/react": "^2.0.15",
14 | "constate": "^3.3.0",
15 | "filepond": "^4.28.2",
16 | "filepond-plugin-image-preview": "^4.6.11",
17 | "firebase": "9.0.0-beta.7",
18 | "framer-motion": "^4.1.17",
19 | "preact": "^10.5.13",
20 | "react-filepond": "^7.1.1",
21 | "react-helmet": "^6.1.0",
22 | "react-use": "^17.2.4",
23 | "rxjs": "^7.3.0"
24 | },
25 | "devDependencies": {
26 | "@preact/preset-vite": "^2.5.0",
27 | "@trivago/prettier-plugin-sort-imports": "^2.0.2",
28 | "@types/react-helmet": "^6.1.2",
29 | "prettier": "^2.3.2",
30 | "tslib": "^2.3.0",
31 | "typescript": "^4.3.2",
32 | "vite": "^4.1.2",
33 | "vite-plugin-pwa": "^0.14.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-128x128.png
--------------------------------------------------------------------------------
/public/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-144x144.png
--------------------------------------------------------------------------------
/public/icons/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-152x152.png
--------------------------------------------------------------------------------
/public/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-192x192.png
--------------------------------------------------------------------------------
/public/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-384x384.png
--------------------------------------------------------------------------------
/public/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-512x512.png
--------------------------------------------------------------------------------
/public/icons/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-72x72.png
--------------------------------------------------------------------------------
/public/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/public/icons/icon-96x96.png
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { ChakraProvider, ColorModeScript, extendTheme, VStack } from "@chakra-ui/react";
2 | import "filepond/dist/filepond.min.css";
3 | import { Helmet } from "react-helmet";
4 | import { useAsync, useObservable } from "react-use";
5 |
6 | import { ViewController, ViewControllerProvider } from "./ViewController";
7 | import { Footer } from "./components/Footer";
8 | import { Hero } from "./components/Hero";
9 | import { LoaderScreen } from "./components/LoaderScreen";
10 | import { Navbar } from "./components/Navbar";
11 | import AuthService from "./services/auth";
12 | import ErrorHandlingService from "./services/error-handling";
13 | import FirebaseService from "./services/firebase";
14 | import "./util/base.css";
15 | import theme from "./util/theme";
16 |
17 | function App() {
18 | const setupState = useAsync(async () => {
19 | try {
20 | await FirebaseService.I.init();
21 | } catch (e) {
22 | ErrorHandlingService.I.notifyUserOfError("Error while initializing Firebase", e);
23 | return;
24 | }
25 | try {
26 | await AuthService.I.authenticate();
27 | } catch (e) {
28 | ErrorHandlingService.I.notifyUserOfError("Error during anonymous authorization", e);
29 | return;
30 | }
31 | });
32 |
33 | const userId = useObservable(AuthService.I.userId$);
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 | {(setupState.loading || setupState.error) && }
42 |
43 |
44 |
45 |
46 |
47 | {!!userId && }
48 |
49 |
50 | );
51 | }
52 |
53 | export default App;
54 |
--------------------------------------------------------------------------------
/src/ViewController.tsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from "preact/compat";
2 | import { StateUpdater, useState } from "preact/hooks";
3 |
4 | import constate from "constate";
5 | import React from "react";
6 |
7 | type View = {
8 | slug: null | "upload" | "download" | "file" | "server" | "auth";
9 | params?: Record;
10 | };
11 |
12 | export const [ViewControllerProvider, useViewController] = constate(() => {
13 | const [view, setView] = useState({ slug: null, params: {} });
14 | return [view, setView] as [View, StateUpdater];
15 | });
16 |
17 | const UploadView = lazy(() => import("./views/upload"));
18 | const DownloadView = lazy(() => import("./views/download"));
19 | const FileView = lazy(() => import("./views/file"));
20 | const ServerView = lazy(() => import("./views/server"));
21 | const AuthView = lazy(() => import("./views/auth"));
22 |
23 | export const ViewController = () => {
24 | const [view, setView] = useViewController();
25 |
26 | const onClose = () => setView(view => ({ ...view, slug: null }));
27 |
28 | return (
29 | /* @ts-ignore */
30 | >}>
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/assets/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/src/assets/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/src/assets/favicon.png
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Box, HStack, Link, Stack, Text } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import { HeartIcon } from "./icons/heart";
5 |
6 | export const Footer = () => (
7 |
17 |
18 | made with
19 |
20 |
21 |
22 |
23 | by{" "}
24 |
25 |
26 | Cole Gawin
27 |
28 |
29 |
30 |
31 |
32 | •
33 |
34 |
40 | Read about the project
41 |
42 |
43 | );
44 |
--------------------------------------------------------------------------------
/src/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Flex, Heading, Stack, Text, useColorMode, VStack } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import { useViewController } from "../ViewController";
5 | import { DownloadIcon } from "./icons/download";
6 | import { UploadIcon } from "./icons/upload";
7 |
8 | export const Hero: React.FC = () => {
9 | const [, setView] = useViewController();
10 |
11 | const { colorMode } = useColorMode();
12 |
13 | return (
14 |
15 |
16 |
17 |
18 | LIGHTNING SHARE
19 |
20 |
21 | send files to other devices,{" "}
22 |
23 | lightning fast.
24 |
25 |
26 |
27 |
33 | }
34 | colorScheme={{ light: "blueGray", dark: "gray" }[colorMode]}
35 | shadow={"md"}
36 | onClick={() => setView({ slug: "upload" })}
37 | >
38 | Share new file
39 |
40 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/components/HostedFileItem.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Input, Stack, Text } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import { useViewController } from "../ViewController";
5 |
6 | export const HostedFileItem: React.FC<{ wordCode: string; filename: string }> = ({ wordCode, filename }) => {
7 | const [, setView] = useViewController();
8 |
9 | return (
10 |
11 |
12 |
13 | File
14 |
15 |
16 |
17 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/LoaderScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Flex, CircularProgress } from "@chakra-ui/react";
4 |
5 | export const LoaderScreen = () => (
6 |
17 |
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Heading, HStack, IconButton, useColorMode } from "@chakra-ui/react";
2 |
3 | import { useViewController } from "../ViewController";
4 | import { LogoColor } from "./icons/logo-color";
5 | import { LogoWhite } from "./icons/logo-white";
6 | import { MoonStarsIcon } from "./icons/moon-stars";
7 | import { ServerIcon } from "./icons/server";
8 | import { SunIcon } from "./icons/sun";
9 | import { UserIcon } from "./icons/user";
10 |
11 | export const Navbar = () => {
12 | const [, setView] = useViewController();
13 |
14 | const { colorMode, toggleColorMode } = useColorMode();
15 |
16 | return (
17 |
18 |
19 |
23 |
24 |
25 | }
26 | onClick={() => setView({ slug: "server" })}
27 | />
28 |
32 |
33 |
34 | }
35 | onClick={() => setView({ slug: "auth" })}
36 | />
37 |
38 |
39 | {colorMode === "dark" ? : }
40 |
41 | LIGHTNING SHARE
42 |
43 |
44 |
45 |
49 | {colorMode !== "dark" ? : }
50 |
51 | }
52 | onClick={toggleColorMode}
53 | />
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/components/icons/apple.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const AppleIcon = (props: ChakraProps) => (
5 |
15 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/src/components/icons/download.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const DownloadIcon = (props: ChakraProps) => (
5 |
15 |
20 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/icons/file-magnifying-glass.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const FileMagnifyingGlassIcon = (props: ChakraProps) => (
5 |
15 |
20 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/icons/github.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const GithubIcon = (props: ChakraProps) => (
5 |
15 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/src/components/icons/google.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const GoogleIcon = (props: ChakraProps) => (
5 |
15 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/src/components/icons/heart.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const HeartIcon = (props: ChakraProps) => (
5 |
15 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/src/components/icons/logo-color.tsx:
--------------------------------------------------------------------------------
1 | export const LogoColor = () => (
2 |
25 | );
26 |
--------------------------------------------------------------------------------
/src/components/icons/logo-white.tsx:
--------------------------------------------------------------------------------
1 | export const LogoWhite = () => (
2 |
25 | );
26 |
--------------------------------------------------------------------------------
/src/components/icons/moon-stars.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const MoonStarsIcon = (props: ChakraProps) => (
5 |
15 |
20 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/icons/server.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const ServerIcon = (props: ChakraProps) => (
5 |
15 |
20 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/icons/sun.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const SunIcon = (props: ChakraProps) => (
5 |
15 |
20 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/icons/upload.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const UploadIcon = (props: ChakraProps) => (
5 |
15 |
20 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/icons/user.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@chakra-ui/react";
2 | import { ChakraProps } from "@chakra-ui/system/dist/types/system.types";
3 |
4 | export const UserIcon = (props: ChakraProps) => (
5 |
15 |
20 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "preact";
2 | import { registerSW } from "virtual:pwa-register";
3 |
4 | import App from "./App";
5 |
6 | registerSW({
7 | onRegistered() {
8 | console.info("[SW]:", "Registered!");
9 | },
10 | });
11 |
12 | render(, document.getElementById("app")!);
13 |
--------------------------------------------------------------------------------
/src/preact.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chroline/lightning-share/8f5a6b90e0c991d9ff5812a99efe21b43d03d64b/src/preact.d.ts
--------------------------------------------------------------------------------
/src/services/auth.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getAdditionalUserInfo,
3 | getAuth,
4 | getRedirectResult,
5 | GithubAuthProvider,
6 | GoogleAuthProvider,
7 | OAuthProvider,
8 | onAuthStateChanged,
9 | signInAnonymously,
10 | signInWithRedirect,
11 | signOut,
12 | User,
13 | } from "firebase/auth";
14 | import { BehaviorSubject } from "rxjs";
15 |
16 | import toast from "../util/toast";
17 | import UserDataService from "./user-data";
18 |
19 | const providers = {
20 | google: new GoogleAuthProvider(),
21 | github: new GithubAuthProvider(),
22 | apple: new OAuthProvider("apple.com"),
23 | };
24 |
25 | providers.apple.addScope("email");
26 | providers.apple.addScope("name");
27 |
28 | class AuthService {
29 | private static _I = new AuthService();
30 | public static get I() {
31 | return this._I;
32 | }
33 |
34 | /**
35 | * User data
36 | */
37 | private _userInfo$ = new BehaviorSubject(null);
38 | public get userInfo$() {
39 | return this._userInfo$;
40 | }
41 |
42 | /**
43 | * ID of user
44 | */
45 | private _userId$ = new BehaviorSubject(null);
46 | public get userId$() {
47 | return this._userId$;
48 | }
49 |
50 | /**
51 | * Anonymously authenticate with Firebase Authentication.
52 | *
53 | * - Signs in with anonymous auth provider.
54 | * - Updates {@link userId$} subject.
55 | * - Seed user data doc.
56 | * - Watches for auth changes and re-executes auth process.
57 | */
58 | async authenticate() {
59 | const auth = getAuth();
60 |
61 | onAuthStateChanged(auth, async user => {
62 | this.userInfo$.next(user);
63 | let userId = user?.uid || null;
64 | if (!user || !user.uid) {
65 | const { user } = await signInAnonymously(auth);
66 | userId = user.uid;
67 | this.userId$.next(userId);
68 | await UserDataService.I.seed();
69 | } else {
70 | const prevUserId = this.userId$.value;
71 | this.userId$.next(userId);
72 |
73 | try {
74 | const redirectResult = await getRedirectResult(auth);
75 | if (redirectResult) {
76 | const addlUserInfo = getAdditionalUserInfo(redirectResult);
77 |
78 | if (addlUserInfo?.isNewUser) {
79 | await UserDataService.I.seed();
80 | }
81 |
82 | !prevUserId &&
83 | toast({
84 | title: "Successfully signed in!",
85 | status: "success",
86 | isClosable: true,
87 | });
88 | }
89 | } catch (e) {
90 | if (e.code === "auth/account-exists-with-different-credential") {
91 | toast({
92 | title: "Whoops! You already have an account.",
93 | description: "Try signing in with the original method you used to sign up.",
94 | status: "error",
95 | isClosable: true,
96 | });
97 | } else {
98 | toast({
99 | title: "Error signing in",
100 | description: e.message,
101 | status: "error",
102 | isClosable: true,
103 | });
104 | }
105 | }
106 | }
107 | });
108 | }
109 |
110 | /**
111 | * Sign in using Google with given provider using redirect strategy
112 | *
113 | * @param {string} provider - provider used to sign in
114 | */
115 | async signInWithProvider(provider: keyof typeof providers) {
116 | const auth = getAuth();
117 | signInWithRedirect(auth, providers[provider]);
118 | }
119 |
120 | /**
121 | * Signs user out
122 | */
123 | async signOut() {
124 | const auth = getAuth();
125 | await signOut(auth);
126 | }
127 | }
128 |
129 | export default AuthService;
130 |
--------------------------------------------------------------------------------
/src/services/error-handling.ts:
--------------------------------------------------------------------------------
1 | import toast from "../util/toast";
2 |
3 | class ErrorHandlingService {
4 | private static _I = new ErrorHandlingService();
5 | public static get I() {
6 | return this._I;
7 | }
8 |
9 | /**
10 | * Notify the user of an unexpected error that occurred.
11 | *
12 | * @param {string} title - title of error that occurred
13 | * @param {Error} error - error that occurred
14 | */
15 | notifyUserOfError(title: string, error: Error) {
16 | console.error(title, error);
17 | toast({
18 | title,
19 | description: error.message || error.name,
20 | status: "error",
21 | duration: undefined,
22 | isClosable: true,
23 | });
24 | }
25 | }
26 |
27 | export default ErrorHandlingService;
28 |
--------------------------------------------------------------------------------
/src/services/file.ts:
--------------------------------------------------------------------------------
1 | import {
2 | arrayRemove,
3 | arrayUnion,
4 | collection,
5 | deleteDoc,
6 | doc,
7 | getDoc,
8 | getFirestore,
9 | setDoc,
10 | updateDoc,
11 | } from "firebase/firestore/lite";
12 | import { deleteObject, getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
13 |
14 | import { FileInfo } from "../util/file-info";
15 | import AuthService from "./auth";
16 |
17 | class FileService {
18 | private static _I = new FileService();
19 | public static get I() {
20 | return this._I;
21 | }
22 | /**
23 | * Upload file.
24 | *
25 | * - Generates word code for file.
26 | * - Adds file document to "files" collection with file metadata.
27 | * - Uploads file to Firebase storage.
28 | * - Adds word code to "files" property in user data.
29 | * - Returns the generated word code for file.
30 | *
31 | * @param {File} file - file to be uploaded
32 | * @returns {string} generated word code for file
33 | */
34 | async upload(file: File): Promise {
35 | const uploadDate = new Date(),
36 | uid = AuthService.I.userId$.value!,
37 | firestore = getFirestore();
38 |
39 | async function generateWordCode(): Promise {
40 | const { phrase } = await fetch("https://words-aas.vercel.app/api/a $adjective $noun").then(async v => v.json());
41 | return (await getDoc(doc(collection(firestore, "files"), phrase))).exists() ? await generateWordCode() : phrase;
42 | }
43 |
44 | const wordCode = await generateWordCode();
45 |
46 | await setDoc(doc(collection(firestore, "files"), wordCode), {
47 | name: file.name,
48 | filetype: file.type,
49 | uploadDate,
50 | owner: uid,
51 | } as FileInfo);
52 |
53 | await uploadBytes(ref(getStorage(), `${wordCode}/${file.name}`), file);
54 |
55 | await updateDoc(doc(collection(firestore, "users"), uid), {
56 | files: arrayUnion(wordCode),
57 | });
58 |
59 | return wordCode;
60 | }
61 |
62 | /**
63 | * Download file.
64 | *
65 | * - Gets download URL of file.
66 | * - Opens download URL in new window.
67 | *
68 | * @param {string} wordCode - word code of the file
69 | * @param {string} filename - name of file to be downloaded
70 | */
71 | async download(wordCode: string, filename: string) {
72 | const url = await getDownloadURL(ref(getStorage(), `${wordCode}/${filename}`));
73 | const file = await fetch(url).then(f => f.blob());
74 | const a = document.createElement("a");
75 | a.href = window.URL.createObjectURL(file);
76 | a.download = filename;
77 | document.body.appendChild(a);
78 | a.click();
79 | window.URL.revokeObjectURL(url);
80 | a.remove();
81 | }
82 |
83 | /**
84 | * Check if file exists.
85 | *
86 | * @param {string} wordCode - word code of the file
87 | */
88 | async doesExist(wordCode: string): Promise {
89 | return await getDoc(doc(collection(getFirestore(), "files"), wordCode)).then(v => v.exists());
90 | }
91 |
92 | /**
93 | * Get info of file.
94 | *
95 | * - Loads file data
96 | * - Throws an error if the file data doesn't exist.
97 | * - Transforms Firebase Timestamp datatype into native JavasSript {@link Date}.
98 | * - Returns file data.
99 | *
100 | * @param {string} wordCode - word code of the file
101 | * @returns {FileInfo} file information data
102 | */
103 | async getInfo(wordCode: string): Promise {
104 | let fileData = await getDoc(doc(collection(getFirestore(), "files"), wordCode)).then(v => v.data());
105 | if (!fileData) throw Error("Could not retrieve file metadata");
106 | fileData.uploadDate = (fileData.uploadDate as import("@firebase/firestore").Timestamp).toDate();
107 | return fileData as FileInfo;
108 | }
109 |
110 | /**
111 | * Remove file.
112 | *
113 | * - Deletes file from storage.
114 | * - Removes word code from files property in user data.
115 | * - Removes file document from files collection.
116 | *
117 | * @param {string} wordCode - word code of the file
118 | * @param {string} filename - name of file to be downloaded
119 | */
120 | async remove(wordCode: string, filename: string) {
121 | await deleteObject(ref(getStorage(), `${wordCode}/${filename}`));
122 |
123 | await updateDoc(doc(collection(getFirestore(), "users"), AuthService.I.userId$.value!), {
124 | files: arrayRemove(wordCode),
125 | });
126 |
127 | await deleteDoc(doc(collection(getFirestore(), "files"), wordCode));
128 | }
129 | }
130 |
131 | export default FileService;
132 |
--------------------------------------------------------------------------------
/src/services/firebase.ts:
--------------------------------------------------------------------------------
1 | import { initializeApp } from "firebase/app";
2 |
3 | import firebaseConfig from "../util/firebase-config";
4 |
5 | class FirebaseService {
6 | private static _I = new FirebaseService();
7 | public static get I() {
8 | return this._I;
9 | }
10 |
11 | /**
12 | * Firebase application reference
13 | */
14 | /**
15 | * Firebase application reference
16 | */
17 | private _app!: import("@firebase/app").FirebaseApp;
18 |
19 | /**
20 | * Initializes Firebase using {@link firebaseConfig}
21 | */
22 | async init() {
23 | this._app = initializeApp(firebaseConfig);
24 | }
25 | }
26 |
27 | export default FirebaseService;
28 |
--------------------------------------------------------------------------------
/src/services/user-data.ts:
--------------------------------------------------------------------------------
1 | import { collection, doc, getDoc, getFirestore, setDoc } from "firebase/firestore/lite";
2 |
3 | import AuthService from "./auth";
4 |
5 | class UserDataService {
6 | private static _I = new UserDataService();
7 | public static get I() {
8 | return this._I;
9 | }
10 |
11 | /**
12 | * Seed user document with files field.
13 | */
14 | async seed() {
15 | const firestore = getFirestore();
16 | await setDoc(doc(collection(firestore, "users"), AuthService.I.userId$.value!), { files: [] }, { merge: true });
17 | }
18 |
19 | /**
20 | * Get list of hosted files uploaded by current user.
21 | *
22 | * @returns {Promise} list of word codes of hosted files
23 | */
24 | async getHostedFiles(): Promise {
25 | const firestore = getFirestore();
26 | const userDocRef = doc(collection(firestore, "users"), AuthService.I.userId$.value!);
27 | return (await getDoc(userDocRef)).get("files") as string[];
28 | }
29 | }
30 |
31 | export default UserDataService;
32 |
--------------------------------------------------------------------------------
/src/util/base.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap");
2 | @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500&display=swap");
3 |
4 | html,
5 | body,
6 | #app {
7 | font-family: "IBM Plex Sans", sans-serif;
8 | -webkit-font-smoothing: subpixel-antialiased;
9 | margin: 0;
10 | height: 100%;
11 | width: 100%;
12 | overflow: visible;
13 | }
14 |
15 | *:not(.chakra-portal *) {
16 | box-sizing: border-box;
17 | /* transition: all 0.2s ease; */
18 | }
19 |
20 | .filepond--wrapper input {
21 | display: none;
22 | }
23 |
24 | .filepond--root {
25 | font-family: inherit !important;
26 | }
--------------------------------------------------------------------------------
/src/util/file-info.ts:
--------------------------------------------------------------------------------
1 | export interface FileInfo {
2 | name: string;
3 | filetype: string;
4 | uploadDate: Date;
5 | owner: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/util/firebase-config.ts:
--------------------------------------------------------------------------------
1 | const firebaseConfig = {
2 | apiKey: "AIzaSyAbmNCiyt-fPkZ0B44kvDMQbP3txnpOyHw",
3 | authDomain: "lightningshare-app.firebaseapp.com",
4 | projectId: "lightningshare-app",
5 | storageBucket: "lightningshare-app.appspot.com",
6 | messagingSenderId: "284597221778",
7 | appId: "1:284597221778:web:b71c6c144639b6cf73e94b"
8 | }
9 |
10 | export default firebaseConfig;
11 |
--------------------------------------------------------------------------------
/src/util/theme.ts:
--------------------------------------------------------------------------------
1 | import { ThemeConfig } from "@chakra-ui/react";
2 | import { createBreakpoints, mode } from "@chakra-ui/theme-tools";
3 |
4 | const fonts = {
5 | body: `'IBM Plex Sans', sans-serif`,
6 | heading: `'Space Grotesk', monospace`,
7 | mono: `'Menlo', monospace`,
8 | };
9 |
10 | const breakpoints = createBreakpoints({
11 | sm: "640px",
12 | md: "768px",
13 | lg: "1024px",
14 | xl: "1280px",
15 | "2xl": "1536px",
16 | });
17 |
18 | const colors = {
19 | black: "#000",
20 | white: "#fff",
21 | whiteTheme: {
22 | 50: "rgba(255,255,255,0.25)",
23 | 100: "#fff",
24 | 200: "#fff",
25 | 300: "#fff",
26 | 400: "#fff",
27 | 500: "#fff",
28 | 600: "#fff",
29 | 700: "#fff",
30 | 800: "#fff",
31 | 900: "#fff",
32 | },
33 | rose: {
34 | 50: "#fff1f2",
35 | 100: "#ffe4e6",
36 | 200: "#fecdd3",
37 | 300: "#fda4af",
38 | 400: "#fb7185",
39 | 500: "#f43f5e",
40 | 600: "#e11d48",
41 | 700: "#be123c",
42 | 800: "#9f1239",
43 | 900: "#881337",
44 | },
45 | pink: {
46 | 50: "#fdf2f8",
47 | 100: "#fce7f3",
48 | 200: "#fbcfe8",
49 | 300: "#f9a8d4",
50 | 400: "#f472b6",
51 | 500: "#ec4899",
52 | 600: "#db2777",
53 | 700: "#be185d",
54 | 800: "#9d174d",
55 | 900: "#831843",
56 | },
57 | fuchsia: {
58 | 50: "#fdf4ff",
59 | 100: "#fae8ff",
60 | 200: "#f5d0fe",
61 | 300: "#f0abfc",
62 | 400: "#e879f9",
63 | 500: "#d946ef",
64 | 600: "#c026d3",
65 | 700: "#a21caf",
66 | 800: "#86198f",
67 | 900: "#701a75",
68 | },
69 | purple: {
70 | 50: "#faf5ff",
71 | 100: "#f3e8ff",
72 | 200: "#e9d5ff",
73 | 300: "#d8b4fe",
74 | 400: "#c084fc",
75 | 500: "#a855f7",
76 | 600: "#9333ea",
77 | 700: "#7e22ce",
78 | 800: "#6b21a8",
79 | 900: "#581c87",
80 | },
81 | violet: {
82 | 50: "#f5f3ff",
83 | 100: "#ede9fe",
84 | 200: "#ddd6fe",
85 | 300: "#c4b5fd",
86 | 400: "#a78bfa",
87 | 500: "#8b5cf6",
88 | 600: "#7c3aed",
89 | 700: "#6d28d9",
90 | 800: "#5b21b6",
91 | 900: "#4c1d95",
92 | },
93 | indigo: {
94 | 50: "#ECEEF8",
95 | 100: "#CACFED",
96 | 200: "#A8B1E1",
97 | 300: "#8692D5",
98 | 400: "#6473C9",
99 | 500: "#4255BD",
100 | 600: "#354497",
101 | 700: "#283371",
102 | 800: "#1A224C",
103 | 900: "#0D1126",
104 | },
105 | blue: {
106 | 50: "#eff6ff",
107 | 100: "#dbeafe",
108 | 200: "#bfdbfe",
109 | 300: "#93c5fd",
110 | 400: "#60a5fa",
111 | 500: "#3b82f6",
112 | 600: "#2563eb",
113 | 700: "#1d4ed8",
114 | 800: "#1e40af",
115 | 900: "#1e3a8a",
116 | },
117 | lightBlue: {
118 | 50: "#f0f9ff",
119 | 100: "#e0f2fe",
120 | 200: "#bae6fd",
121 | 300: "#7dd3fc",
122 | 400: "#38bdf8",
123 | 500: "#0ea5e9",
124 | 600: "#0284c7",
125 | 700: "#0369a1",
126 | 800: "#075985",
127 | 900: "#0c4a6e",
128 | },
129 | cyan: {
130 | 50: "#ecfeff",
131 | 100: "#cffafe",
132 | 200: "#a5f3fc",
133 | 300: "#67e8f9",
134 | 400: "#22d3ee",
135 | 500: "#06b6d4",
136 | 600: "#0891b2",
137 | 700: "#0e7490",
138 | 800: "#155e75",
139 | 900: "#164e63",
140 | },
141 | teal: {
142 | 50: "#E6FFFA",
143 | 100: "#B2F5EA",
144 | 200: "#81E6D9",
145 | 300: "#4FD1C5",
146 | 400: "#38B2AC",
147 | 500: "#319795",
148 | 600: "#2C7A7B",
149 | 700: "#285E61",
150 | 800: "#234E52",
151 | 900: "#1D4044",
152 | },
153 | emerald: {
154 | 50: "#ecfdf5",
155 | 100: "#d1fae5",
156 | 200: "#a7f3d0",
157 | 300: "#6ee7b7",
158 | 400: "#34d399",
159 | 500: "#10b981",
160 | 600: "#059669",
161 | 700: "#047857",
162 | 800: "#065f46",
163 | 900: "#064e3b",
164 | },
165 | green: {
166 | 50: "#f0fdf4",
167 | 100: "#dcfce7",
168 | 200: "#bbf7d0",
169 | 300: "#86efac",
170 | 400: "#4ade80",
171 | 500: "#22c55e",
172 | 600: "#16a34a",
173 | 700: "#15803d",
174 | 800: "#166534",
175 | 900: "#14532d",
176 | },
177 | lime: {
178 | 50: "#f7fee7",
179 | 100: "#ecfccb",
180 | 200: "#d9f99d",
181 | 300: "#bef264",
182 | 400: "#a3e635",
183 | 500: "#84cc16",
184 | 600: "#65a30d",
185 | 700: "#4d7c0f",
186 | 800: "#3f6212",
187 | 900: "#365314",
188 | },
189 | yellow: {
190 | 50: "#fefce8",
191 | 100: "#fef9c3",
192 | 200: "#fef08a",
193 | 300: "#fde047",
194 | 400: "#facc15",
195 | 500: "#eab308",
196 | 600: "#ca8a04",
197 | 700: "#a16207",
198 | 800: "#854d0e",
199 | 900: "#713f12",
200 | },
201 | amber: {
202 | 50: "#fffbeb",
203 | 100: "#fef3c7",
204 | 200: "#fde68a",
205 | 300: "#fcd34d",
206 | 400: "#fbbf24",
207 | 500: "#f59e0b",
208 | 600: "#d97706",
209 | 700: "#b45309",
210 | 800: "#92400e",
211 | 900: "#78350f",
212 | },
213 | orange: {
214 | 50: "#fff3e0",
215 | 100: "#ffe0b2",
216 | 200: "#ffcc80",
217 | 300: "#ffb74d",
218 | 400: "#ffa726",
219 | 500: "#ff9800",
220 | 600: "#fb8c00",
221 | 700: "#f57c00",
222 | 800: "#ef6c00",
223 | 900: "#e65100",
224 | a100: "#ffd180",
225 | a200: "#ffab40",
226 | a400: "#ff9100",
227 | a700: "#ff6d00",
228 | },
229 | red: {
230 | 50: "#fef2f2",
231 | 100: "#fee2e2",
232 | 200: "#fecaca",
233 | 300: "#fca5a5",
234 | 400: "#f87171",
235 | 500: "#ef4444",
236 | 600: "#dc2626",
237 | 700: "#b91c1c",
238 | 800: "#991b1b",
239 | 900: "#7f1d1d",
240 | },
241 | warmGray: {
242 | 50: "#fafaf9",
243 | 100: "#f5f5f4",
244 | 200: "#e7e5e4",
245 | 300: "#d6d3d1",
246 | 400: "#a8a29e",
247 | 500: "#78716c",
248 | 600: "#57534e",
249 | 700: "#44403c",
250 | 800: "#292524",
251 | 900: "#1c1917",
252 | },
253 | trueGray: {
254 | 50: "#fafafa",
255 | 100: "#f5f5f5",
256 | 200: "#e5e5e5",
257 | 300: "#d4d4d4",
258 | 400: "#a3a3a3",
259 | 500: "#737373",
260 | 600: "#525252",
261 | 700: "#404040",
262 | 800: "#262626",
263 | 900: "#171717",
264 | },
265 | gray: {
266 | 50: "#f9fafb",
267 | 100: "#f3f4f6",
268 | 200: "#e5e7eb",
269 | 300: "#d1d5db",
270 | 400: "#9ca3af",
271 | 500: "#6b7280",
272 | 600: "#4b5563",
273 | 700: "#374151",
274 | 800: "#1f2937",
275 | 900: "#111827",
276 | },
277 | blueGray: {
278 | 50: "#f8fafc",
279 | 100: "#f1f5f9",
280 | 200: "#e2e8f0",
281 | 300: "#cbd5e1",
282 | 400: "#94a3b8",
283 | 500: "#64748b",
284 | 600: "#475569",
285 | 700: "#334155",
286 | 800: "#1e293b",
287 | 900: "#0f172a",
288 | },
289 | };
290 |
291 | const shadows = {
292 | sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
293 | DEFAULT: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
294 | md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
295 | lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
296 | xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
297 | "2xl": "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
298 | inner: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)",
299 | none: "none",
300 | };
301 |
302 | const space = {
303 | px: "1px",
304 | 0: "0",
305 | 0.5: "0.125rem",
306 | 1: "0.25rem",
307 | 1.5: "0.375rem",
308 | 2: "0.5rem",
309 | 2.5: "0.625rem",
310 | 3: "0.75rem",
311 | 3.5: "0.875rem",
312 | 4: "1rem",
313 | 5: "1.25rem",
314 | 6: "1.5rem",
315 | 7: "1.75rem",
316 | 8: "2rem",
317 | 9: "2.25rem",
318 | 10: "2.5rem",
319 | 12: "3rem",
320 | 14: "3.5rem",
321 | 16: "4rem",
322 | 20: "5rem",
323 | 24: "6rem",
324 | 28: "7rem",
325 | 32: "8rem",
326 | 36: "9rem",
327 | 40: "10rem",
328 | 44: "11rem",
329 | 48: "12rem",
330 | 52: "13rem",
331 | 56: "14rem",
332 | 60: "15rem",
333 | 64: "16rem",
334 | 72: "18rem",
335 | 80: "20rem",
336 | 96: "24rem",
337 | };
338 |
339 | const sizes = {
340 | ...space,
341 | max: "max-content",
342 | min: "min-content",
343 | full: "100%",
344 | "3xs": "14rem",
345 | "2xs": "16rem",
346 | xs: "20rem",
347 | sm: "24rem",
348 | md: "28rem",
349 | lg: "32rem",
350 | xl: "36rem",
351 | "2xl": "42rem",
352 | "3xl": "48rem",
353 | "4xl": "56rem",
354 | "5xl": "64rem",
355 | "6xl": "72rem",
356 | "7xl": "80rem",
357 | "8xl": "90rem",
358 | container: {
359 | sm: "640px",
360 | md: "768px",
361 | lg: "1024px",
362 | xl: "1280px",
363 | },
364 | };
365 |
366 | const styles = {
367 | global: (props: any) => ({
368 | body: {
369 | fontFamily: "body",
370 | color: mode("blackAlpha.900", "whiteAlpha.900")(props),
371 | bg: mode("gray.100", "gray.800")(props),
372 | },
373 | }),
374 | };
375 |
376 | const config: ThemeConfig = {
377 | initialColorMode: "dark",
378 | useSystemColorMode: false,
379 | };
380 |
381 | const theme = { fonts, breakpoints, colors, shadows, space, sizes, styles, config };
382 | export default theme;
383 |
--------------------------------------------------------------------------------
/src/util/toast.ts:
--------------------------------------------------------------------------------
1 | import { createStandaloneToast, extendTheme } from "@chakra-ui/react";
2 |
3 | import theme from "./theme";
4 |
5 | const toast = createStandaloneToast({ theme: extendTheme(theme) });
6 |
7 | export default toast;
8 |
--------------------------------------------------------------------------------
/src/views/auth.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from "preact/hooks";
2 |
3 | import {
4 | Button,
5 | Heading,
6 | HStack,
7 | Modal,
8 | ModalBody,
9 | ModalCloseButton,
10 | ModalContent,
11 | ModalFooter,
12 | ModalHeader,
13 | ModalOverlay,
14 | Text,
15 | useColorMode,
16 | VStack,
17 | } from "@chakra-ui/react";
18 | import { useAsyncFn } from "react-use";
19 |
20 | import { AppleIcon } from "../components/icons/apple";
21 | import { GithubIcon } from "../components/icons/github";
22 | import { GoogleIcon } from "../components/icons/google";
23 | import AuthService from "../services/auth";
24 | import ErrorHandlingService from "../services/error-handling";
25 | import toast from "../util/toast";
26 |
27 | const _SignInBtn = ({
28 | name,
29 | color,
30 | icon,
31 | onClick,
32 | }: {
33 | name: string;
34 | color: string;
35 | icon: JSX.Element;
36 | onClick: () => void;
37 | }) => (
38 |
46 | );
47 |
48 | export default function AuthView({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
49 | const [signInState, signInFn] = useAsyncFn(async function signIn(provider: "google" | "github" | "apple") {
50 | signInState.error = undefined;
51 |
52 | try {
53 | await AuthService.I.signInWithProvider(provider);
54 | } catch (e) {
55 | ErrorHandlingService.I.notifyUserOfError("Error signing in", e);
56 | return;
57 | }
58 | });
59 |
60 | const [signOutState, signOutFn] = useAsyncFn(async function signOut() {
61 | signOutState.error = undefined;
62 |
63 | try {
64 | await AuthService.I.signOut();
65 | toast({
66 | title: "Successfully signed out!",
67 | status: "success",
68 | isClosable: true,
69 | });
70 | onClose();
71 | } catch (e) {
72 | ErrorHandlingService.I.notifyUserOfError("Error signing out", e);
73 | return;
74 | }
75 | });
76 |
77 | const initialRef = useRef();
78 |
79 | const { colorMode } = useColorMode();
80 |
81 | const isAnonymous = AuthService.I.userInfo$.value?.isAnonymous ?? true;
82 |
83 | return (
84 | <>
85 |
86 |
87 |
88 |
89 | Manage Account
90 |
91 |
92 |
93 | {!isAnonymous ? (
94 |
95 | Signed in as{" "}
96 |
97 | {`${AuthService.I.userInfo$.value!.email} (${
98 | { "google.com": "Google", "github.com": "Github", "apple.com": "Apple" }[
99 | AuthService.I.userInfo$.value!.providerData[0].providerId
100 | ]
101 | })`}
102 |
103 |
104 | ) : (
105 | <>
106 |
107 | <_SignInBtn
108 | name={"Google"}
109 | color={"red"}
110 | icon={}
111 | onClick={() => signInFn("google")}
112 | />
113 | <_SignInBtn
114 | name={"Github"}
115 | color={"purple"}
116 | icon={}
117 | onClick={() => signInFn("github")}
118 | />
119 | <_SignInBtn
120 | name={"Apple"}
121 | color={"gray"}
122 | icon={}
123 | onClick={() => signInFn("apple")}
124 | />
125 |
126 |
132 | Choose a sign-in method
133 |
134 | >
135 | )}
136 |
137 | {!isAnonymous && (
138 |
139 |
142 |
143 | )}
144 |
145 |
146 | >
147 | );
148 | }
149 |
--------------------------------------------------------------------------------
/src/views/download.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "preact/hooks";
2 |
3 | import {
4 | Button,
5 | FormControl,
6 | FormHelperText,
7 | FormLabel,
8 | Heading,
9 | Input,
10 | Modal,
11 | ModalBody,
12 | ModalCloseButton,
13 | ModalContent,
14 | ModalFooter,
15 | ModalHeader,
16 | ModalOverlay,
17 | } from "@chakra-ui/react";
18 | import { useAsyncFn } from "react-use";
19 |
20 | import { useViewController } from "../ViewController";
21 | import ErrorHandlingService from "../services/error-handling";
22 | import FileService from "../services/file";
23 | import toast from "../util/toast";
24 |
25 | export default function DownloadView({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
26 | const [, setView] = useViewController();
27 |
28 | const [wordCode, setWordCode] = useState("");
29 |
30 | const [openFileState, openFileFn] = useAsyncFn(async function openFile(wordCode: string) {
31 | openFileState.error = undefined;
32 |
33 | let exists;
34 | try {
35 | exists = await FileService.I.doesExist(wordCode);
36 | } catch (e) {
37 | ErrorHandlingService.I.notifyUserOfError("Error checking for file existance", e);
38 | return;
39 | }
40 |
41 | if (exists) {
42 | await setView({ slug: "file", params: { wordCode } });
43 | setWordCode("");
44 | } else {
45 | toast({
46 | title: "This file doesn't exist",
47 | status: "error",
48 | duration: undefined,
49 | isClosable: true,
50 | });
51 | throw new Error();
52 | }
53 | });
54 |
55 | const _onClose = () => {
56 | onClose();
57 | setWordCode("");
58 | };
59 |
60 | const initialRef = useRef();
61 |
62 | return (
63 | <>
64 |
65 |
66 |
67 |
68 | Download file
69 |
70 |
71 |
72 |
97 |
98 |
99 |
107 |
108 |
109 |
110 | >
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/views/file.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from "preact/hooks";
2 |
3 | import {
4 | Box,
5 | Button,
6 | Divider,
7 | Drawer,
8 | DrawerBody,
9 | DrawerCloseButton,
10 | DrawerContent,
11 | DrawerFooter,
12 | DrawerHeader,
13 | DrawerOverlay,
14 | Flex,
15 | FormLabel,
16 | Heading,
17 | Input,
18 | Modal,
19 | ModalBody,
20 | ModalCloseButton,
21 | ModalContent,
22 | ModalFooter,
23 | ModalHeader,
24 | ModalOverlay,
25 | Stack,
26 | Text,
27 | Textarea,
28 | useDisclosure,
29 | } from "@chakra-ui/react";
30 | import copyToClipboard from "copy-to-clipboard";
31 | import { useAsync, useAsyncFn } from "react-use";
32 |
33 | import { useViewController } from "../ViewController";
34 | import AuthService from "../services/auth";
35 | import ErrorHandlingService from "../services/error-handling";
36 | import FileService from "../services/file";
37 | import toast from "../util/toast";
38 |
39 | export default function FileView({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
40 | const [view] = useViewController(),
41 | { wordCode } = view.params || {};
42 |
43 | const fileState = useAsync(async () => {
44 | if (!wordCode) return;
45 | try {
46 | return await FileService.I.getInfo(wordCode);
47 | } catch (e) {
48 | ErrorHandlingService.I.notifyUserOfError("Error retrieving file information", e);
49 | onClose();
50 | }
51 | }, [wordCode]);
52 |
53 | const [downloadFileState, downloadFileFn] = useAsyncFn(async function downloadFile(
54 | wordCode: string,
55 | filename: string
56 | ) {
57 | try {
58 | await FileService.I.download(wordCode, filename);
59 | } catch (e) {
60 | ErrorHandlingService.I.notifyUserOfError("Error downloading file", e);
61 | }
62 | });
63 |
64 | const { isOpen: isRemoveModalOpen, onOpen: openRemoveModal, onClose: closeRemoveModal } = useDisclosure();
65 | const [removeFileState, removeFileFn] = useAsyncFn(async function removeFile(wordCode: string, filename: string) {
66 | try {
67 | await FileService.I.remove(wordCode, filename);
68 | closeRemoveModal();
69 | onClose();
70 | } catch (e) {
71 | ErrorHandlingService.I.notifyUserOfError("Error removing file", e);
72 | }
73 | });
74 |
75 | function copyWordCode() {
76 | try {
77 | copyToClipboard(wordCode);
78 | toast({
79 | title: "Word code copied!",
80 | status: "success",
81 | duration: 2000,
82 | isClosable: true,
83 | });
84 | } catch (e) {
85 | ErrorHandlingService.I.notifyUserOfError("Copying word code unsuccessful", e);
86 | }
87 | }
88 |
89 | const initialRef = useRef();
90 |
91 | return (
92 | <>
93 |
94 |
95 |
96 |
97 |
98 | File information
99 |
100 |
101 |
102 |
103 |
104 | Name
105 |
112 |
113 |
114 | Filetype
115 |
122 |
123 |
124 | Upload date
125 |
132 |
133 |
134 |
135 | Word code
136 |
147 |
148 |
151 |
152 |
153 |
154 |
155 |
156 | {fileState.value?.owner === AuthService.I.userId$.value && (
157 |
166 | )}
167 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | Remove file
182 |
183 |
184 | Are you sure you want to remove this file? This action is irreversible!
185 |
186 |
187 |
194 |
195 |
196 |
197 | >
198 | );
199 | }
200 |
--------------------------------------------------------------------------------
/src/views/server.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "preact/hooks";
2 |
3 | import {
4 | Box,
5 | Drawer,
6 | DrawerBody,
7 | DrawerCloseButton,
8 | DrawerContent,
9 | DrawerHeader,
10 | DrawerOverlay,
11 | Flex,
12 | Heading,
13 | Link,
14 | Stack,
15 | Text,
16 | useColorMode,
17 | VStack,
18 | } from "@chakra-ui/react";
19 | import { useShallowCompareEffect } from "react-use";
20 | import { AsyncState } from "react-use/lib/useAsyncFn";
21 |
22 | import { useViewController } from "../ViewController";
23 | import { HostedFileItem } from "../components/HostedFileItem";
24 | import { FileMagnifyingGlassIcon } from "../components/icons/file-magnifying-glass";
25 | import AuthService from "../services/auth";
26 | import ErrorHandlingService from "../services/error-handling";
27 | import FileService from "../services/file";
28 | import UserDataService from "../services/user-data";
29 |
30 | export default function ServerView({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
31 | const [hostedFilesState, setHostedFilesState] = useState>({ loading: true });
32 |
33 | useShallowCompareEffect(
34 | function loadHostedFiles() {
35 | if (AuthService.I.userInfo$.value?.isAnonymous) return;
36 |
37 | try {
38 | UserDataService.I.getHostedFiles()
39 | .then(hostedFiles =>
40 | Promise.all(
41 | hostedFiles.map(async fileId => {
42 | const { name } = await FileService.I.getInfo(fileId);
43 | return [fileId, name] as [string, string];
44 | }) || []
45 | )
46 | )
47 | .then(files => setHostedFilesState({ loading: false, value: files }));
48 | } catch (e) {
49 | setHostedFilesState({ loading: false, error: e });
50 | ErrorHandlingService.I.notifyUserOfError("Error loading hosted files", e);
51 | onClose();
52 | }
53 | },
54 | [isOpen]
55 | );
56 |
57 | const initialRef = useRef();
58 |
59 | const { colorMode } = useColorMode();
60 | const [, setView] = useViewController();
61 |
62 | return (
63 | <>
64 |
65 |
66 |
67 |
68 |
69 | Hosted files
70 |
71 |
72 |
73 | {(() => {
74 | if (AuthService.I.userInfo$.value?.isAnonymous) {
75 | return (
76 |
77 |
78 |
79 |
80 |
81 |
82 | setView({ slug: "auth" })}
85 | >
86 | Sign in
87 | {" "}
88 | to view hosted files
89 |
90 |
91 |
92 | );
93 | }
94 | if (!hostedFilesState.value || hostedFilesState.value.length === 0) {
95 | return (
96 |
97 |
98 |
99 |
100 |
101 |
102 | No hosted files
103 |
104 |
105 |
106 | );
107 | } else {
108 | return hostedFilesState.value?.map(([fileId, name]) => (
109 |
110 | ));
111 | }
112 | })()}
113 |
114 |
115 |
116 |
117 | >
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/src/views/upload.tsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from "preact/compat";
2 | import { useState } from "preact/hooks";
3 |
4 | import {
5 | Button,
6 | Heading,
7 | Modal,
8 | ModalBody,
9 | ModalCloseButton,
10 | ModalContent,
11 | ModalFooter,
12 | ModalHeader,
13 | ModalOverlay,
14 | } from "@chakra-ui/react";
15 | import { registerPlugin } from "filepond";
16 | import FilePondPluginImagePreview from "filepond-plugin-image-preview";
17 | import "filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css";
18 | import { useAsyncFn } from "react-use";
19 |
20 | import { useViewController } from "../ViewController";
21 | import ErrorHandlingService from "../services/error-handling";
22 | import FileService from "../services/file";
23 | import toast from "../util/toast";
24 |
25 | const FilePond = lazy(async () => (await import("react-filepond")).FilePond);
26 | registerPlugin(FilePondPluginImagePreview);
27 |
28 | export default function UploadView({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
29 | const [, setView] = useViewController();
30 |
31 | const [files, setFiles] = useState([]);
32 |
33 | const _onClose = () => {
34 | onClose();
35 | setFiles([]);
36 | };
37 |
38 | const [uploadFileState, uploadFileFn] = useAsyncFn(async function uploadFile(file: File) {
39 | // reject files >20MB
40 | if ((file.size || 0) > 20 * 1024 * 1024) {
41 | toast({
42 | title: "File must be under 20MB",
43 | status: "error",
44 | isClosable: true,
45 | });
46 | return;
47 | }
48 |
49 | try {
50 | const wordCode = await FileService.I.upload(file);
51 | toast({
52 | title: "File uploaded!",
53 | description: "Your file was successfully uploaded.",
54 | status: "success",
55 | isClosable: true,
56 | });
57 | setView({ slug: "file", params: { wordCode } });
58 | setFiles([]);
59 | } catch (e) {
60 | ErrorHandlingService.I.notifyUserOfError("Error uploading file", e);
61 | }
62 | });
63 |
64 | return (
65 | <>
66 |
67 |
68 |
69 |
70 | Upload file
71 |
72 |
73 |
74 | {/* @ts-ignore */}
75 | >}>
76 | {
81 | const file = fileItems[0]?.file;
82 | setFiles([file].filter(Boolean));
83 | }}
84 | />
85 |
86 |
87 |
88 |
96 |
97 |
98 |
99 | >
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "allowJs": false,
6 | "skipLibCheck": true,
7 | "esModuleInterop": false,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "ESNext",
12 | "moduleResolution": "Node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "preserve",
17 | "jsxFactory": "h",
18 | "jsxFragmentFactory": "Fragment",
19 | },
20 | "exclude": ["node_modules"],
21 | "include": ["src/*.ts", "webmanifest.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import preact from "@preact/preset-vite";
2 | import { defineConfig } from "vite";
3 | import { VitePWA } from "vite-plugin-pwa";
4 |
5 | import webmanifest from "./webmanifest.config";
6 |
7 | export default defineConfig({
8 | plugins: [preact(), VitePWA({ registerType: "autoUpdate", manifest: webmanifest as any })],
9 | build: {
10 | rollupOptions: {
11 | output: {
12 | manualChunks(id) {
13 | if (id.includes("firebase")) {
14 | return "firebase";
15 | }
16 | },
17 | },
18 | },
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/webmanifest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "LIGHTNING SHARE",
3 | short_name: "LIGHTNING",
4 | theme_color: "#22C55E",
5 | background_color: "#F3F4F6",
6 | display: "minimal-ui",
7 | orientation: "portrait",
8 | scope: "/",
9 | start_url: "/",
10 | icons: [
11 | {
12 | src: "/icons/icon-72x72.png",
13 | sizes: "72x72",
14 | type: "image/png",
15 | },
16 | {
17 | src: "/icons/icon-96x96.png",
18 | sizes: "96x96",
19 | type: "image/png",
20 | },
21 | {
22 | src: "/icons/icon-128x128.png",
23 | sizes: "128x128",
24 | type: "image/png",
25 | },
26 | {
27 | src: "/icons/icon-144x144.png",
28 | sizes: "144x144",
29 | type: "image/png",
30 | },
31 | {
32 | src: "/icons/icon-152x152.png",
33 | sizes: "152x152",
34 | type: "image/png",
35 | },
36 | {
37 | src: "/icons/icon-192x192.png",
38 | sizes: "192x192",
39 | type: "image/png",
40 | },
41 | {
42 | src: "/icons/icon-384x384.png",
43 | sizes: "384x384",
44 | type: "image/png",
45 | },
46 | {
47 | src: "/icons/icon-512x512.png",
48 | sizes: "512x512",
49 | type: "image/png",
50 | },
51 | ],
52 | };
53 |
--------------------------------------------------------------------------------