+
{title && {title}}
{description && (
{description}
@@ -30,9 +27,9 @@ export function Toaster() {
{action}
- );
+ )
})}
- );
+ )
}
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
index d94aaf7..e0dd4e8 100644
--- a/src/components/ui/tooltip.tsx
+++ b/src/components/ui/tooltip.tsx
@@ -1,14 +1,15 @@
-"use client";
+"use client"
-import * as TooltipPrimitive from "@radix-ui/react-tooltip";
-import * as React from "react";
-import { cn } from "~/utils";
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
-const TooltipProvider = TooltipPrimitive.Provider;
+import { cn } from "~/utils"
-const Tooltip = TooltipPrimitive.Root;
+const TooltipProvider = TooltipPrimitive.Provider
-const TooltipTrigger = TooltipPrimitive.Trigger;
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef,
@@ -19,11 +20,11 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
- className,
+ className
)}
{...props}
/>
-));
-TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts
index f9aa688..d6698ef 100644
--- a/src/components/ui/use-toast.ts
+++ b/src/components/ui/use-toast.ts
@@ -1,75 +1,78 @@
-"use client";
+"use client"
// Inspired by react-hot-toast library
-import * as React from "react";
+import * as React from "react"
-import type { ToastActionElement, ToastProps } from "~/components/ui/toast";
+import type {
+ ToastActionElement,
+ ToastProps,
+} from "~/components/ui/toast"
-const TOAST_LIMIT = 3;
-const TOAST_REMOVE_DELAY = 1000000;
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
- id: string;
- title?: React.ReactNode;
- description?: React.ReactNode;
- action?: ToastActionElement;
-};
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
-} as const;
+} as const
-let count = 0;
+let count = 0
function genId() {
- count = (count + 1) % Number.MAX_SAFE_INTEGER;
- return count.toString();
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
}
-type ActionType = typeof actionTypes;
+type ActionType = typeof actionTypes
type Action =
| {
- type: ActionType["ADD_TOAST"];
- toast: ToasterToast;
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
}
| {
- type: ActionType["UPDATE_TOAST"];
- toast: Partial;
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
}
| {
- type: ActionType["DISMISS_TOAST"];
- toastId?: ToasterToast["id"];
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
}
| {
- type: ActionType["REMOVE_TOAST"];
- toastId?: ToasterToast["id"];
- };
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
interface State {
- toasts: ToasterToast[];
+ toasts: ToasterToast[]
}
-const toastTimeouts = new Map>();
+const toastTimeouts = new Map>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
- return;
+ return
}
const timeout = setTimeout(() => {
- toastTimeouts.delete(toastId);
+ toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
- });
- }, TOAST_REMOVE_DELAY);
+ })
+ }, TOAST_REMOVE_DELAY)
- toastTimeouts.set(toastId, timeout);
-};
+ toastTimeouts.set(toastId, timeout)
+}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
@@ -77,27 +80,27 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
- };
+ }
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
- t.id === action.toast.id ? { ...t, ...action.toast } : t,
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
),
- };
+ }
case "DISMISS_TOAST": {
- const { toastId } = action;
+ const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
- addToRemoveQueue(toastId);
+ addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
- addToRemoveQueue(toast.id);
- });
+ addToRemoveQueue(toast.id)
+ })
}
return {
@@ -108,46 +111,46 @@ export const reducer = (state: State, action: Action): State => {
...t,
open: false,
}
- : t,
+ : t
),
- };
+ }
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
- };
+ }
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
- };
+ }
}
-};
-
-const listeners: Array<(state: State) => void> = [];
-
-let memoryState: State = { toasts: [] };
-
-function dispatch(action: Action) {
- memoryState = reducer(memoryState, action);
- listeners.forEach((listener) => {
- listener(memoryState);
- });
}
-type Toast = Omit;
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
function toast({ ...props }: Toast) {
- const id = genId();
+ const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
- });
- const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
@@ -156,36 +159,36 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
- if (!open) dismiss();
+ if (!open) dismiss()
},
},
- });
+ })
return {
id: id,
dismiss,
update,
- };
+ }
}
function useToast() {
- const [state, setState] = React.useState(memoryState);
+ const [state, setState] = React.useState(memoryState)
React.useEffect(() => {
- listeners.push(setState);
+ listeners.push(setState)
return () => {
- const index = listeners.indexOf(setState);
+ const index = listeners.indexOf(setState)
if (index > -1) {
- listeners.splice(index, 1);
+ listeners.splice(index, 1)
}
- };
- }, [state]);
+ }
+ }, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
- };
+ }
}
-export { toast, useToast };
+export { useToast, toast }
diff --git a/src/config/gIndex.config.ts b/src/config/gIndex.config.ts
index b46f114..860fafb 100644
--- a/src/config/gIndex.config.ts
+++ b/src/config/gIndex.config.ts
@@ -13,14 +13,27 @@ 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
}`,
+ /**
+ * Allow access to the deploy guide
+ * Will use the `/deploy` route, might be overlap with file / folder name
+ *
+ * Set this to false on final deployment
+ *
+ * I'm using this to show the deploy guide on my own demo deployment
+ *
+ * @default false
+ */
+ showDeployGuide: true,
+
/**
* DEPRECATED
* Since in 2.0 we're using server side data fetching, this is not needed anymore.
@@ -52,16 +65,19 @@ const config: z.input = {
// "c760fc0eae9990d4accbc2134af21e45a378d412af2c78020070a9f9ac548b98fe61c4f6be953a8d7be6a035e6f7766c",
rootFolder:
"b76c7c22083307a3aa99c28ab7cc69851d682f5a250d995679d4be5276cab16ab6c37f4d5b7ad1a9b93fb9bf768e752c",
+
/**
- * If your root folder inside a shared drive, set this to true
- * If not, set this to false
+ * If your rootfolder inside a shared drive, you NEED to set this to true
+ * If not, you can set this to false
+ *
+ * You also need to set the shared drive ID to make it work
+ * Make sure you have add your service account to the shared drive since the service account can't access the shared drive by default
*
- * You need to set the shared drive id to make it work
* Where to get the shared drive id?
- * Go to your Shared Drives -> Click on the shared drive -> Copy the id from the url
+ * Go to your Shared Drive > Click on the shared drive > copy the ID from the url
* ex: https://drive.google.com/drive/u/0/folders/:shared_drive_id
*
- * Then encrypt it using `/api/internal/encrypt?q=:shared_drive_id` route
+ * Then you need to encrypt it using `/api/internal/encrypt?q=:shared_drive_id` route
*/
isTeamDrive: true,
sharedDrive:
@@ -181,9 +197,19 @@ 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
* You can also set it to empty array if you don't want to use it
*
* Basic markdown is supported (bold, italic, and link)
@@ -195,6 +221,7 @@ const config: z.input = {
* - {{ author }} will be replaced with author from siteAuthor config above (If it's not set, it will be set to mbaharip)
* - {{ version }} will be replaced with the current version
* - {{ siteName }} will be replaced with the siteName config above
+ * - {{ handle }} will be replaced with the twitter handle from twitterHandle config above
* - {{ creator }} will be replaced with mbaharip if you want to credit me
*/
footer: [
diff --git a/src/hooks/useCopyText.ts b/src/hooks/useCopyText.ts
index 8703b98..84980ed 100644
--- a/src/hooks/useCopyText.ts
+++ b/src/hooks/useCopyText.ts
@@ -1,4 +1,4 @@
-import { toast } from "react-toastify";
+import { toast } from "react-hot-toast";
export default function useCopyText() {
return (text: string) => {
diff --git a/src/schema.ts b/src/schema.ts
index ea3f7af..5aa1e31 100644
--- a/src/schema.ts
+++ b/src/schema.ts
@@ -34,12 +34,58 @@ export const Schema_File = z.object({
.nullable(),
});
-export const Schema_Config = z.object({
- version: z.string(),
+export const Schema_Old_Config = z.object({
+ version: z.literal("1.0.0"),
basePath: z.string(),
+ masterKey: z.string(),
cacheControl: z.string(),
apiConfig: z.object({
+ rootFolder: z.string(),
+ isTeamDrive: z.boolean(),
+ sharedDrive: z.string().optional(),
+ defaultQuery: z.array(z.string()),
+ defaultField: z.string(),
+ defaultOrder: z.string(),
+ itemsPerPage: z.number().positive(),
+ searchRsult: z.number().positive(),
+
+ specialFile: z.object({
+ password: z.string(),
+ readme: z.string(),
+ banner: z.string(),
+ }),
+ hiddenFiles: z.array(z.string()),
+
+ allowDownloadProtectedFile: z.boolean(),
+ temporaryTokenDuration: z.number().positive(),
+ maxFileSize: z.number().positive(),
+ }),
+
+ siteConfig: z.object({
+ siteName: z.string(),
+ siteDescription: z.string(),
+ siteIcon: z.string(),
+ favIcon: z.string(),
+ twitterHandle: z.string().optional().default("@__mbaharip__"),
+
+ defaultAccentColor: z.string(),
+
+ privateIndex: z.boolean().optional().default(false),
+
+ navbarItems: z.array(
+ z.object({
+ icon: z.string(),
+ name: z.string(),
+ href: z.string(),
+ external: z.boolean().optional().default(false),
+ }),
+ ),
+ }),
+});
+
+export const Schema_Config_API = z
+ .object({
rootFolder: z.string(),
isTeamDrive: z.boolean(),
sharedDrive: z.string().optional(),
@@ -60,64 +106,132 @@ export const Schema_Config = z.object({
allowDownloadProtectedFile: z.boolean(),
temporaryTokenDuration: z.number().positive(),
maxFileSize: z.number().positive(),
- }),
+ })
+ .refine(
+ (data) => {
+ if (data.isTeamDrive && !data.sharedDrive) return false;
+ },
+ { message: "sharedDrive is required when isTeamDrive is true" },
+ );
+export const Schema_Config_Site = z.object({
+ siteName: z.string(),
+ siteNameTemplate: z.string().optional().default("%s"),
+ siteDescription: z.string(),
+ siteIcon: z.string(),
+ siteAuthor: z.string().optional().default("mbaharip"),
+ favIcon: z.string(),
+ robots: z.string().optional().default("noindex, nofollow"),
+ twitterHandle: z.string().optional().default("@__mbaharip__"),
- siteConfig: z.object({
- siteName: z.string(),
- siteNameTemplate: z.string().optional().default("%s"),
- siteDescription: z.string(),
- siteIcon: z.string(),
- siteAuthor: z.string().optional().default("mbaharip"),
- favIcon: z.string(),
- 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()))
- .optional()
- .default([
- "{{ year }}",
- "{{ repository }}",
- "{{ author }}",
- "{{ version }}",
- "{{ siteName }}",
- "{{ creator }}",
+ footer: z.string().array().optional(),
+
+ privateIndex: z.boolean().optional().default(false),
+ breadcrumbMax: z.number(),
+
+ toaster: z
+ .object({
+ position: z.enum([
+ "top-left",
+ "top-right",
+ "bottom-left",
+ "bottom-right",
]),
+ duration: z.number().positive(),
+ })
+ .optional()
+ .default({
+ position: "top-right",
+ duration: 5000,
+ }),
- privateIndex: z.boolean().optional().default(false),
- breadcrumbMax: z.number(),
-
- toaster: z
- .object({
- position: z.enum([
- "top-left",
- "top-right",
- "bottom-left",
- "bottom-right",
- ]),
- duration: z.number().positive(),
- })
- .optional()
- .default({
- position: "top-right",
- duration: 5000,
- }),
-
- navbarItems: z.array(
- z.object({
- icon: z.enum(Object.keys(icons) as [keyof typeof icons]),
- name: z.string(),
- href: z.string(),
- external: z.boolean().optional().default(false),
- }),
- ),
- supports: z.array(
- z.object({
- name: z.string(),
- currency: z.string(),
- href: z.string(),
- }),
- ),
- }),
+ navbarItems: z.array(
+ z.object({
+ icon: z.enum(Object.keys(icons) as [keyof typeof icons]),
+ name: z.string(),
+ href: z.string(),
+ external: z.boolean().optional().default(false),
+ }),
+ ),
+ supports: z.array(
+ z.object({
+ name: z.string(),
+ currency: z.string(),
+ href: z.string(),
+ }),
+ ),
+});
+export const Schema_App_Configuration_Env = z.object({
+ GD_SERVICE_B64: z.string(),
+ ENCRYPTION_KEY: z.string(),
+ SITE_PASSWORD: z.string().optional(),
+ NEXT_PUBLIC_DOMAIN: z.string().optional(),
+});
+
+export const Schema_Config = z.object({
+ version: z.string(),
+ basePath: z.string(),
+ cacheControl: z.string(),
+ showDeployGuide: z.boolean(),
+
+ apiConfig: Schema_Config_API,
+
+ siteConfig: Schema_Config_Site,
+});
+
+export const Schema_ServiceAccount = z.object({
+ type: z.literal("service_account"),
+ project_id: z.string(),
+ private_key_id: z.string(),
+ private_key: z.string(),
+ client_email: z.string().email("Invalid client_email field"),
+ client_id: z.string(),
+ auth_uri: z.string().url(),
+ token_uri: z.string().url(),
+ auth_provider_x509_cert_url: z.string().url(),
+ client_x509_cert_url: z.string().url(),
+ universe_domain: z.string().optional(),
+});
+
+export const Schema_App_Configuration = z.object({
+ environment: Schema_App_Configuration_Env,
+ api: Schema_Config_API,
+ site: Schema_Config_Site,
+});
+
+export type ConfigurationCategory = keyof z.infer<
+ typeof Schema_App_Configuration
+>;
+export type ConfigurationKeys<
+ T extends keyof z.infer,
+> = keyof z.infer[T];
+export type ConfigurationValue<
+ T extends keyof z.infer,
+ K extends keyof z.infer[T],
+> = z.infer[T][K];
+
+export type ConfigState = "idle" | "loading";
+
+export const Schema_Theme = z.object({
+ "background": z.string(),
+ "foreground": z.string(),
+ "card": z.string(),
+ "card-foreground": z.string(),
+ "popover": z.string(),
+ "popover-foreground": z.string(),
+ "primary": z.string(),
+ "primary-foreground": z.string(),
+ "secondary": z.string(),
+ "secondary-foreground": z.string(),
+ "muted": z.string(),
+ "muted-foreground": z.string(),
+ "accent": z.string(),
+ "accent-foreground": z.string(),
+ "destructive": z.string(),
+ "destructive-foreground": z.string(),
+ "border": z.string(),
+ "input": z.string(),
+ "ring": z.string(),
+ "radius": z.string(),
});
diff --git a/src/utils/encryptionHelper/hash.ts b/src/utils/encryptionHelper/hash.ts
index 8025133..64c0a56 100644
--- a/src/utils/encryptionHelper/hash.ts
+++ b/src/utils/encryptionHelper/hash.ts
@@ -14,11 +14,14 @@ const generateKey = () => {
return data;
};
const key = generateKey();
-const iv = Buffer.from(key);
-export async function encryptData(data: string): Promise {
+export async function encryptData(
+ data: string,
+ encryptKey: string = key,
+): Promise {
try {
- const cipher = crypto.createCipheriv("aes-128-cbc", key, iv);
+ const ivKey = Buffer.from(key);
+ const cipher = crypto.createCipheriv("aes-128-cbc", encryptKey, ivKey);
return Buffer.concat([
cipher.update(data, "utf-8"),
cipher.final(),
@@ -30,9 +33,13 @@ export async function encryptData(data: string): Promise {
}
}
-export async function decryptData(hash: string): Promise {
+export async function decryptData(
+ hash: string,
+ encryptKey: string = key,
+): Promise {
try {
- const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
+ const ivKey = Buffer.from(key);
+ const decipher = crypto.createDecipheriv("aes-128-cbc", encryptKey, ivKey);
return Buffer.concat([
decipher.update(hash, "hex"),
diff --git a/src/utils/footerFormatter.ts b/src/utils/footerFormatter.ts
new file mode 100644
index 0000000..5039b3d
--- /dev/null
+++ b/src/utils/footerFormatter.ts
@@ -0,0 +1,19 @@
+import config from "~/config/gIndex.config";
+
+export function formatFooter(text: string[]): string {
+ return text
+ .join("\n")
+ .replaceAll("{{ year }}", new Date().getFullYear().toString())
+ .replaceAll(
+ "{{ repository }}",
+ "[Repository](https://github.com/mbaharip/next-gdrive-index)",
+ )
+ .replaceAll("{{ author }}", config.siteConfig.siteAuthor || "mbaharip")
+ .replaceAll("{{ version }}", config.version || "0.0.0")
+ .replaceAll("{{ siteName }}", config.siteConfig.siteName)
+ .replaceAll(
+ "{{ handle }}",
+ config.siteConfig.twitterHandle || "@__mbaharip__",
+ )
+ .replaceAll("{{ creator }}", "mbaharip");
+}
diff --git a/src/utils/parseConfigFile.ts b/src/utils/parseConfigFile.ts
new file mode 100644
index 0000000..cd80d6f
--- /dev/null
+++ b/src/utils/parseConfigFile.ts
@@ -0,0 +1,65 @@
+// Used for configuration on deploy guide page
+import { z } from "zod";
+import {
+ Schema_Config,
+ Schema_Config_API,
+ Schema_Config_Site,
+ Schema_Old_Config,
+} from "~/schema";
+
+export function parseConfigFile(config: string):
+ | {
+ api: z.infer;
+ site: z.infer;
+ }
+ | {
+ success: false;
+ message: string;
+ } {
+ try {
+ // Parse string to JSON
+ const configuration = config
+ .split(/const config:\s.*?=\s/g)[1]
+ .split("export default config;")[0]
+ .replace(/\\/g, "") // Remove all escape backslashes
+
+ .replace(/\/\*[\s\S]*?\*\//g, "") // Remove all multi-line comments
+ .replace(/,\s\/\/\s.*/g, ",") // Remove comments after values
+ .replace(/[^,]\/\/\s.*?,/g, "") // Remove single line comments
+
+ .replace(/\r\n/g, "")
+ .replace(/\n/g, "")
+ .replace(/\t/g, "") // Remove line breaks and tabs
+
+ .replace(/basePath:(.*?),/g, 'basePath: "placeholder-domain",') // Replace basePath variable with placeholder
+ .replace(/maxFileSize:(.*?),/g, "maxFileSize: 4194304,") // Set maxFileSize to 4MB
+
+ .replace(/([a-zA-Z]*?):\s/g, '"$1": ') // Add double quotes to keys
+ .trim()
+ .slice(0, -1)
+
+ .replace(/\s{2,4}|/g, "") // Replace all double+ spaces with single space
+ .replace(/,(?=[^,]*$)/, "") // Remove trailing comma
+ .replace(/(,\])/g, "]") // Remove trailing comma before closing bracket
+ .replace(/(,\})/g, "}"); // Remove trailing comma before closing brace
+
+ const parseJSON = JSON.parse(configuration);
+ const version: string | undefined = parseJSON.version;
+ if (!version)
+ throw new Error(
+ "Version not found, please check your configuration file.",
+ );
+
+ const data = parseJSON as
+ | z.infer
+ | z.infer;
+
+ return {
+ api: data.apiConfig as z.infer,
+ site: data.siteConfig as z.infer,
+ };
+ } catch (error) {
+ const e = error as Error;
+ return { success: false, message: e.message };
+ }
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 62c9ba0..83d0966 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -20,7 +20,7 @@ const config: Config = {
},
extend: {
fontFamily: {
- sans: ["var(--font-source-sans-3)", ...tw.fontFamily.sans],
+ sans: ["var(--font-outfit)", ...tw.fontFamily.sans],
mono: ["var(--font-jetbrains-mono)", ...tw.fontFamily.mono],
},
colors: {
diff --git a/yarn.lock b/yarn.lock
index e6dc034..de412a0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1894,6 +1894,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/mdast@npm:^4.0.0":
+ version: 4.0.3
+ resolution: "@types/mdast@npm:4.0.3"
+ dependencies:
+ "@types/unist": "npm:*"
+ checksum: 10c0/e6994404f5ce58073aa6c1a37ceac3060326470a464e2d751580a9f89e2dbca3a2a6222b849bdaaa5bffbe89033c50a886d17e49fca3b040a4ffcf970e387a0c
+ languageName: node
+ linkType: hard
+
"@types/mime-types@npm:^2.1.1":
version: 2.1.4
resolution: "@types/mime-types@npm:2.1.4"
@@ -2006,6 +2015,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/unist@npm:*, @types/unist@npm:^3.0.0":
+ version: 3.0.2
+ resolution: "@types/unist@npm:3.0.2"
+ checksum: 10c0/39f220ce184a773c55c18a127062bfc4d0d30c987250cd59bab544d97be6cfec93717a49ef96e81f024b575718f798d4d329eb81c452fc57d6d051af8b043ebf
+ languageName: node
+ linkType: hard
+
"@types/unist@npm:^2, @types/unist@npm:^2.0.0":
version: 2.0.10
resolution: "@types/unist@npm:2.0.10"
@@ -3059,6 +3075,15 @@ __metadata:
languageName: node
linkType: hard
+"devlop@npm:^1.0.0":
+ version: 1.1.0
+ resolution: "devlop@npm:1.1.0"
+ dependencies:
+ dequal: "npm:^2.0.0"
+ checksum: 10c0/e0928ab8f94c59417a2b8389c45c55ce0a02d9ac7fd74ef62d01ba48060129e1d594501b77de01f3eeafc7cb00773819b0df74d96251cf20b31c5b3071f45c0e
+ languageName: node
+ linkType: hard
+
"didyoumean@npm:^1.2.2":
version: 1.2.2
resolution: "didyoumean@npm:1.2.2"
@@ -3130,31 +3155,31 @@ __metadata:
languageName: node
linkType: hard
-"embla-carousel-react@npm:^8.0.1":
- version: 8.0.1
- resolution: "embla-carousel-react@npm:8.0.1"
+"embla-carousel-react@npm:^8.0.2":
+ version: 8.0.2
+ resolution: "embla-carousel-react@npm:8.0.2"
dependencies:
- embla-carousel: "npm:8.0.1"
- embla-carousel-reactive-utils: "npm:8.0.1"
+ embla-carousel: "npm:8.0.2"
+ embla-carousel-reactive-utils: "npm:8.0.2"
peerDependencies:
react: ^16.8.0 || ^17.0.1 || ^18.0.0
- checksum: 10c0/a16af76be911133f00ff38491b0ed12f09571949234b22bfe83cbd2d8b0d3cf43b666ffc4fc6aaf17e04691495fdad69fc1bc33db3eeec9ba7f67fe8db056a25
+ checksum: 10c0/7e45266d4251a960515a3283e65454b61e54d6f400f2dee7f56a3ac50532f7ce4735d926494cb1570d81de4bf299c3d1417ab4e64467cda139a7f60b49583757
languageName: node
linkType: hard
-"embla-carousel-reactive-utils@npm:8.0.1":
- version: 8.0.1
- resolution: "embla-carousel-reactive-utils@npm:8.0.1"
+"embla-carousel-reactive-utils@npm:8.0.2":
+ version: 8.0.2
+ resolution: "embla-carousel-reactive-utils@npm:8.0.2"
peerDependencies:
- embla-carousel: 8.0.1
- checksum: 10c0/c511dbbcd869f11f102e826aea600f1668f8097792b4e185678f44240466fe6a224b68833c408bd9eb6c9b26e40321b6f18f70f45bd19df3cd403d964e1fba95
+ embla-carousel: 8.0.2
+ checksum: 10c0/e7e81916971008642700af0b96a59117324214c020759a53feb1d96edb34649329018777dc00b87c631c9d04efd544945f5914dc3cbd5289766a6cdcea253f4e
languageName: node
linkType: hard
-"embla-carousel@npm:8.0.1":
- version: 8.0.1
- resolution: "embla-carousel@npm:8.0.1"
- checksum: 10c0/9ce30759a77e75ff4ce490102c429794fd46f03bbcc4a6af4ecefbe55a5de5289b7ac0f7607d1774a9b23c241d5781bf3d45459590768b15679a9da5b56ef6df
+"embla-carousel@npm:8.0.2":
+ version: 8.0.2
+ resolution: "embla-carousel@npm:8.0.2"
+ checksum: 10c0/e63ce4e387c0e227ce211a1131f81c74dbe6c4bb32a5d072e0c1e30774305ca9a30fc86ebb042707bf8d6ec8cb57575628dfa1b18b36dd3206d4774cd34b73bc
languageName: node
linkType: hard
@@ -5350,6 +5375,18 @@ __metadata:
languageName: node
linkType: hard
+"mdast-util-find-and-replace@npm:^3.0.0":
+ version: 3.0.1
+ resolution: "mdast-util-find-and-replace@npm:3.0.1"
+ dependencies:
+ "@types/mdast": "npm:^4.0.0"
+ escape-string-regexp: "npm:^5.0.0"
+ unist-util-is: "npm:^6.0.0"
+ unist-util-visit-parents: "npm:^6.0.0"
+ checksum: 10c0/1faca98c4ee10a919f23b8cc6d818e5bb6953216a71dfd35f51066ed5d51ef86e5063b43dcfdc6061cd946e016a9f0d44a1dccadd58452cf4ed14e39377f00cb
+ languageName: node
+ linkType: hard
+
"mdast-util-from-markdown@npm:^1.0.0":
version: 1.3.1
resolution: "mdast-util-from-markdown@npm:1.3.1"
@@ -5451,6 +5488,16 @@ __metadata:
languageName: node
linkType: hard
+"mdast-util-newline-to-break@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "mdast-util-newline-to-break@npm:2.0.0"
+ dependencies:
+ "@types/mdast": "npm:^4.0.0"
+ mdast-util-find-and-replace: "npm:^3.0.0"
+ checksum: 10c0/756a5660b0a821e0d6d6a0b2d9b13ac32e41cc028c485a91bccf6300977e2557236c6cc93dbd55c68b785f1ed6eae69209a4ffe182533cd1cdfda369021bebd2
+ languageName: node
+ linkType: hard
+
"mdast-util-phrasing@npm:^3.0.0":
version: 3.0.1
resolution: "mdast-util-phrasing@npm:3.0.1"
@@ -6170,7 +6217,7 @@ __metadata:
clsx: "npm:^2.1.0"
cmdk: "npm:^1.0.0"
date-fns: "npm:^3.6.0"
- embla-carousel-react: "npm:^8.0.1"
+ embla-carousel-react: "npm:^8.0.2"
encoding: "npm:^0.1.13"
eslint: "npm:8.38.0"
eslint-config-next: "npm:^14.1.4"
@@ -6186,6 +6233,7 @@ __metadata:
prettier: "npm:3.0.0"
prettier-plugin-tailwindcss: "npm:0.5.12"
react: "npm:^18"
+ react-colorful: "npm:^5.6.1"
react-day-picker: "npm:^8.10.0"
react-dom: "npm:^18"
react-h5-audio-player: "npm:^3.9.1"
@@ -6200,6 +6248,7 @@ __metadata:
rehype-katex: "npm:^6.0.3"
rehype-prism-plus: "npm:^1.6.3"
rehype-raw: "npm:6.1.1"
+ remark-breaks: "npm:^4.0.0"
remark-gfm: "npm:^3.0.1"
remark-math: "npm:^5.1.1"
remark-slug: "npm:^7.0.1"
@@ -6209,6 +6258,7 @@ __metadata:
tailwindcss: "npm:^3.4.1"
tailwindcss-animate: "npm:^1.0.7"
typescript: "npm:^5"
+ use-debouncy: "npm:^5.0.1"
vaul: "npm:^0.9.0"
zod: "npm:^3.22.4"
languageName: unknown
@@ -6951,6 +7001,16 @@ __metadata:
languageName: node
linkType: hard
+"react-colorful@npm:^5.6.1":
+ version: 5.6.1
+ resolution: "react-colorful@npm:5.6.1"
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: ">=16.8.0"
+ checksum: 10c0/48eb73cf71e10841c2a61b6b06ab81da9fffa9876134c239bfdebcf348ce2a47e56b146338e35dfb03512c85966bfc9a53844fc56bc50154e71f8daee59ff6f0
+ languageName: node
+ linkType: hard
+
"react-day-picker@npm:^8.10.0":
version: 8.10.0
resolution: "react-day-picker@npm:8.10.0"
@@ -7330,6 +7390,17 @@ __metadata:
languageName: node
linkType: hard
+"remark-breaks@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "remark-breaks@npm:4.0.0"
+ dependencies:
+ "@types/mdast": "npm:^4.0.0"
+ mdast-util-newline-to-break: "npm:^2.0.0"
+ unified: "npm:^11.0.0"
+ checksum: 10c0/d7b319a7993b54c5d574e9255080c5de68cfa24f993873b0ee296af13f478521c41d4b7ae0fc14b4607ea70c8f6967e998ab7a467de13139141e66a1a34cb6be
+ languageName: node
+ linkType: hard
+
"remark-gfm@npm:^3.0.1":
version: 3.0.1
resolution: "remark-gfm@npm:3.0.1"
@@ -8313,6 +8384,21 @@ __metadata:
languageName: node
linkType: hard
+"unified@npm:^11.0.0":
+ version: 11.0.4
+ resolution: "unified@npm:11.0.4"
+ dependencies:
+ "@types/unist": "npm:^3.0.0"
+ bail: "npm:^2.0.0"
+ devlop: "npm:^1.0.0"
+ extend: "npm:^3.0.0"
+ is-plain-obj: "npm:^4.0.0"
+ trough: "npm:^2.0.0"
+ vfile: "npm:^6.0.0"
+ checksum: 10c0/b550cdc994d54c84e2e098eb02cfa53535cbc140c148aa3296f235cb43082b499d239110f342fa65eb37ad919472a93cc62f062a83541485a69498084cc87ba1
+ languageName: node
+ linkType: hard
+
"unique-filename@npm:^3.0.0":
version: 3.0.0
resolution: "unique-filename@npm:3.0.0"
@@ -8368,6 +8454,15 @@ __metadata:
languageName: node
linkType: hard
+"unist-util-is@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "unist-util-is@npm:6.0.0"
+ dependencies:
+ "@types/unist": "npm:^3.0.0"
+ checksum: 10c0/9419352181eaa1da35eca9490634a6df70d2217815bb5938a04af3a662c12c5607a2f1014197ec9c426fbef18834f6371bfdb6f033040fa8aa3e965300d70e7e
+ languageName: node
+ linkType: hard
+
"unist-util-position@npm:^4.0.0":
version: 4.0.4
resolution: "unist-util-position@npm:4.0.4"
@@ -8396,6 +8491,15 @@ __metadata:
languageName: node
linkType: hard
+"unist-util-stringify-position@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "unist-util-stringify-position@npm:4.0.0"
+ dependencies:
+ "@types/unist": "npm:^3.0.0"
+ checksum: 10c0/dfe1dbe79ba31f589108cb35e523f14029b6675d741a79dea7e5f3d098785045d556d5650ec6a8338af11e9e78d2a30df12b1ee86529cded1098da3f17ee999e
+ languageName: node
+ linkType: hard
+
"unist-util-visit-parents@npm:^5.0.0, unist-util-visit-parents@npm:^5.1.1":
version: 5.1.3
resolution: "unist-util-visit-parents@npm:5.1.3"
@@ -8406,6 +8510,16 @@ __metadata:
languageName: node
linkType: hard
+"unist-util-visit-parents@npm:^6.0.0":
+ version: 6.0.1
+ resolution: "unist-util-visit-parents@npm:6.0.1"
+ dependencies:
+ "@types/unist": "npm:^3.0.0"
+ unist-util-is: "npm:^6.0.0"
+ checksum: 10c0/51b1a5b0aa23c97d3e03e7288f0cdf136974df2217d0999d3de573c05001ef04cccd246f51d2ebdfb9e8b0ed2704451ad90ba85ae3f3177cf9772cef67f56206
+ languageName: node
+ linkType: hard
+
"unist-util-visit@npm:^4.0.0":
version: 4.1.2
resolution: "unist-util-visit@npm:4.1.2"
@@ -8462,6 +8576,15 @@ __metadata:
languageName: node
linkType: hard
+"use-debouncy@npm:^5.0.1":
+ version: 5.0.1
+ resolution: "use-debouncy@npm:5.0.1"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ checksum: 10c0/d5d2b5f330c161ed6de4045a6f9499b459366c7f6902ed4200c7ee5f88491969e48b5dee180d2a1028a744b360cfdf59ff66b5620a01dc52f01ba106d2577695
+ languageName: node
+ linkType: hard
+
"use-sidecar@npm:^1.1.2":
version: 1.1.2
resolution: "use-sidecar@npm:1.1.2"
@@ -8540,6 +8663,16 @@ __metadata:
languageName: node
linkType: hard
+"vfile-message@npm:^4.0.0":
+ version: 4.0.2
+ resolution: "vfile-message@npm:4.0.2"
+ dependencies:
+ "@types/unist": "npm:^3.0.0"
+ unist-util-stringify-position: "npm:^4.0.0"
+ checksum: 10c0/07671d239a075f888b78f318bc1d54de02799db4e9dce322474e67c35d75ac4a5ac0aaf37b18801d91c9f8152974ea39678aa72d7198758b07f3ba04fb7d7514
+ languageName: node
+ linkType: hard
+
"vfile@npm:^5.0.0":
version: 5.3.7
resolution: "vfile@npm:5.3.7"
@@ -8552,6 +8685,17 @@ __metadata:
languageName: node
linkType: hard
+"vfile@npm:^6.0.0":
+ version: 6.0.1
+ resolution: "vfile@npm:6.0.1"
+ dependencies:
+ "@types/unist": "npm:^3.0.0"
+ unist-util-stringify-position: "npm:^4.0.0"
+ vfile-message: "npm:^4.0.0"
+ checksum: 10c0/443bda43e5ad3b73c5976e987dba2b2d761439867ba7d5d7c5f4b01d3c1cb1b976f5f0e6b2399a00dc9b4eaec611bd9984ce9ce8a75a72e60aed518b10a902d2
+ languageName: node
+ linkType: hard
+
"warning@npm:^4.0.2":
version: 4.0.3
resolution: "warning@npm:4.0.3"