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] =?UTF-8?q?=F0=9F=96=A5=EF=B8=8F=20wip:=20Config=20screen?= 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) => ( -