From ac7449f96757e10038a9b566ac6e401093d22f4d Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Thu, 11 Apr 2024 22:51:54 +0700 Subject: [PATCH 01/12] Add configuration to show / hide file extension on name --- src/app/@file.grid.tsx | 24 ++++++++++++++++++------ src/app/@file.list.tsx | 24 ++++++++++++++++++------ src/config/gIndex.config.ts | 11 +++++++++++ src/schema.ts | 2 ++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/app/@file.grid.tsx b/src/app/@file.grid.tsx index f86b9ca..236a60d 100644 --- a/src/app/@file.grid.tsx +++ b/src/app/@file.grid.tsx @@ -260,22 +260,34 @@ export default function FileGrid({ data }: Props) { {/* File data */}
- {data.fileExtension + {config.siteConfig.showFileExtension + ? data.name + : data.fileExtension ? data.name.replace(new RegExp(`.${data.fileExtension}$`), "") : data.name}
- + {data.mimeType.includes("folder") ? "folder" : data.fileExtension} {!data.mimeType.includes("folder") && ( <> - + {config.siteConfig.showFileExtension ? null : ( + + )} {bytesToReadable(data.size || 0)} diff --git a/src/app/@file.list.tsx b/src/app/@file.list.tsx index e61a5ac..87041dd 100644 --- a/src/app/@file.list.tsx +++ b/src/app/@file.list.tsx @@ -248,22 +248,34 @@ export default function FileList({ data }: Props) { {/* File data */}
- {data.fileExtension + {config.siteConfig.showFileExtension + ? data.name + : data.fileExtension ? data.name.replace(new RegExp(`.${data.fileExtension}$`), "") : data.name}
- + {data.mimeType.includes("folder") ? "folder" : data.fileExtension} {!data.mimeType.includes("folder") && ( <> - + {config.siteConfig.showFileExtension ? null : ( + + )} {bytesToReadable(data.size || 0)} diff --git a/src/config/gIndex.config.ts b/src/config/gIndex.config.ts index 8780264..3892a54 100644 --- a/src/config/gIndex.config.ts +++ b/src/config/gIndex.config.ts @@ -165,6 +165,17 @@ const config: z.input = { robots: "noindex, nofollow", twitterHandle: "@mbaharip_", + /** + * Show file extension on the file name + * Example: + * true | false + * file.txt | file + * 100KB | txt / 100KB + * + * Default: false + */ + showFileExtension: false, + /** * Footer content * You can use string or array of string for multiple lines diff --git a/src/schema.ts b/src/schema.ts index 30f090a..f1ee2d0 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -71,6 +71,8 @@ export const Schema_Config = z.object({ robots: z.string().optional().default("noindex, nofollow"), twitterHandle: z.string().optional().default("@__mbaharip__"), + showFileExtension: z.boolean().optional().default(false), + footer: z .string() .or(z.array(z.string())) From bccd5502758d13e57736df556762aa86a31bc52f Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Thu, 11 Apr 2024 22:52:24 +0700 Subject: [PATCH 02/12] Add switch between markdown or raw files on rich preview and readme --- src/app/@markdown.tsx | 83 +++++++++++++++---------- src/app/@preview.layout.tsx | 75 ++++++++++++++++++++++ src/app/@preview.rich.tsx | 37 +++++++---- src/app/@readme.tsx | 37 +++++++++++ src/app/@rich-header.tsx | 39 ++++++++++++ src/app/[...rest]/page.tsx | 120 +++++++++++++----------------------- src/app/page.tsx | 34 +++++----- 7 files changed, 290 insertions(+), 135 deletions(-) create mode 100644 src/app/@preview.layout.tsx create mode 100644 src/app/@readme.tsx create mode 100644 src/app/@rich-header.tsx diff --git a/src/app/@markdown.tsx b/src/app/@markdown.tsx index 041763d..f70a7e5 100644 --- a/src/app/@markdown.tsx +++ b/src/app/@markdown.tsx @@ -10,6 +10,7 @@ import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import remarkSlug from "remark-slug"; import remarkToc from "remark-toc"; +import { cn } from "~/utils"; import Icon from "~/components/Icon"; @@ -17,41 +18,59 @@ import "./highlight.css"; type Props = { content: string; + view: "markdown" | "raw"; }; -export default function Markdown({ content }: Props) { +export default function Markdown({ content, view }: Props) { return ( -
- ( -

- {children} -

- ), - pre: PreComponent, - code: ({ node, inline, className, children, ...props }) => ( - - {children} - - ), - }} +
+
- {content} - +
+          {content}
+        
+
+
+ ( +

+ {children} +

+ ), + pre: PreComponent, + code: ({ node, inline, className, children, ...props }) => ( + + {children} + + ), + }} + > + {content} +
+
); } diff --git a/src/app/@preview.layout.tsx b/src/app/@preview.layout.tsx new file mode 100644 index 0000000..287dfce --- /dev/null +++ b/src/app/@preview.layout.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState } from "react"; +import { z } from "zod"; +import { Schema_File } from "~/schema"; + +import { Card, CardContent } from "~/components/ui/card"; + +import { getFileType } from "~/utils/previewHelper"; + +import PreviewAction from "./@preview.action"; +import PreviewAudio from "./@preview.audio"; +import PreviewDoc from "./@preview.doc"; +import PreviewImage from "./@preview.image"; +import PreviewManga from "./@preview.manga"; +import PreviewRich from "./@preview.rich"; +import PreviewUnknown from "./@preview.unknown"; +import PreviewVideo from "./@preview.video"; +import RichHeader from "./@rich-header"; + +type Props = { + data: z.infer; + fileType: "unknown" | ReturnType; +}; +export default function FilePreviewLayout({ data, fileType }: Props) { + const [view, setView] = useState<"markdown" | "raw">("markdown"); + + return ( + <> + + + +
+ {fileType === "image" ? ( + + ) : fileType === "audio" ? ( + + ) : fileType === "video" ? ( + + ) : fileType === "code" ? ( + + ) : fileType === "text" ? ( + + ) : fileType === "markdown" ? ( + + ) : fileType === "document" ? ( + + ) : fileType === "pdf" ? ( + + ) : fileType === "manga" ? ( + + ) : ( + + )} +
+
+
+ + + ); +} diff --git a/src/app/@preview.rich.tsx b/src/app/@preview.rich.tsx index ff2095f..a546939 100644 --- a/src/app/@preview.rich.tsx +++ b/src/app/@preview.rich.tsx @@ -13,9 +13,11 @@ import { GetContent } from "./actions"; type Props = { file: z.infer; + view: "markdown" | "raw"; code?: boolean; }; -export default function PreviewRich({ file, code }: Props) { +export default function PreviewRich({ file, code, view }: Props) { + const [fetchedContent, setFetchedContent] = useState(""); const [content, setContent] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -30,11 +32,12 @@ export default function PreviewRich({ file, code }: Props) { setError("Looks like there is no content to preview"); return; } - if (code) { - setContent(`\`\`\`${file.fileExtension}\n${text}\`\`\``); - } else { - setContent(text); - } + // setFetchedContent(text); + // if (code) { + // setContent(`\`\`\`${file.fileExtension}\n${text}\`\`\``); + // } else { + setContent(text); + // } } catch (error) { const e = error as Error; console.error(e); @@ -46,7 +49,7 @@ export default function PreviewRich({ file, code }: Props) { }, [file, code]); return ( -
+
{loading ? (
) : (
- +
("markdown"); + + return ( +
+ + + + + + +
+ ); +} diff --git a/src/app/@rich-header.tsx b/src/app/@rich-header.tsx new file mode 100644 index 0000000..e3a2179 --- /dev/null +++ b/src/app/@rich-header.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Button } from "~/components/ui/button"; +import { CardHeader, CardTitle } from "~/components/ui/card"; +import { Separator } from "~/components/ui/separator"; + +type Props = { + title: string; + view: "markdown" | "raw"; + onViewChange: (value: "markdown" | "raw") => void; +}; +export default function RichHeader({ title, view, onViewChange }: Props) { + return ( + +
+ {title} +
+ + +
+
+ +
+ ); +} diff --git a/src/app/[...rest]/page.tsx b/src/app/[...rest]/page.tsx index 97baa60..8d9cd9f 100644 --- a/src/app/[...rest]/page.tsx +++ b/src/app/[...rest]/page.tsx @@ -14,16 +14,9 @@ import { getFileType } from "~/utils/previewHelper"; import FileBrowser from "../@explorer"; import Header from "../@header"; import HeaderButton from "../@header.button"; -import Markdown from "../@markdown"; import Password from "../@password"; -import PreviewAction from "../@preview.action"; -import PreviewAudio from "../@preview.audio"; -import PreviewDoc from "../@preview.doc"; -import PreviewImage from "../@preview.image"; -import PreviewManga from "../@preview.manga"; -import PreviewRich from "../@preview.rich"; -import PreviewUnknown from "../@preview.unknown"; -import PreviewVideo from "../@preview.video"; +import FilePreviewLayout from "../@preview.layout"; +import Readme from "../@readme"; import { CheckPassword, CheckPaths, @@ -141,76 +134,51 @@ export default async function RestPage({ params: { rest } }: Props) { slot='content' className='w-full' > - - - {isFile ? ( -
- - {data.name} - -
- ) : ( -
- Browse files - -
- )} - -
- - {isFile ? ( -
- {fileType === "image" ? ( - - ) : fileType === "audio" ? ( - - ) : fileType === "video" ? ( - - ) : fileType === "code" ? ( - - ) : fileType === "text" ? ( - - ) : fileType === "markdown" ? ( - - ) : fileType === "document" ? ( - - ) : fileType === "pdf" ? ( - - ) : fileType === "manga" ? ( - - ) : ( - - )} -
- ) : ( - + ) : ( + <> + + +
+ Browse files + +
+ +
+ + + +
+ {readme && ( + + //
+ // + // + // README.md + // + // + // + // + // + // + //
)} -
-
+ + )}
- {readme && ( -
- - - README.md - - - - - - -
- )} - {isFile && }
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 3cd1812..b290e1b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,7 +6,7 @@ import { Separator } from "~/components/ui/separator"; import FileBrowser from "./@explorer"; import Header from "./@header"; import HeaderButton from "./@header.button"; -import Markdown from "./@markdown"; +import Readme from "./@readme"; import { GetFiles, GetReadme } from "./actions"; export const revalidate = 300; @@ -43,20 +43,24 @@ export default async function RootPage() {
{readme && ( -
- - - README.md - - - - - - -
+ + //
+ // + // + // README.md + // + // + // + // + // + // + //
)}
); From b8a7926fd28ebd88b2250157061a460be28280bb Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Fri, 12 Apr 2024 05:34:49 +0700 Subject: [PATCH 03/12] Prioritize to use `NEXT_PUBLIC_DOMAIN` --- src/config/gIndex.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/gIndex.config.ts b/src/config/gIndex.config.ts index 3892a54..02cb3fa 100644 --- a/src/config/gIndex.config.ts +++ b/src/config/gIndex.config.ts @@ -13,12 +13,13 @@ const config: z.input = { * If you're using another port for development, you can set it here * * @default process.env.NEXT_PUBLIC_DOMAIN + * @fallback process.env.NEXT_PUBLIC_VERCEL_URL */ basePath: process.env.NODE_ENV === "development" ? "http://localhost:3000" : `https://${ - process.env.NEXT_PUBLIC_VERCEL_URL || process.env.NEXT_PUBLIC_DOMAIN + process.env.NEXT_PUBLIC_DOMAIN || process.env.NEXT_PUBLIC_VERCEL_URL }`, /** From 43b108cf475cf2d5e053ace914095ef248d2b02d Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Fri, 12 Apr 2024 05:35:01 +0700 Subject: [PATCH 04/12] Update explanation --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 5bea85b..7c6b1e4 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,6 @@ NEXT_PUBLIC_ENCRYPTION_KEY= NEXT_PUBLIC_SITE_PASSWORD= # Only fill with the domain name, without the protocol and trailing slash -# This will be used as a fallback if VERCEL_URL is not available -# Example: https://drive-demo.mbaharip.com -> drive.mbaharip.com +# This will be used, and will fallback to VERCEL_URL if it's not available +# Example: https://drive-demo.mbaharip.com -> drive-demo.mbaharip.com NEXT_PUBLIC_DOMAIN= From c5434fe9c415db33567e9923ca397ab86c17a127 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Fri, 12 Apr 2024 05:35:19 +0700 Subject: [PATCH 05/12] Add missing `size` field causing timeout when downloading large files --- src/app/api/download/[encryptedId]/route.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/app/api/download/[encryptedId]/route.ts b/src/app/api/download/[encryptedId]/route.ts index bba8c4e..2eb64a6 100644 --- a/src/app/api/download/[encryptedId]/route.ts +++ b/src/app/api/download/[encryptedId]/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; + import { CheckDownloadToken, CheckPassword, @@ -50,7 +51,7 @@ If you've already entered the password, please make sure your browser is not blo const _filePaths = RedirectSearchFile(encryptedId); const _fileMeta = gdrive.files.get({ fileId: decryptedId, - fields: "id, name, mimeType, fileExtension, webContentLink", + fields: "id, name, mimeType, size, fileExtension, webContentLink", supportsAllDrives: config.apiConfig.isTeamDrive, }); const _fileContent = gdrive.files.get( @@ -69,6 +70,7 @@ If you've already entered the password, please make sure your browser is not blo _fileContent, _filePaths, ]); + if (!config.apiConfig.allowDownloadProtectedFile) { const checkPath = await CheckPaths(filePaths.split("/")); if (!checkPath.success) throw new Error("File not found"); @@ -107,6 +109,7 @@ If you've already entered the password, please make sure your browser is not blo config.apiConfig.maxFileSize && fileSize > config.apiConfig.maxFileSize ) { + console.log("File size is too large, redirecting to webContentLink"); return NextResponse.redirect(fileMeta.data.webContentLink, { status: 302, headers: { @@ -141,19 +144,6 @@ If you've already entered the password, please make sure your browser is not blo "Cache-Control": config.cacheControl, }, }); - // const data = await GetFile(encryptedId); - // if (data.mimeType?.includes("folder")) - // throw new Error("Can't download folder"); - // if (!data.encryptedWebContentLink) - // throw new Error("No download link found"); - - // const decryptedWebContent = await decryptData(data.encryptedWebContentLink); - // return new NextResponse(null, { - // status: 302, - // headers: { - // Location: decryptedWebContent, - // }, - // }); } catch (error) { const e = error as Error; console.error(e.message); From 3afe51581bae58df9e576212c8cd9e33bb1e1b2d Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Mon, 22 Apr 2024 21:22:15 +0700 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=96=A5=EF=B8=8F=20wip:=20Config=20s?= =?UTF-8?q?creen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- src/app/@markdown.tsx | 14 +- src/app/@navbar.tsx | 55 +++- src/app/[...rest]/deploy/docs.tsx | 473 ++++++++++++++++++++++++++++ src/app/[...rest]/deploy/index.tsx | 293 +++++++++++++++++ src/app/[...rest]/page.tsx | 9 + src/app/actions.ts | 23 ++ src/app/layout.tsx | 10 +- src/app/markdown.css | 6 +- src/components/ui/breadcrumb.tsx | 82 +++-- src/components/ui/button.tsx | 43 ++- src/components/ui/card.tsx | 48 ++- src/components/ui/carousel.tsx | 181 ++++++----- src/components/ui/dialog.tsx | 61 ++-- src/components/ui/drawer.tsx | 51 +-- src/components/ui/dropdown-menu.tsx | 93 +++--- src/components/ui/input.tsx | 17 +- src/components/ui/label.tsx | 21 +- src/components/ui/menubar.tsx | 109 +++---- src/components/ui/select.tsx | 69 ++-- src/components/ui/separator.tsx | 21 +- src/components/ui/sheet.tsx | 63 ++-- src/components/ui/skeleton.tsx | 6 +- src/components/ui/table.tsx | 53 ++-- src/components/ui/textarea.tsx | 17 +- src/components/ui/toast.tsx | 61 ++-- src/components/ui/toaster.tsx | 19 +- src/components/ui/tooltip.tsx | 23 +- src/components/ui/use-toast.ts | 141 +++++---- src/config/gIndex.config.ts | 12 + src/schema.ts | 51 +++ src/utils/encryptionHelper/hash.ts | 16 +- tailwind.config.ts | 2 +- yarn.lock | 176 ++++++++++- 34 files changed, 1698 insertions(+), 626 deletions(-) create mode 100644 src/app/[...rest]/deploy/docs.tsx create mode 100644 src/app/[...rest]/deploy/index.tsx diff --git a/package.json b/package.json index 723b166..33e98c3 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "clsx": "^2.1.0", "cmdk": "^1.0.0", "date-fns": "^3.6.0", - "embla-carousel-react": "^8.0.1", + "embla-carousel-react": "^8.0.2", "googleapis": "^118.0.0", "input-otp": "^1.2.3", "jsonwebtoken": "^9.0.0", @@ -55,6 +55,7 @@ "next-themes": "^0.3.0", "nextjs-toploader": "^1.6.11", "react": "^18", + "react-colorful": "^5.6.1", "react-day-picker": "^8.10.0", "react-dom": "^18", "react-h5-audio-player": "^3.9.1", @@ -69,6 +70,7 @@ "rehype-katex": "^6.0.3", "rehype-prism-plus": "^1.6.3", "rehype-raw": "6.1.1", + "remark-breaks": "^4.0.0", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "remark-slug": "^7.0.1", @@ -76,6 +78,7 @@ "sonner": "^1.4.41", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", + "use-debouncy": "^5.0.1", "vaul": "^0.9.0", "zod": "^3.22.4" }, diff --git a/src/app/@markdown.tsx b/src/app/@markdown.tsx index f70a7e5..485edc2 100644 --- a/src/app/@markdown.tsx +++ b/src/app/@markdown.tsx @@ -6,6 +6,7 @@ import ReactMarkdown from "react-markdown"; import rehypeKatex from "rehype-katex"; import rehypePrism from "rehype-prism-plus"; import rehypeRaw from "rehype-raw"; +import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import remarkSlug from "remark-slug"; @@ -19,10 +20,11 @@ import "./highlight.css"; type Props = { content: string; view: "markdown" | "raw"; + className?: string; }; -export default function Markdown({ content, view }: Props) { +export default function Markdown({ content, view, className }: Props) { return ( -
+
+ {config.showDeployGuide && ( + + + + + + + +

Deploy Guide

+
+
+ )} + )} + + @@ -457,6 +487,29 @@ export default function Navbar() { ))} + + {config.showDeployGuide && ( + + )}
diff --git a/src/app/[...rest]/deploy/docs.tsx b/src/app/[...rest]/deploy/docs.tsx new file mode 100644 index 0000000..08ad2e6 --- /dev/null +++ b/src/app/[...rest]/deploy/docs.tsx @@ -0,0 +1,473 @@ +"use client"; + +import { ChangeEvent, useMemo, useRef, useState } from "react"; +import { HslColor, HslColorPicker } from "react-colorful"; +import toast from "react-hot-toast"; +import { z } from "zod"; +import { Schema_Config } from "~/schema"; +import { cn } from "~/utils"; + +import { GenerateAESKey, VerifyAESKey } from "~/app/actions"; +import Icon from "~/components/Icon"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Separator } from "~/components/ui/separator"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "~/components/ui/tooltip"; + +import config from "~/config/gIndex.config"; + +export const getting_started = `Welcome to the deployment guide! This guide will help you to deploy the application to Vercel or similar services. + +If you are new to this project, you can follow along from the beginning. +But if you've already deployed the app before and want to upgrade from v1, you can skip to the [Migrating from v1](#migrating) section. +You can also use this guide to [configure the app](#config) and [customize the theme](#theme). + +_**Note:** This guide assumes you have a basic understanding of how to deploy a Next.js app on Vercel or other platforms._`; + +export const new_user_guide = `Prerequisites: +- A basic understanding of Vercel (or similar services) +- Google Cloud Platform account + +### Fork or Clone the repository +It's pretty obvious, but you need to fork the repository to your account. +You can [click here](https://github.com/mbahArip/next-gdrive-index/fork) to fork the repository. +You can choose any repository name, description, and visibility. + +But if you want to run it locally, you can clone the repository instead. + +### Create a Google Cloud Platform project and enable Google Drive API +We need an access to Google Drive API to get the files from Google Drive. +So you need to create a project in Google Cloud Platform and enable Google Drive API to get your own credentials. + +1. Go to [Google Cloud Platform](https://console.cloud.google.com/) +2. Click the \`New Project\` button +3. Enter a project name, and click the \`Create\` button +4. After the project is created, click the \`Enable APIs and Services\` button +5. Search for Google Drive API, and click the \`Enable\` button + +### Create a Service Account and get the credentials +After enabling the Google Drive API, we need to create a service account to get the credentials. +The credentials will be used to authenticate the application to access the Google Drive API and get the files. + +1. On [APIs & Services](https://console.cloud.google.com/apis/dashboard) page, click the \`Credentials\` menu on the sidebar +2. Click the \`Create Credentials\` button, and choose \`Service account\` +3. Enter your service account name and description, and then click the \`Done\` button +4. You will see the service account you just created on \`Service Account\` table, click the name of the service account to open the details +5. Go to \`Keys\` tab, then click the \`Add Key\` button and choose the \`Create new key\` +6. Pick \`JSON\` as the key type and click the \`Create\` button +7. The JSON file will be downloaded to your computer, and **keep it safe**. We will use it later on the configuration + +_**Note:** The JSON file contains sensitive information, don't share it with anyone_ + +### Create shared folder in Google Drive +Since the service account can't access your Root folder, you need to create a new folder, and share it with the service account. +This folder will be used as the root folder for the application. + +1. Go to [Google Drive](https://drive.google.com/) +2. Click the \`New\` button, and choose \`Folder\` to create a new folder, you can name it anything you want +3. Right-click the folder you just created, and choose \`Share\` +4. Enter the email address of the service account you just created (you can find it on the JSON file, or on the service account details page) +5. To allow download files larger than deployment limit, you need to enable \`Link sharing\` and set it to \`Anyone with the link\` +6. Copy the folder ID from the URL, it's the part after \`/folders/\` in the URL (e.g: https://drive.google.com/drive/u/0/folders/ \`\` ) + +### Configuring the app and Customizing the theme +Now we need to configure the app to use the credentials and folder ID we just created. +You can follow the [App Configuration](#config) and [Customize Theme](#theme) sections to configure the app and customize the theme. + +_**Note:** You can skip the theme customization, but you **NEED** to configure the app_ + +### Deploy the app +On this guide we will use Vercel to deploy the app, but you can use other platforms like Netlify, Heroku, etc. +But don't forget to adjust the \`fileSizeLimit\` on the [configuration](#config) if you use other platforms. + +> Before deploying, make sure you have pushed the changes to your repository + + +1. Go to [Vercel](https://vercel.com/) +2. Click on the \`Add new\` button, and choose \`Project\` +3. Choose the repository you just forked +4. On the \`Environment Variables\` section, copy the whole content from \`.env.local\` you just downloaded from [configuration](#config) section, and paste it on the key fields. It will automatically add all the environment variables needed +5. Click the \`Deploy\` button +6. Wait for the deployment to finish, and open project +7. Go to \`Settings\` tab, and click the \`Functions\` menu, and select your \`Function Region\` to the nearest region to your location for optimal speed +8. Go to \`Deployment\` tab, click the 3 dots on the right side of the latest \`Production\` deployment, and click the \`Redeploy\` button to apply the changes + +For other platforms, you can check their own documentation for Next.js deployment guide. + +### Done! 🎉 +Congratulations! You have successfully deployed the app.`; + +export const migration_guide = `If you've already deployed the app before and want to upgrade from v1, you can follow this guide to migrate the app to the latest version. + +### Update your environment and configuration +If you still have the \`.env.local file\`, you can go to the [configuration](#config) section, and load the file to update the environment variables. +If you don't have it, go to your deployment platform and copy the environment variables from the platform to the [configuration](#config) section. + +You can also load the old \`gindex.config.ts\` file to the [configuration](#config) section to set the default configuration. + +### Update the repository +First, you need to update the repository to the latest version. +If you open your forked repository, you will see a notification that the repository is behind the original repository. +You can sync the repository by clicking the \`Sync fork\` button. +After the repository is updated, you can replace the \`gindex.config.ts\` file with the new one. + +### Update deployment +Now go to your Vercel project page (or other platforms). +Go to the \`Settings\` tab, and click the \`Environment Variables\` menu. +You can delete all the old environment variables, and copy the new environment variables from the updated \`.env.local\` file. +Now you can redeploy the app to apply the changes. +`; + +type Environment = + | "GD_SERVICE_B64" + | "ENCRYPTION_KEY" + | "SITE_PASSWORD" + | "NEXT_PUBLIC_DOMAIN"; +type EnvironmentInput = { + key: Environment; + title: string; + description?: string; + value: string; + onChange: (e: ChangeEvent) => void; + validation?: (value: string) => void; + action?: { + label: string; + onClick: (e: React.MouseEvent) => void; + }; +}; +export function Configuration() { + const [configuration, setConfiguration] = + useState>(config); + const [environment, setEnvironment] = useState>({ + GD_SERVICE_B64: "", + ENCRYPTION_KEY: "", + SITE_PASSWORD: "", + NEXT_PUBLIC_DOMAIN: "", + }); + const [btnState, setBtnState] = useState< + Record + >({ + ENCRYPTION_KEY: "idle", + }); + const [error, setError] = useState>({}); + const fileConfigRef = useRef(null); + const fileEnvRef = useRef(null); + + const envInputs = useMemo( + () => [ + { + key: "ENCRYPTION_KEY", + title: "Encryption Key", + description: + "The encryption key used to encrypt all the sensitive data, must be 16 characters", + value: environment.ENCRYPTION_KEY, + onChange: (e) => { + setEnvironment((prev) => ({ + ...prev, + ENCRYPTION_KEY: e.target.value, + })); + }, + validation(value) { + if (!value) return; + if (value.length !== 16) + throw new Error("Encryption key must be 16 characters"); + }, + action: { + label: "Generate", + onClick: generateEncryption, + }, + }, + ], + [environment], + ); + + async function generateEncryption( + e: React.MouseEvent, + ) { + e.preventDefault(); + setBtnState((prev) => ({ ...prev, ENCRYPTION_KEY: "loading" })); + + try { + const keyStr = await GenerateAESKey(); + setEnvironment((prev) => ({ + ...prev, + ENCRYPTION_KEY: keyStr, + })); + const valid = await VerifyAESKey("This is a test", keyStr); + setError((prev) => ({ + ...prev, + encryption: valid ? "" : "Invalid encryption key", + })); + } catch (error) { + const e = error as Error; + console.error(error); + toast.error(e.message); + } finally { + setBtnState((prev) => ({ ...prev, ENCRYPTION_KEY: "idle" })); + } + } + + return ( + + +
+ + App Configuration + + { + const selectedFile = e.target.files?.[0]; + const expectedFileName = "gindex.config.ts"; + if (selectedFile?.name !== expectedFileName) { + toast.error( + `Invalid file, expected ${expectedFileName} but got ${selectedFile?.name}`, + ); + e.target.value = ""; + return; + } + }} + /> + { + const selectedFile = e.target.files?.[0]; + }} + /> + + + + + + +
fileConfigRef.current?.click()} + > + Load config file + +
+
+ +
fileEnvRef.current?.click()} + > + Load environment file + +
+
+
+
+
+ +
+ +
+

Environment Config

+ +
+ {envInputs.map((input) => ( +
+
+ + {input.description && ( + + + + + +

{input.description}

+
+
+ )} +
+
+
+ { + input.validation?.(e.target.value); + input.onChange(e); + }} + onBlur={async () => { + try { + setError((prev) => ({ + ...prev, + [input.key]: "", + })); + if (!input.value) return; + input.validation?.(input.value); + } catch (error) { + const e = error as Error; + setError((prev) => ({ + ...prev, + [input.key]: e.message, + })); + } + }} + /> + {input.action && ( + + )} +
+ + {error[input.key]} + +
+
+ ))} +
+ +
+
+ ); +} + +export function CustomizeTheme() { + const ColorLabel = ({ + label, + value, + onChange, + }: { + label: string; + value: HslColor; + onChange: (newColor: HslColor) => void; + }) => { + return ( +
+ {label}: + + +
+
+
+ + + + + +
+ ); + }; + + const [colors, setColors] = useState({ + primary: { h: 200, s: 50, l: 50 }, + secondary: { h: 200, s: 50, l: 50 }, + }); + + return ( + + + + Customize Theme + + + + + +
+ +
+ { + setColors((prev) => ({ ...prev, primary: e })); + }} + /> +
+
+ ); +} diff --git a/src/app/[...rest]/deploy/index.tsx b/src/app/[...rest]/deploy/index.tsx new file mode 100644 index 0000000..d5c74eb --- /dev/null +++ b/src/app/[...rest]/deploy/index.tsx @@ -0,0 +1,293 @@ +"use client"; + +import { icons } from "lucide-react"; +import { useMemo, useState } from "react"; +import { cn } from "~/utils"; + +import Markdown from "~/app/@markdown"; +import Icon from "~/components/Icon"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; +import { Separator } from "~/components/ui/separator"; + +import useMediaQuery from "~/hooks/useMediaQuery"; + +import { + Configuration, + CustomizeTheme, + getting_started, + migration_guide, + new_user_guide, +} from "./docs"; + +type Section = "start" | "new-user" | "migrating" | "config" | "theme"; +type SectionItem = { + id: Section; + title: string; + icon: keyof typeof icons; +}; +export default function DeployGuidePage() { + const sectionMenu = useMemo( + () => [ + { + id: "start", + title: "Top of page", + icon: "ChevronUp", + }, + { + id: "new-user", + title: "New User Guide", + icon: "UserPlus", + }, + { + id: "migrating", + title: "Migrating from v1", + icon: "GitBranch", + }, + { + id: "config", + title: "App Configuration", + icon: "Settings", + }, + { + id: "theme", + title: "Theme Customization", + icon: "PaintRoller", + }, + ], + [], + ); + const [sectionOpen, setSectionOpen] = useState(false); + const isDesktop = useMediaQuery("(min-width: 768px)"); + + return ( +
+
+ + + + + + {sectionMenu.map((item) => ( + +
{ + const target = document.getElementById(item.id); + if (target) { + target.scrollIntoView({ behavior: "smooth" }); + } + }} + > + {item.title} + +
+
+ ))} +
+
+ {/* {isDesktop ? ( + + + + + + {sectionMenu.map((item) => ( + + + + ))} + + + ) : ( + + + + + + + Sections + Jump to a section + + +
+ {sectionMenu.map((item) => ( + + + + ))} +
+ + + + + + +
+
+ )} */} +
+ + + + + Deployment Guide + + + + + + + + + + + + New User Guide + + + + + + + + + + + + Migrating from v1 + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/[...rest]/page.tsx b/src/app/[...rest]/page.tsx index 8d9cd9f..2ab287e 100644 --- a/src/app/[...rest]/page.tsx +++ b/src/app/[...rest]/page.tsx @@ -11,6 +11,8 @@ import { decryptData } from "~/utils/encryptionHelper/hash"; import gdrive from "~/utils/gdriveInstance"; import { getFileType } from "~/utils/previewHelper"; +import config from "~/config/gIndex.config"; + import FileBrowser from "../@explorer"; import Header from "../@header"; import HeaderButton from "../@header.button"; @@ -25,6 +27,7 @@ import { GetFiles, GetReadme, } from "../actions"; +import DeployGuidePage from "./deploy"; export const revalidate = 300; export const dynamic = "force-dynamic"; @@ -39,6 +42,9 @@ export async function generateMetadata( { params: { rest } }: Props, parent: ResolvedMetadata, ): Promise { + if (rest[0] === "deploy" && config.showDeployGuide) + return { title: "Deploy Guide" }; + const paths = await CheckPaths(rest); if (!paths.success) return { title: "Not Found" }; @@ -71,6 +77,9 @@ export async function generateMetadata( } export default async function RestPage({ params: { rest } }: Props) { + if (rest[0] === "deploy" && config.showDeployGuide) + return ; + const paths = await CheckPaths(rest); if (!paths.success) notFound(); const unlocked = await CheckPassword(paths.data); diff --git a/src/app/actions.ts b/src/app/actions.ts index 1dc7f91..2749280 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -1,5 +1,6 @@ "use server"; +import crypto from "crypto"; import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; import { z } from "zod"; @@ -699,3 +700,25 @@ export async function CheckDownloadToken(token: string): Promise<{ }; } } +export async function GenerateAESKey(): Promise { + try { + const key = crypto.randomBytes(8).toString("hex"); + return key; + } catch (error) { + const e = error as Error; + console.error(e.message); + throw new Error(e.message); + } +} +export async function VerifyAESKey( + data: string, + key: string, +): Promise { + try { + const encrypt = await encryptData(data, key); + const decrypt = await decryptData(encrypt, key); + return !!encrypt && !!decrypt; + } catch (error) { + return false; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8be26be..40c4f54 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,5 @@ import { Metadata } from "next"; -import { JetBrains_Mono, Source_Sans_3 } from "next/font/google"; +import { JetBrains_Mono, Outfit, Source_Sans_3 } from "next/font/google"; import { cn } from "~/utils"; import config from "~/config/gIndex.config"; @@ -19,6 +19,13 @@ const sourceSans3 = Source_Sans_3({ subsets: ["latin", "latin-ext"], variable: "--font-source-sans-3", }); +const outfit = Outfit({ + weight: ["300", "400", "600", "700"], + style: ["normal"], + display: "auto", + subsets: ["latin", "latin-ext"], + variable: "--font-outfit", +}); const jetbrainsMono = JetBrains_Mono({ weight: ["300", "400", "600", "700"], style: ["normal", "italic"], @@ -94,6 +101,7 @@ export default async function RootLayout({ "h-full bg-background font-sans text-foreground", jetbrainsMono.variable, sourceSans3.variable, + outfit.variable, )} > & { - separator?: React.ReactNode; + separator?: React.ReactNode } ->(({ ...props }, ref) => ( -