Utilities & Libraries
Comprehensive guide to utility functions, custom hooks, and shared libraries used throughout the YouTube Analyzer application.
This section documents all utility functions, custom React hooks, validation schemas, and shared libraries that power the YouTube Analyzer application.
Core Utilities
lib/utils.ts
Central utility functions used throughout the application.
Core Functions
// Class name utility for Tailwind CSS
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Date formatting
export function formatDate(input: string | number): string {
const date = new Date(input);
return date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}
// Absolute URL generation
export function absoluteUrl(path: string) {
return `${env.NEXT_PUBLIC_APP_URL}${path}`;
}Metadata Construction
export function constructMetadata({
title = siteConfig.name,
description = siteConfig.description,
image = siteConfig.ogImage,
icons = "/favicon.ico",
noIndex = false,
}: {
title?: string;
description?: string;
image?: string;
icons?: string;
noIndex?: boolean;
} = {}): Metadata {
return {
title,
description,
keywords: ["Next.js", "React", "Prisma", "YouTube", "Analysis"],
authors: [{ name: "sfoerster" }],
openGraph: {
type: "website",
locale: "en_US",
url: siteConfig.url,
title,
description,
siteName: title,
},
twitter: {
card: "summary_large_image",
title,
description,
images: [image],
},
// ... additional metadata
};
}Helper Functions
// Time ago formatting
export const timeAgo = (timestamp: Date, timeOnly?: boolean): string => {
if (!timestamp) return "never";
return `${ms(Date.now() - new Date(timestamp).getTime())}${
timeOnly ? "" : " ago"
}`;
};
// Fetch wrapper with error handling
export async function fetcher<JSON = any>(
input: RequestInfo,
init?: RequestInit,
): Promise<JSON> {
const res = await fetch(input, init);
if (!res.ok) {
const json = await res.json();
if (json.error) {
const error = new Error(json.error) as Error & { status: number };
error.status = res.status;
throw error;
} else {
throw new Error("An unexpected error occurred");
}
}
return res.json();
}
// Number formatting
export function nFormatter(num: number, digits?: number) {
const lookup = [
{ value: 1, symbol: "" },
{ value: 1e3, symbol: "k" },
{ value: 1e6, symbol: "M" },
{ value: 1e9, symbol: "G" },
{ value: 1e12, symbol: "T" },
{ value: 1e15, symbol: "P" },
{ value: 1e18, symbol: "E" }
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
var item = lookup.slice().reverse().find(function(item) {
return num >= item.value;
});
return item ? (num / item.value).toFixed(digits || 1).replace(rx, "$1") + item.symbol : "0";
}
// String utilities
export function capitalize(str: string) {
if (!str || typeof str !== "string") return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const truncate = (str: string, length: number) => {
if (!str || str.length <= length) return str;
return `${str.slice(0, length)}...`;
};Custom React Hooks
hooks/use-media-query.ts
Responsive design hook for device detection.
export function useMediaQuery() {
const [device, setDevice] = useState<"mobile" | "sm" | "tablet" | "desktop" | null>(null);
const [dimensions, setDimensions] = useState<{ width: number; height: number; } | null>(null);
useEffect(() => {
const checkDevice = () => {
if (window.matchMedia("(max-width: 640px)").matches) {
setDevice("mobile");
} else if (window.matchMedia("(max-width: 768px)").matches) {
setDevice("sm");
} else if (window.matchMedia("(min-width: 641px) and (max-width: 1024px)").matches) {
setDevice("tablet");
} else {
setDevice("desktop");
}
setDimensions({ width: window.innerWidth, height: window.innerHeight });
};
checkDevice();
window.addEventListener("resize", checkDevice);
return () => window.removeEventListener("resize", checkDevice);
}, []);
return {
device,
width: dimensions?.width,
height: dimensions?.height,
isMobile: device === "mobile",
isSm: device === "sm",
isTablet: device === "tablet",
isDesktop: device === "desktop",
};
}hooks/use-intersection-observer.ts
Viewport intersection detection for animations and lazy loading.
interface Args extends IntersectionObserverInit {
freezeOnceVisible?: boolean;
}
function useIntersectionObserver(
elementRef: RefObject<Element>,
{ threshold = 0, root = null, rootMargin = "0%", freezeOnceVisible = false }: Args,
): IntersectionObserverEntry | undefined {
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const frozen = entry?.isIntersecting && freezeOnceVisible;
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
setEntry(entry);
};
useEffect(() => {
const node = elementRef?.current;
const hasIOSupport = !!window.IntersectionObserver;
if (!hasIOSupport || frozen || !node) return;
const observerParams = { threshold, root, rootMargin };
const observer = new IntersectionObserver(updateEntry, observerParams);
observer.observe(node);
return () => observer.disconnect();
}, [elementRef?.current, JSON.stringify(threshold), root, rootMargin, frozen]);
return entry;
}hooks/use-local-storage.ts
Persistent local storage state management.
const useLocalStorage = <T>(
key: string,
initialValue: T,
): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState(initialValue);
useEffect(() => {
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
}
}, [key]);
const setValue = (value: T) => {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
};
return [storedValue, setValue];
};hooks/use-scroll.ts
Scroll position detection for navigation styling.
export function useScroll(threshold: number) {
const [scrolled, setScrolled] = useState(false);
const onScroll = useCallback(() => {
setScrolled(window.pageYOffset > threshold);
}, [threshold]);
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [onScroll]);
return scrolled;
}hooks/use-lock-body.ts
Body scroll locking for modals and overlays.
export function useLockBody() {
React.useLayoutEffect((): (() => void) => {
const originalStyle: string = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = "hidden";
return () => (document.body.style.overflow = originalStyle);
}, []);
}hooks/use-mounted.ts
Client-side hydration detection.
export function useMounted() {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
return mounted;
}Validation Schemas
lib/validations/auth.ts
Authentication form validation using Zod.
import * as z from "zod";
export const userAuthSchema = z.object({
email: z.string().email(),
});lib/validations/user.ts
User profile validation schemas.
import { UserRole } from "@prisma/client";
import * as z from "zod";
export const userNameSchema = z.object({
name: z.string().min(3).max(32),
});
export const userRoleSchema = z.object({
role: z.nativeEnum(UserRole),
});lib/validations/og.ts
Open Graph image generation validation.
import * as z from "zod";
export const ogImageSchema = z.object({
heading: z.string(),
type: z.string(),
mode: z.enum(["light", "dark"]).default("dark"),
});Shared Components & Libraries
Icons Library
The components/shared/icons.tsx file provides a centralized icon system using Lucide React icons and custom SVG icons.
import { LucideIcon, LucideProps } from "lucide-react";
export type Icon = LucideIcon;
export const Icons = {
// Lucide icons
add: Plus,
arrowRight: ArrowRight,
billing: CreditCard,
dashboard: LayoutPanelLeft,
// ... more icons
// Custom SVG icons
gitHub: ({ ...props }: LucideProps) => (
<svg aria-hidden="true" focusable="false" role="img" {...props}>
{/* SVG path */}
</svg>
),
google: ({ ...props }: LucideProps) => (
<svg aria-hidden="true" focusable="false" role="img" {...props}>
{/* SVG path */}
</svg>
),
// ... more custom icons
};Usage
import { Icons } from "@/components/shared/icons";
// In component
<Icons.dashboard className="h-4 w-4" />
<Icons.gitHub className="h-6 w-6" />UI Components
The application uses a comprehensive UI component library built on top of Radix UI primitives with Tailwind CSS styling. Key components include:
- Form Components: Input, Label, Button, Select, Textarea
- Layout Components: Card, Container, Skeleton, Separator
- Feedback Components: Toast, Alert, Badge, Progress
- Navigation Components: Dropdown Menu, Tabs, Accordion
- Data Display: Table, Chart, Avatar, Tooltip
- Overlay Components: Dialog, Sheet, Popover, Modal
Component Architecture
// Example: Button component with variants
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
// ... more variants
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
// ... more sizes
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);Service Libraries
YouTube API Service
// lib/youtube.ts
export class YouTubeService {
async getChannelInfo(channelId: string) {
// Channel data retrieval
}
async getChannelVideos(channelId: string, maxResults: number) {
// Video list retrieval
}
async getVideoDetails(videoIds: string[]) {
// Detailed video information
}
}OpenAI Integration
// lib/openai-streaming.ts
export async function generateAnalysis(
videos: VideoData[],
analysisType: string
) {
// Streaming AI analysis generation
}Email Service
// lib/email.ts
export const sendVerificationRequest = async ({ identifier, url, provider }) => {
const user = await getUserByEmail(identifier);
const userVerified = user?.emailVerified ? true : false;
await resend.emails.send({
from: provider.from,
to: identifier,
subject: userVerified ? "Sign-in link" : "Activate your account",
react: MagicLinkEmail({
firstName: user?.name,
actionUrl: url,
mailType: userVerified ? "login" : "register",
siteName: siteConfig.name,
}),
});
};Development Guidelines
Utility Function Standards
- Pure Functions: Utilities should be pure functions when possible
- Error Handling: Include proper error handling and validation
- TypeScript: Use strict typing for all parameters and return values
- Documentation: Include JSDoc comments for complex functions
- Testing: Add unit tests for critical utility functions
Custom Hook Best Practices
- Single Responsibility: Each hook should have a single, clear purpose
- Naming Convention: Use the
useprefix consistently - Dependencies: Minimize external dependencies
- Performance: Use
useCallbackanduseMemoappropriately - Cleanup: Always clean up event listeners and subscriptions
Validation Schema Guidelines
- Zod Integration: Use Zod for all form and API validation
- Reusable Schemas: Create composable schemas for common patterns
- Error Messages: Provide clear, user-friendly error messages
- Type Safety: Leverage TypeScript integration with
z.infer
Component Library Standards
- Accessibility: Ensure all components meet WCAG guidelines
- Responsive Design: Components should work across all screen sizes
- Theming: Support light/dark mode and customizable themes
- Performance: Optimize for rendering performance
- Storybook: Document components with Storybook where applicable
Common Patterns
Data Fetching with Error Handling
const { data, error, isLoading } = useSWR(
'/api/analyses',
fetcher,
{
onError: (error) => {
toast.error('Failed to load analyses');
}
}
);Form Handling with Validation
const form = useForm<FormData>({
resolver: zodResolver(userNameSchema),
defaultValues: { name: user?.name || "" },
});
const onSubmit = handleSubmit(async (data) => {
try {
await updateUserName(data);
toast.success('Name updated successfully');
} catch (error) {
toast.error('Failed to update name');
}
});Responsive Component Usage
const { isMobile, isTablet } = useMediaQuery();
return (
<div className={cn(
"grid gap-4",
isMobile ? "grid-cols-1" : isTablet ? "grid-cols-2" : "grid-cols-3"
)}>
{/* Content */}
</div>
);This utilities documentation provides a comprehensive reference for all the helper functions, hooks, and shared libraries that make the YouTube Analyzer application maintainable and efficient.