diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.github/renovate.json b/.github/renovate.json index 650bd20..d6c85b7 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,4 +2,4 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:base"], "labels": ["dependencies"] -} \ No newline at end of file +} diff --git a/.github/workflows/next-build.yml b/.github/workflows/build.yml similarity index 77% rename from .github/workflows/next-build.yml rename to .github/workflows/build.yml index f751104..411b320 100644 --- a/.github/workflows/next-build.yml +++ b/.github/workflows/build.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '#skip-lint')" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "16" + node-version: "20" cache: yarn - name: Install dependencies run: yarn install @@ -26,11 +26,11 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '#skip-build')" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "16" + node-version: "20" cache: yarn - name: Install dependencies run: yarn install diff --git a/.gitignore b/.gitignore index 88b6f0d..11fe0e0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ yarn-error.log* # typescript *.tsbuildinfo + +.dev.vars +.wrangler +.react-router \ No newline at end of file diff --git a/README.md b/README.md index b076dc4..e83f9fc 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,79 @@ -# DropBox-Index +# Welcome to React Router! -Index your dropbox files publically. Built with NextJS & Tailwind +A modern, production-ready template for building full-stack React applications using React Router. -[![NextJS Build Test](https://github.com/ArnabXD/Dropbox-Index/actions/workflows/next-build.yml/badge.svg)](https://github.com/ArnabXD/Dropbox-Index/actions/workflows/next-build.yml) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/ArnabXD/Dropbox-Index.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ArnabXD/Dropbox-Index/alerts/) -[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ArnabXD/Dropbox-Index.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ArnabXD/Dropbox-Index/context:javascript) +## Features -## ENV Vars +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) -- `APP_ID` : Dropbox App Client ID -- `APP_SECRET` : Dropbox App Client Secret -- `REFRESH_TOKEN` : Dropbox refresh token -- `NEXT_PUBLIC_TITLE` : Website title and Navbar heading +## Getting Started -## Deploy +### Installation -[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy) +Install the dependencies: -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FArnabXD%2FDropbox-Index&env=APP_ID,APP_SECRET,REFRESH_TOKEN,NEXT_PUBLIC_TITLE) \ No newline at end of file +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Previewing the Production Build + +Preview the production build locally: + +```bash +npm run preview +``` + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +Deployment is done using the Wrangler CLI. + +To build and deploy directly to production: + +```sh +npm run deploy +``` + +To deploy a preview URL: + +```sh +npx wrangler versions upload +``` + +You can then promote a version to production after verification or roll it out progressively. + +```sh +npx wrangler versions deploy +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/app/app.css b/app/app.css new file mode 100644 index 0000000..076da4f --- /dev/null +++ b/app/app.css @@ -0,0 +1,11 @@ +@import "tailwindcss"; + +html, +body { + @apply bg-white dark:bg-slate-950; + @apply text-slate-950 dark:text-white; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/app/entry.server.tsx b/app/entry.server.tsx new file mode 100644 index 0000000..9db4060 --- /dev/null +++ b/app/entry.server.tsx @@ -0,0 +1,44 @@ +import type { AppLoadContext, EntryContext } from "react-router"; +import { ServerRouter } from "react-router"; +import { isbot } from "isbot"; +import { renderToReadableStream } from "react-dom/server"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + _loadContext: AppLoadContext, +) { + let shellRendered = false; + let status = responseStatusCode; + const userAgent = request.headers.get("user-agent"); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + status = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + headers: responseHeaders, + status, + }); +} diff --git a/app/root.tsx b/app/root.tsx new file mode 100644 index 0000000..9fc6636 --- /dev/null +++ b/app/root.tsx @@ -0,0 +1,75 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +import type { Route } from "./+types/root"; +import "./app.css"; + +export const links: Route.LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/app/routes.ts b/app/routes.ts new file mode 100644 index 0000000..b3b0903 --- /dev/null +++ b/app/routes.ts @@ -0,0 +1,7 @@ +import { type RouteConfig, route, index } from "@react-router/dev/routes"; + +export default [ + index("routes/home.tsx"), + route("/setup", "routes/setup.tsx"), + route("/authorize", "routes/authorize.tsx"), +] satisfies RouteConfig; diff --git a/app/routes/authorize.tsx b/app/routes/authorize.tsx new file mode 100644 index 0000000..dc2aca1 --- /dev/null +++ b/app/routes/authorize.tsx @@ -0,0 +1,59 @@ +import { redirect } from "react-router"; + +import type { Route } from "./+types/authorize"; +import { DropboxService } from "~/services/dropbox"; + +export const meta = ({ data }: Route.MetaArgs) => { + return [ + { title: `${data.title} | ${data.error}` }, + { name: "description", content: data.error_description }, + ]; +}; + +export const loader = async ({ context, request }: Route.LoaderArgs) => { + const dropbox = new DropboxService( + context.cloudflare.env.DROPBOX_APP_KEY, + context.cloudflare.env.DROPBOX_APP_SECRET, + context.cloudflare.env.TOKEN, + ); + + const code = new URL(request.url).searchParams.get("code"); + if (!code) { + return { + error: "Missing code", + error_description: "The authorization code is missing.", + title: context.cloudflare.env.VITE_SITE_TITLE, + }; + } + + const token = await dropbox.getToken(code, request.url); + + if ("access_token" in token) { + return redirect("/home"); + } + + return { + error: token.error, + error_description: token.error_description, + title: context.cloudflare.env.VITE_SITE_TITLE, + }; +}; + +export default function Authorize({ loaderData }: Route.ComponentProps) { + if (!loaderData.error) { + return ( +
+

Success

+

Authorization successful!

+
+ ); + } + + return ( +
+

Error

+

{loaderData.error}

+

{loaderData.error_description}

+
+ ); +} diff --git a/app/routes/home.tsx b/app/routes/home.tsx new file mode 100644 index 0000000..cf121ac --- /dev/null +++ b/app/routes/home.tsx @@ -0,0 +1,18 @@ +import type { Route } from "./+types/home"; + +export function meta({ data }: Route.MetaArgs) { + return [ + { title: data.title }, + { name: "description", content: "Welcome to React Router!" }, + ]; +} + +export function loader({ context }: Route.LoaderArgs) { + return { + title: context.cloudflare.env.VITE_SITE_TITLE, + }; +} + +export default function Home({ loaderData }: Route.ComponentProps) { + return

Hi

; +} diff --git a/app/routes/setup.tsx b/app/routes/setup.tsx new file mode 100644 index 0000000..6f511f2 --- /dev/null +++ b/app/routes/setup.tsx @@ -0,0 +1,48 @@ +import { DropboxService } from "~/services/dropbox"; +import type { Route } from "./+types/setup"; +import { redirect } from "react-router"; + +export const loader = async ({ context, request }: Route.LoaderArgs) => { + const dropbox = new DropboxService( + context.cloudflare.env.DROPBOX_APP_KEY, + context.cloudflare.env.DROPBOX_APP_SECRET, + context.cloudflare.env.TOKEN + ); + + const refreshToken = await dropbox.getRefreshToken(); + if (refreshToken) { + return { + setupComplete: true, + }; + } + + const authorizationUrl = dropbox.getAuthUrl( + new URL(request.url).host + "/authorize" + ); + + redirect(authorizationUrl, 301); +}; + +export const meta = ({ data }: Route.MetaArgs) => { + return [ + { title: "Setup Dropbox Integration" }, + { name: "description", content: "Setup your Dropbox integration" }, + ]; +}; + +export default function Setup({ loaderData }: Route.ComponentProps) { + if (loaderData?.setupComplete) { + return ( +
+

Setup Complete

+

+ You have successfully set up the Dropbox integration. You can now + proceed to use the application. +

+ Home +
+ ); + } + + return null; +} diff --git a/app/services/dropbox.ts b/app/services/dropbox.ts new file mode 100644 index 0000000..947ef08 --- /dev/null +++ b/app/services/dropbox.ts @@ -0,0 +1,171 @@ +import ky from "ky"; +import type { KyInstance, KyRequest } from "ky"; + +interface DropboxAuthResponse { + access_token: string; + expires_in: number; + refresh_token: string; + scope: string; + uid: string; + account_id: string; +} + +interface DropboxError { + error: string; + error_description: string; +} + +export class DropboxService { + private API_KEY: string; + private API_SECRET: string; + private TOKEN_DB: KVNamespace; + private client: KyInstance; + + constructor(API_KEY: string, API_SECRET: string, TOKEN_DB: KVNamespace) { + this.API_KEY = API_KEY; + this.API_SECRET = API_SECRET; + this.TOKEN_DB = TOKEN_DB; + + this.client = ky.create({ + prefixUrl: "https://api.dropboxapi.com/2", + headers: { + "Content-Type": "application/json", + }, + hooks: { + beforeRequest: [ + async (request) => { + const token = await this.getAccessToken(); + request.headers.set("Authorization", `Bearer ${token}`); + }, + ], + afterResponse: [ + async (request, _options, response) => { + if (response.status === 401) { + const originalRequest = request.clone() as KyRequest; + + const newToken = await this.refreshToken(); + if ("access_token" in newToken) { + request.headers.set( + "Authorization", + `Bearer ${newToken.access_token}` + ); + } + + return await ky(originalRequest); + } + }, + ], + }, + }); + } + + getAuthUrl = (redirectUri: string) => { + const authorizationUrl = new URL( + "https://www.dropbox.com/oauth2/authorize" + ); + + authorizationUrl.searchParams.set("client_id", this.API_KEY); + authorizationUrl.searchParams.set("response_type", "code"); + authorizationUrl.searchParams.set("token_access_type", "offline"); + authorizationUrl.searchParams.set("redirect_uri", redirectUri); + + return authorizationUrl.toString(); + }; + + getToken = async (code: string, redirectUri: string) => { + const body = new FormData(); + + const redirect = new URL(redirectUri); + + body.append("code", code); + body.append("grant_type", "authorization_code"); + body.append("client_id", this.API_KEY); + body.append("client_secret", this.API_SECRET); + body.append("redirect_uri", redirect.origin + redirect.pathname); + + const response = await ky + .post("https://api.dropboxapi.com/oauth2/token", { + body, + throwHttpErrors: false, + }) + .json(); + + if (!response || "error_description" in response) { + console.debug(response); + return response; + } + + await this.TOKEN_DB.put("REFRESH_TOKEN", response.refresh_token); + await this.TOKEN_DB.put("ACCESS_TOKEN", response.access_token); + + return response; + }; + + getAccessToken = async () => { + const token = await this.TOKEN_DB.get("ACCESS_TOKEN"); + if (!token) { + throw new Error("No access token found", { + cause: { + status: 401, + message: "No access token found", + }, + }); + } + + return token; + }; + + getRefreshToken = async () => { + const token = await this.TOKEN_DB.get("REFRESH_TOKEN"); + if (!token) { + throw new Error("No refresh token found"); + } + + return token; + }; + + refreshToken = async () => { + const refreshToken = await this.getRefreshToken(); + + const body = new FormData(); + + body.append("grant_type", "refresh_token"); + body.append("client_id", this.API_KEY); + body.append("client_secret", this.API_SECRET); + body.append("refresh_token", refreshToken); + + const response = await ky + .post("https://api.dropboxapi.com/oauth2/token", { + body, + throwHttpErrors: false, + }) + .json(); + + if (!response || "error_description" in response) { + console.debug(response); + return response; + } + + await this.TOKEN_DB.put("REFRESH_TOKEN", response.refresh_token); + await this.TOKEN_DB.put("ACCESS_TOKEN", response.access_token); + + return response; + }; + + listFolders = async (path = "") => { + const response = await this.client + .post("files/list_folder", { + json: { + path, + recursive: false, + include_media_info: false, + include_deleted: false, + include_has_explicit_shared_members: false, + include_mounted_folders: true, + }, + }) + .json(); + + return response; + }; +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..ba510b7 --- /dev/null +++ b/biome.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules", "dist", "build"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "ignore": ["node_modules", "dist", "build", "worker-configuration.d.ts"] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + }, + "ignore": [ + "node_modules", + "dist", + ".react-router", + "worker-configuration.d.ts" + ] + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/components/Breadcrumb.tsx b/components/Breadcrumb.tsx deleted file mode 100644 index 072eaf1..0000000 --- a/components/Breadcrumb.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useEffect, useState } from "react"; -import { HomeIcon } from "@heroicons/react/solid"; -import Link from "next/link"; -import { IconButton } from "./Icons"; - -export interface BCPaths { - display: string; - link?: string; -} - -interface BCProps { - path?: string; -} - -export const Breadcrumb = (props: BCProps) => { - const [path, setPath] = useState([]); - - useEffect(() => { - if (!props.path) { - return setPath([]); - } - if (!props.path.includes("/")) { - return setPath([{ display: props.path }]); - } - const paths = props.path.split("/").map((value, index) => { - return { - display: value, - link: - index !== props.path!.split("/").length - 1 - ? "/" + - props - .path!.split("/") - .slice(0, index + 1) - .join("/") - : undefined, - }; - }); - setPath(paths); - }, [props.path]); - - return ( -
- - - - - - {path.length ? ( - path.map((p) => ( - - {" / "} - {p.link ? ( - -

{p.display}

- - ) : ( -

{p.display}

- )} -
- )) - ) : ( - <> - )} -
- ); -}; diff --git a/components/Icons.tsx b/components/Icons.tsx deleted file mode 100644 index d0bb4db..0000000 --- a/components/Icons.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { ReactChild } from "react"; - -const Loader = () => ( -
-
-
-
-
-
-
-
-); - -export const Loading = React.memo(Loader); - -interface IconButtonI { - children: ReactChild; - onClick?: () => any; -} - -export const IconButton = ({ children, onClick }: IconButtonI) => { - return ( -
- {children} -
- ); -}; diff --git a/components/List.tsx b/components/List.tsx deleted file mode 100644 index 5f4f3a4..0000000 --- a/components/List.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { - DownloadIcon, - DocumentIcon, - FolderIcon, - ClipboardIcon, -} from "@heroicons/react/solid"; -import { DropboxResponse, files } from "dropbox"; -import { useClipboard } from "@chakra-ui/hooks"; -import { Toaster, toast } from "react-hot-toast"; -import { useRouter } from "next/router"; - -import { IconButton } from "./Icons"; -import ky from "ky"; -import prettyBytes from "pretty-bytes"; - -interface ListProps { - entries: (files.FileMetadataReference | files.FolderMetadataReference)[]; - path?: string; -} - -export const List = (props: ListProps) => { - const [link, setLink] = useState(""); - const { onCopy } = useClipboard(link); - - const router = useRouter(); - - useEffect(() => { - if (link) { - onCopy(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [link]); - - const getDownloadLink = async (path: string) => - await ky - .get("/api/download" + path) - .json< - DropboxResponse< - files.GetTemporaryLinkResult | files.GetTemporaryLinkError - > - >(); - - const copyToClipboard = async ( - data: files.FileMetadataReference | files.FolderMetadataReference - ) => { - if (data[".tag"] === "folder") { - setLink(window.location.origin + data.path_display); - } else { - let resp = await getDownloadLink(data.path_display!); - if ("link" in resp.result) { - setLink(resp.result.link); - } else { - throw new Error("Failed to generate download link"); - } - } - }; - - return ( -
-
-
Name
-
- Info -
-
- Actions -
-
- - {props.entries.map((file) => { - return ( -
-
{ - if (file[".tag"] === "folder") { - router.push(file.path_display || "/"); - } else { - getDownloadLink(file.path_display!).then((resp) => { - if ("link" in resp.result) { - router.push(resp.result.link); - } else { - toast.error("Failed to Download Generate Link"); - } - }); - } - }} - > - {file[".tag"] === "file" ? ( - - ) : ( - - )} -

{file.name}

-
-
- {file[".tag"] === "file" && prettyBytes(file.size)} -
-
- - toast.promise( - copyToClipboard(file), - { - success: "Link copied successfully", - error: "Failed to generate link", - loading: "Generating link", - }, - { - style: { - backgroundColor: "#1e293b", - borderWidth: 2, - borderColor: "#475569", - color: "white", - }, - } - ) - } - > - - - {file[".tag"] === "file" && ( - { - getDownloadLink(file.path_display!).then((resp) => { - if ("link" in resp.result) { - router.push(resp.result.link); - } else { - toast.error("Failed to Download Generate Link"); - } - }); - }} - > - - - )} -
-
- ); - })} -
- ); -}; diff --git a/components/Nav.tsx b/components/Nav.tsx deleted file mode 100644 index d9c471f..0000000 --- a/components/Nav.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import Link from "next/link"; -import { useDisclosure } from "@chakra-ui/hooks"; -import { SearchIcon } from "@heroicons/react/solid"; -import Head from "next/head"; - -export const Nav = () => { - const title = process.env.NEXT_PUBLIC_TITLE ?? "D-Index"; - const { isOpen, onToggle } = useDisclosure(); - return ( - <> - - {title} - - - - ); -}; diff --git a/components/index.ts b/components/index.ts deleted file mode 100644 index 6747adc..0000000 --- a/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Breadcrumb } from "./Breadcrumb"; -export { Loading, IconButton } from "./Icons"; -export { List } from "./List"; -export { Nav } from "./Nav"; diff --git a/lib/dropbox.ts b/lib/dropbox.ts deleted file mode 100644 index 6691961..0000000 --- a/lib/dropbox.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Dropbox } from "dropbox"; - -export const dropbox = new Dropbox({ - clientId: process.env.APP_ID, - clientSecret: process.env.APP_SECRET, - refreshToken: process.env.REFRESH_TOKEN, -}); diff --git a/next-env.d.ts b/next-env.d.ts deleted file mode 100644 index 4f11a03..0000000 --- a/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js deleted file mode 100644 index 8b61df4..0000000 --- a/next.config.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('next').NextConfig} */ -module.exports = { - reactStrictMode: true, -} diff --git a/package.json b/package.json index 3f7e515..373a041 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,37 @@ { - "name": "dropbox-index", + "name": "dropbox-index-rr", "private": true, + "type": "module", "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" + "build": "react-router build", + "cf-typegen": "wrangler types", + "deploy": "yarn run build && wrangler deploy", + "dev": "react-router dev", + "preview": "yarn run build && vite preview", + "typecheck": "npm run cf-typegen && react-router typegen && tsc -b", + "lint": "biome lint", + "format": "biome format --write ." }, "dependencies": { - "@chakra-ui/hooks": "2.0.4", - "@heroicons/react": "1.0.6", - "dropbox": "10.32.0", - "ky": "0.31.1", - "next": "12.2.2", - "pretty-bytes": "6.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-hot-toast": "2.3.0", - "react-query": "3.39.2" + "isbot": "^5.1.17", + "ky": "^1.8.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.5.0" }, "devDependencies": { - "@types/node": "16.11.43", - "@types/react": "17.0.45", - "autoprefixer": "10.4.8", - "eslint": "8.22.0", - "eslint-config-next": "12.2.2", - "postcss": "8.4.14", - "tailwindcss": "3.1.7", - "typescript": "4.7.4" - } + "@biomejs/biome": "1.9.4", + "@cloudflare/vite-plugin": "^1.0.0", + "@react-router/dev": "^7.5.0", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^20", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.2", + "vite": "^6.2.1", + "vite-tsconfig-paths": "^5.1.4", + "wrangler": "^4.10.0" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/pages/[...path].tsx b/pages/[...path].tsx deleted file mode 100644 index decaa30..0000000 --- a/pages/[...path].tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useEffect, useState } from "react"; -import type { NextPage } from "next"; -import ky from "ky"; -import { useQuery } from "react-query"; -import { DropboxResponse, files } from "dropbox"; -import { useRouter } from "next/router"; -import { Nav, Loading, List, Breadcrumb } from "../components"; - -const Home: NextPage = () => { - const router = useRouter(); - const [path, setPath] = useState(""); - - useEffect(() => { - if (router.query.path) { - if (Array.isArray(router.query.path)) { - setPath(router.query.path.join("/")); - } else { - setPath(router.query.path); - } - } - }, [router.query]); - - const { data, isLoading, isFetching, refetch } = useQuery( - "path-" + path, - async () => { - const resp = await ky - .get("/api/explore/" + path) - .json< - DropboxResponse - >(); - if (resp.status === 200) { - const resp2 = resp as DropboxResponse; - return resp2.result.entries.filter( - (file) => file[".tag"] === "file" || file[".tag"] === "folder" - ) as (files.FileMetadataReference | files.FolderMetadataReference)[]; - } else { - throw new Error("Something Went Wrong"); - } - }, - { enabled: false } - ); - - useEffect(() => { - if (path) { - console.log(path); - refetch(); - } - }, [path, refetch]); - - return ( - <> -