mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat: Implement robust error/not-found pages & UI components
- Introduce dedicated error pages for dashboard and video routes - Add specific not-found pages for dashboard, projects, videos, and settings - Implement global `not-found.tsx` for general unhandled routes - Integrate root and dashboard layouts with ErrorBoundary and Suspense - Add new UI components: Accordion, Hover Card, Menubar, Navigation Menu, Select, Tabs - Update Navbar to utilize the new Navigation Menu component - Enhance `button` component with a `link` variant for better styling - Refine existing UI components (dialog, dropdown, input, etc.) - Update Tailwind config with new colors and animation extensions
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
* Standardized API error response format
|
||||
* All API routes should use this format for consistency
|
||||
*/
|
||||
export interface ApiErrorResponse {
|
||||
error: string;
|
||||
code?: string;
|
||||
details?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardized API success response format
|
||||
*/
|
||||
export interface ApiSuccessResponse<T = unknown> {
|
||||
data: T;
|
||||
meta?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
total?: number;
|
||||
totalPages?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP status codes used in the API
|
||||
*/
|
||||
export const HttpStatus = {
|
||||
OK: 200,
|
||||
CREATED: 201,
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
CONFLICT: 409,
|
||||
UNPROCESSABLE_ENTITY: 422,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Error codes for client-side handling
|
||||
*/
|
||||
export const ErrorCode = {
|
||||
// Authentication errors
|
||||
UNAUTHORIZED: "UNAUTHORIZED",
|
||||
FORBIDDEN: "FORBIDDEN",
|
||||
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
|
||||
|
||||
// Resource errors
|
||||
NOT_FOUND: "NOT_FOUND",
|
||||
ALREADY_EXISTS: "ALREADY_EXISTS",
|
||||
|
||||
// Validation errors
|
||||
VALIDATION_ERROR: "VALIDATION_ERROR",
|
||||
INVALID_INPUT: "INVALID_INPUT",
|
||||
|
||||
// Rate limiting
|
||||
RATE_LIMITED: "RATE_LIMITED",
|
||||
|
||||
// Server errors
|
||||
INTERNAL_ERROR: "INTERNAL_ERROR",
|
||||
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Creates a standardized error response
|
||||
*
|
||||
* @param message - Human-readable error message
|
||||
* @param status - HTTP status code
|
||||
* @param code - Machine-readable error code for client handling
|
||||
* @param details - Additional error details for validation errors (field -> messages[])
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* return errorResponse("Project not found", 404, ErrorCode.NOT_FOUND);
|
||||
* return errorResponse("Invalid input", 400, ErrorCode.VALIDATION_ERROR, { email: ["Invalid email format"] });
|
||||
* ```
|
||||
*/
|
||||
export function errorResponse(
|
||||
message: string,
|
||||
status: number,
|
||||
code?: string,
|
||||
details?: Record<string, string[]>
|
||||
): NextResponse<ApiErrorResponse> {
|
||||
const body: ApiErrorResponse = { error: message };
|
||||
if (code) body.code = code;
|
||||
if (details) {
|
||||
// Sanitize: only allow string arrays to prevent accidental data leakage
|
||||
const sanitized: Record<string, string[]> = {};
|
||||
for (const [key, value] of Object.entries(details)) {
|
||||
if (Array.isArray(value) && value.every(v => typeof v === 'string')) {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
if (Object.keys(sanitized).length > 0) {
|
||||
body.details = sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(body, { status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized success response
|
||||
*
|
||||
* @param data - Response data
|
||||
* @param status - HTTP status code (default: 200)
|
||||
* @param meta - Pagination or other metadata (optional)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* return successResponse({ projects: [] });
|
||||
* return successResponse({ projects: [] }, 200, { page: 1, limit: 10, total: 100 });
|
||||
* ```
|
||||
*/
|
||||
export function successResponse<T>(
|
||||
data: T,
|
||||
status: number = HttpStatus.OK,
|
||||
meta?: ApiSuccessResponse["meta"]
|
||||
): NextResponse<ApiSuccessResponse<T>> {
|
||||
const body: ApiSuccessResponse<T> = { data };
|
||||
if (meta) body.meta = meta;
|
||||
|
||||
return NextResponse.json(body, { status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Common error response helpers
|
||||
*/
|
||||
export const apiErrors = {
|
||||
unauthorized: (message = "Unauthorized") =>
|
||||
errorResponse(message, HttpStatus.UNAUTHORIZED, ErrorCode.UNAUTHORIZED),
|
||||
|
||||
forbidden: (message = "Forbidden") =>
|
||||
errorResponse(message, HttpStatus.FORBIDDEN, ErrorCode.FORBIDDEN),
|
||||
|
||||
notFound: (resource = "Resource") =>
|
||||
errorResponse(`${resource} not found`, HttpStatus.NOT_FOUND, ErrorCode.NOT_FOUND),
|
||||
|
||||
badRequest: (message = "Bad request") =>
|
||||
errorResponse(message, HttpStatus.BAD_REQUEST, ErrorCode.INVALID_INPUT),
|
||||
|
||||
validationError: (message: string, details?: Record<string, string[]>) =>
|
||||
errorResponse(message, HttpStatus.UNPROCESSABLE_ENTITY, ErrorCode.VALIDATION_ERROR, details),
|
||||
|
||||
conflict: (message: string) =>
|
||||
errorResponse(message, HttpStatus.CONFLICT, ErrorCode.ALREADY_EXISTS),
|
||||
|
||||
rateLimited: (message = "Too many requests") =>
|
||||
errorResponse(message, HttpStatus.TOO_MANY_REQUESTS, ErrorCode.RATE_LIMITED),
|
||||
|
||||
internalError: (message = "Internal server error") =>
|
||||
errorResponse(message, HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_ERROR),
|
||||
};
|
||||
Reference in New Issue
Block a user