返回 presentation-ai
utils.ts
根目录 / src / lib / utils.ts
1 import { Buffer } from "buffer";
2 import { type ClassValue, clsx } from "clsx";
3 import { customAlphabet } from "nanoid";
4 import { twMerge } from "tailwind-merge";
5
6 export function cn(...inputs: ClassValue[]) {
7 return twMerge(clsx(inputs));
8 }
9
10 /**
11 * Creates a consistent date key string in YYYY-MM-DD format
12 * This ensures timezone-independent date operations
13 * @param date The date to create a key for
14 * @returns A string in YYYY-MM-DD format
15 */
16 export function getConsistentDateKey(date: Date): string {
17 return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
18 }
19
20 export async function sleep(ms: number) {
21 return new Promise((resolve) => setTimeout(resolve, ms));
22 }
23
24 export function safeJsonParse(
25 jsonString: string,
26 ): Record<string, unknown> | null {
27 try {
28 return JSON.parse(jsonString) as Record<string, unknown>;
29 } catch (error) {
30 console.error("Failed to parse JSON:", error);
31 return null; // or you can return a default value
32 }
33 }
34
35 export function haversineDistance(
36 lat1: number,
37 lon1: number,
38 lat2: number,
39 lon2: number,
40 ): number {
41 const toRadians = (degrees: number) => degrees * (Math.PI / 180);
42
43 const R = 6371; // Radius of the Earth in kilometers
44 const dLat = toRadians(lat2 - lat1);
45 const dLon = toRadians(lon2 - lon1);
46
47 const a =
48 Math.sin(dLat / 2) * Math.sin(dLat / 2) +
49 Math.cos(toRadians(lat1)) *
50 Math.cos(toRadians(lat2)) *
51 Math.sin(dLon / 2) *
52 Math.sin(dLon / 2);
53
54 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
55
56 return R * c; // Distance in kilometers
57 }
58
59 export function extractDomain(url: string): string {
60 try {
61 const hostname = new URL(url).hostname;
62 return hostname;
63 } catch (error) {
64 console.error("Invalid URL:", error);
65 return url;
66 }
67 }
68
69 export const formatPrice = (price: number) => {
70 return price.toLocaleString("en-US", {
71 style: "currency",
72 currency: "USD",
73 currencyDisplay: "symbol",
74 });
75 };
76
77 export function capitalizeFirstLetter(text: string): string {
78 if (!text) return text;
79 return text.charAt(0).toUpperCase() + text.slice(1);
80 }
81
82 export function formatNumber(num: number): string {
83 if (num < 1000) return num.toString();
84
85 const units = ["k", "M", "B", "T"];
86 const unitIndex = Math.floor(Math.log10(num) / 3) - 1;
87 const formattedNum = (num / 1000 ** (unitIndex + 1)).toFixed(1);
88
89 return `${formattedNum}${units[unitIndex]}`;
90 }
91
92 export const formatMarketCap = (value: number) => {
93 if (value >= 1_000_000) {
94 return `${(value / 1_000_000).toFixed(2)} T`;
95 } else if (value >= 1_000) {
96 return `${(value / 1_000).toFixed(2)} B`;
97 } else {
98 return `${value.toFixed(2)} M`;
99 }
100 };
101
102 export function getInitials(name: string): string {
103 // Split the name by spaces to get individual words
104 const words = name.split(" ");
105 // Map over the words array, extracting the first letter of each word and converting it to uppercase
106 const initials = words.map((word) => word.charAt(0).toUpperCase());
107 // Join the initials into a single string
108 return initials.join("");
109 }
110
111 export async function convertImagesToBase64(
112 images: globalThis.File | globalThis.File[],
113 ) {
114 images = Array.isArray(images) ? images : [images];
115
116 const base64Images = await Promise.all(
117 images.map(async (image) => {
118 const arrayBuffer = await image.arrayBuffer();
119 const base64String = Buffer.from(arrayBuffer).toString("base64");
120 const mimeType = image.type; // Get MIME type from the file object
121 return `data:${mimeType};base64,${base64String}`;
122 }),
123 );
124 return base64Images;
125 }
126
127 export function formatAnswer(answer: string, isLoading: boolean): string {
128 let formattedAnswer = answer;
129
130 // Remove leading ```markdown if present
131 if (formattedAnswer.startsWith("```markdown")) {
132 formattedAnswer = formattedAnswer.split("```markdown")[1] ?? "";
133 }
134
135 // Remove trailing ``` only if not loading
136 if (!isLoading && formattedAnswer.endsWith("```")) {
137 formattedAnswer = formattedAnswer.slice(0, -3);
138 }
139
140 return formattedAnswer;
141 }
142
143 export function formatCamelCase(str: string): string {
144 // Add space before capital letters and capitalize the first letter
145 const formatted = str
146 // Add space before capital letters
147 .replace(/([A-Z])/g, " $1")
148 // Trim any leading space and capitalize first letter
149 .trim()
150 .replace(/^./, (str) => str.toUpperCase());
151
152 return formatted;
153 }
154
155 /**
156 * Checks if an array has any duplicate elements
157 * @param array The array to check for duplicates
158 * @returns true if array contains duplicates, false otherwise
159 */
160 export function hasDuplicates<T>(array: T[]): boolean {
161 // For empty arrays or single-element arrays, return false immediately
162 if (!array || array.length <= 1) {
163 return false;
164 }
165
166 // For primitive types (strings, numbers, booleans), use Set for efficiency
167 if (
168 array.every(
169 (item) =>
170 typeof item === "string" ||
171 typeof item === "number" ||
172 typeof item === "boolean" ||
173 item === null ||
174 item === undefined,
175 )
176 ) {
177 return new Set(array).size !== array.length;
178 }
179
180 // For arrays with objects or nested structures, use a more thorough comparison
181 for (let i = 0; i < array.length; i++) {
182 for (let j = i + 1; j < array.length; j++) {
183 if (isEqual(array[i], array[j])) {
184 return true;
185 }
186 }
187 }
188
189 return false;
190 }
191
192 /**
193 * Helper function to check if two values are equal
194 * Handles primitive types and objects (shallow comparison)
195 */
196 function isEqual(a: unknown, b: unknown): boolean {
197 // Handle primitive types
198 if (a === b) return true;
199
200 // If either value is not an object or is null, they're not equal
201 if (
202 a == null ||
203 b == null ||
204 typeof a !== "object" ||
205 typeof b !== "object"
206 ) {
207 return false;
208 }
209
210 // Check if both are arrays
211 if (Array.isArray(a) && Array.isArray(b)) {
212 if (a.length !== b.length) return false;
213
214 // Compare array elements
215 for (let i = 0; i < a.length; i++) {
216 if (!isEqual(a[i], b[i])) return false;
217 }
218
219 return true;
220 }
221
222 // For regular objects, compare properties
223 const keysA = Object.keys(a);
224 const keysB = Object.keys(b);
225
226 if (keysA.length !== keysB.length) return false;
227
228 return keysA.every((key) => Object.hasOwn(b, key) && isEqual(a[key], b[key]));
229 }
230
231 /**
232 * Generate a unique token for use in invitations and other secure links
233 */
234 export async function generateUniqueToken(length = 32): Promise<string> {
235 // Using nanoid for secure, URL-friendly unique tokens
236 const alphabet =
237 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
238 const nanoid = customAlphabet(alphabet, length);
239 return nanoid();
240 }
241
242 export function getDateRange(start: Date, end: Date) {
243 const startStr = start.toLocaleDateString("en-US", {
244 month: "short",
245 day: "numeric",
246 });
247 const endStr = end.toLocaleDateString("en-US", {
248 month: "short",
249 day: "numeric",
250 });
251 return `${startStr} - ${endStr}`;
252 }
253
254 export const fetchJSON = async (url: string, next?: NextFetchRequestConfig) => {
255 const response = await fetch(url, { next: next });
256 if (!response.ok) throw new Error(`Failed to fetch data from ${url}`);
257 return response.json();
258 };
259
260 export const extractOrigin = (url: string): string => {
261 const hostname = new URL(url).hostname;
262 return hostname.replace(/^www\./, "");
263 };
264
264 lines TYPESCRIPT