diff --git a/src/app/[...rest]/ConfigurationPage.Api.tsx b/src/app/[...rest]/ConfigurationPage.Api.tsx new file mode 100644 index 0000000..4c3e7b9 --- /dev/null +++ b/src/app/[...rest]/ConfigurationPage.Api.tsx @@ -0,0 +1,666 @@ +import { useState } from "react"; +import { toast } from "sonner"; + +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; +import Icon from "~/components/ui/icon"; +import { Input } from "~/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; +import { Switch } from "~/components/ui/switch"; + +import { cn } from "~/lib/utils"; + +import { FormColumn, FormProps, FormSection } from "./ConfiguratorPage"; + +export default function ApiForm({ form, onResetField }: FormProps) { + const [inputHiddenFile, setInputHiddenFile] = useState(""); + function onHiddenFileSubmit() { + if (!inputHiddenFile) { + toast.error("File name cannot be empty"); + setInputHiddenFile(""); + return; + } + const prevValue = form.watch("api.hiddenFiles"); + const value = inputHiddenFile.trim(); + if (prevValue.some((v) => v.trim() === value)) { + toast.error("File name already exists in the hidden files list"); + setInputHiddenFile(""); + return; + } + form.setValue("api.hiddenFiles", [...form.watch("api.hiddenFiles"), inputHiddenFile], { + shouldDirty: true, + }); + setInputHiddenFile(""); + } + + return ( + + + ( + + { + onResetField?.("api.cache.public"); + }} + > + Public Cache + + + + + Enable public cache for the index. + + + )} + /> + ( + + { + onResetField?.("api.cache.staleWhileRevalidate"); + }} + > + Stale While Revalidate + + + + + Return stale data before requesting fresh data. + + + )} + /> + ( + + { + onResetField?.("api.cache.maxAge"); + }} + > + Max Age + + + + + How long should the cache in browser last. + + + )} + /> + ( + + { + onResetField?.("api.cache.sMaxAge"); + }} + > + Shared Max Age + + + + + How long should the cache in server last. + + + )} + /> + + + ( + + { + onResetField?.("api.rootFolder"); + }} + > + Root Folder ID + + + + + Google Drive folder ID to be used as the root folder. + + + )} + /> + ( + +
+ { + onResetField?.("api.sharedDrive"); + form.setValue("api.isTeamDrive", false); + }} + > + Shared Drive ID + + { + form.setValue("api.isTeamDrive", value); + }} + /> +
+ + + + Google Drive Shared Drive ID. + +
+ )} + /> + + ( + + { + onResetField?.("api.itemsPerPage"); + }} + > + Items Per Page + + + + + Number of items to show before pagination. + + + )} + /> + ( + + { + onResetField?.("api.searchResult"); + }} + > + Search Result Limit + + + + + Number of search results to show. + + + )} + /> + + + ( + + { + onResetField?.("api.specialFile.banner"); + }} + > + Banner File + + + + + Will be used as the banner if found in the folder. + + + )} + /> + ( + + { + onResetField?.("api.specialFile.password"); + }} + > + Password File + + + + + Will be used to protect the folder. + + + )} + /> + ( + + { + onResetField?.("api.specialFile.readme"); + }} + > + Readme File + + + + + Will be used as the description if found in the folder. + + + )} + /> + + { + const watch = form.watch("api.hiddenFiles"); + + return ( + + { + onResetField?.("api.hiddenFiles"); + }} + > + Hidden Files + +
+
+ {watch.length ? ( + <> +
+ {watch.map((name, index) => ( + { + form.setValue( + "api.hiddenFiles", + watch.filter((_, i) => i !== index), + { + shouldDirty: true, + }, + ); + }} + > + {name} + + ))} + Click to remove hidden file +
+ + ) : ( + + All files including special files are visible. + + )} +
+ +
+ setInputHiddenFile(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + onHiddenFileSubmit(); + } + }} + /> + +
+
+ { + if (watch.includes(form.watch("api.specialFile.banner"))) { + return; + } + form.setValue("api.hiddenFiles", [...watch, form.watch("api.specialFile.banner")]); + }} + > + + {watch.includes(form.watch("api.specialFile.banner")) + ? "Banner file is hidden" + : "Add banner file to the list"} + + + { + if (watch.includes(form.watch("api.specialFile.password"))) { + return; + } + form.setValue("api.hiddenFiles", [...watch, form.watch("api.specialFile.password")]); + }} + > + + {watch.includes(form.watch("api.specialFile.password")) + ? "Password file is hidden" + : "Add password file to the list"} + + + { + if (watch.includes(form.watch("api.specialFile.readme"))) { + return; + } + form.setValue("api.hiddenFiles", [...watch, form.watch("api.specialFile.readme")]); + }} + > + + {watch.includes(form.watch("api.specialFile.readme")) + ? "Readme file is hidden" + : "Add readme file to the list"} + +
+
+ Click the badge to add special files to the hidden files list. + +
+ ); + }} + /> + + ( + + { + onResetField?.("api.proxyThumbnail"); + }} + > + Proxy Thumbnail + + + + + Serve thumbnail through API route to avoid CORS issue. + + + )} + /> + + ( + + { + onResetField?.("api.streamMaxSize"); + }} + > + Preview Max Size + + + { + const value = Number(e.target.value ?? "0"); + form.setValue("api.streamMaxSize", value * 1024 * 1024, { + shouldDirty: true, + }); + }} + /> + + + Maximum file size to be previewed in the browser in MB. Larger file won't be previewed. +
+ Will count towards the deployment bandwidth usage.{" "} + Set to 0 to disable the limit +
+ +
+ )} + /> + ( + + { + onResetField?.("api.maxFileSize"); + }} + > + Max File Size + + + { + const value = Number(e.target.value ?? "0"); + form.setValue("api.maxFileSize", value * 1024 * 1024, { + shouldDirty: true, + }); + }} + /> + + + Maximum file size that can be downloaded via API route. Larger file will be using GDrive link. +
+ Will count towards the deployment bandwidth usage.{" "} + Set to 0 to disable the limit +
+ +
+ )} + /> + ( + + { + onResetField?.("api.allowDownloadProtectedFile"); + }} + > + Download Protected File + + + + + Allow download for file inside protected folder. + + + )} + /> + ( + + { + onResetField?.("api.temporaryTokenDuration"); + }} + > + Temporary Token Duration + + + + + + Duration for temporary token in hours. Token will be used for protected folder download. + + + + )} + /> +
+
+ ); +} diff --git a/src/app/[...rest]/ConfigurationPage.Site.tsx b/src/app/[...rest]/ConfigurationPage.Site.tsx new file mode 100644 index 0000000..4264458 --- /dev/null +++ b/src/app/[...rest]/ConfigurationPage.Site.tsx @@ -0,0 +1,912 @@ +import Link from "next/link"; +import { useMemo, useState } from "react"; +import { useFieldArray } from "react-hook-form"; +import ReactMarkdown from "react-markdown"; +import remarkBreaks from "remark-breaks"; +import { toast } from "sonner"; +import { z } from "zod"; + +import { Button } from "~/components/ui/button"; +import { VirtualizedCombobox } from "~/components/ui/combobox.virtualized"; +import { + ResponsiveDialog, + ResponsiveDialogBody, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, + ResponsiveDialogTrigger, +} from "~/components/ui/dialog.responsive"; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; +import Icon, { IconNamesArray } from "~/components/ui/icon"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; +import { Separator } from "~/components/ui/separator"; +import { Textarea } from "~/components/ui/textarea"; + +import { useResponsive } from "~/context/responsiveContext"; +import { cn, formatFooterContent } from "~/lib/utils"; + +import { Schema_App_Configuration } from "~/types/schema"; + +import { FormColumn, FormProps, FormSection } from "./ConfiguratorPage"; + +export default function SiteForm({ form, onResetField }: FormProps) { + return ( + + ( + + { + onResetField?.("site.privateIndex"); + }} + > + Private Index + + + + + + Enable to require a password to access the site.{" "} + Make sure to set a password in the environment section. + + + + )} + /> + ( + + { + onResetField?.("site.guideButton"); + }} + > + Internal Menu + + + + + Show internal menu on the navbar. + + + )} + /> + + ( + + { + onResetField?.("site.siteName"); + }} + > + Site Name + + + + + Site name for meta and navbar. + + + )} + /> + ( + + { + onResetField?.("site.siteNameTemplate"); + }} + > + Site Name Template + + + + + + Template for the site name. %t for site name and{" "} + %s for page title. + + + + )} + /> + +
+ + {(form.watch("site.siteNameTemplate") ?? "No template") + .replace("%t", form.watch("site.siteName") ?? "next-gdrive-index") + .replace("%s", "Page Title Goes Here")} + +
+
+ ( + + { + onResetField?.("site.siteDescription"); + }} + > + Site Description + + +