Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
153
web/maplefile-frontend/src/utils/colorUtils.js
Normal file
153
web/maplefile-frontend/src/utils/colorUtils.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* Color Utilities for Tag Visualization
|
||||
*
|
||||
* Provides utilities for blending multiple tag colors and creating
|
||||
* visual indicators for collections with multiple tags.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB
|
||||
* @param {string} hex - Hex color code (e.g., "#FF5733")
|
||||
* @returns {{r: number, g: number, b: number}} RGB object
|
||||
*/
|
||||
function hexToRgb(hex) {
|
||||
// Remove # if present
|
||||
hex = hex.replace(/^#/, '');
|
||||
|
||||
// Parse hex values
|
||||
const bigint = parseInt(hex, 16);
|
||||
const r = (bigint >> 16) & 255;
|
||||
const g = (bigint >> 8) & 255;
|
||||
const b = bigint & 255;
|
||||
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGB to hex color
|
||||
* @param {number} r - Red (0-255)
|
||||
* @param {number} g - Green (0-255)
|
||||
* @param {number} b - Blue (0-255)
|
||||
* @returns {string} Hex color code
|
||||
*/
|
||||
function rgbToHex(r, g, b) {
|
||||
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b))
|
||||
.toString(16)
|
||||
.slice(1)
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blend multiple colors using average RGB values
|
||||
* @param {string[]} colors - Array of hex color codes
|
||||
* @returns {string} Blended hex color
|
||||
*/
|
||||
export function blendColors(colors) {
|
||||
if (!colors || colors.length === 0) {
|
||||
return '#9CA3AF'; // Default gray color
|
||||
}
|
||||
|
||||
if (colors.length === 1) {
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
// Convert all colors to RGB
|
||||
const rgbColors = colors.map(hexToRgb);
|
||||
|
||||
// Calculate average RGB values
|
||||
const avgR = rgbColors.reduce((sum, c) => sum + c.r, 0) / rgbColors.length;
|
||||
const avgG = rgbColors.reduce((sum, c) => sum + c.g, 0) / rgbColors.length;
|
||||
const avgB = rgbColors.reduce((sum, c) => sum + c.b, 0) / rgbColors.length;
|
||||
|
||||
// Convert back to hex
|
||||
return rgbToHex(avgR, avgG, avgB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a gradient string from multiple colors
|
||||
* @param {string[]} colors - Array of hex color codes
|
||||
* @returns {string} CSS linear gradient string
|
||||
*/
|
||||
export function createGradientFromColors(colors) {
|
||||
if (!colors || colors.length === 0) {
|
||||
return 'linear-gradient(135deg, #9CA3AF, #6B7280)';
|
||||
}
|
||||
|
||||
if (colors.length === 1) {
|
||||
const color = colors[0];
|
||||
// Create a subtle gradient with the same color (lighter to darker)
|
||||
return `linear-gradient(135deg, ${color}E6, ${color})`;
|
||||
}
|
||||
|
||||
if (colors.length === 2) {
|
||||
return `linear-gradient(135deg, ${colors[0]}, ${colors[1]})`;
|
||||
}
|
||||
|
||||
// For 3+ colors, create evenly spaced gradient stops
|
||||
const stops = colors.map((color, index) => {
|
||||
const percentage = (index / (colors.length - 1)) * 100;
|
||||
return `${color} ${percentage}%`;
|
||||
}).join(', ');
|
||||
|
||||
return `linear-gradient(135deg, ${stops})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lighten a color by a percentage
|
||||
* @param {string} color - Hex color code
|
||||
* @param {number} percent - Percentage to lighten (0-100)
|
||||
* @returns {string} Lightened hex color
|
||||
*/
|
||||
export function lightenColor(color, percent) {
|
||||
const rgb = hexToRgb(color);
|
||||
const factor = 1 + (percent / 100);
|
||||
|
||||
const r = Math.min(255, rgb.r * factor);
|
||||
const g = Math.min(255, rgb.g * factor);
|
||||
const b = Math.min(255, rgb.b * factor);
|
||||
|
||||
return rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Darken a color by a percentage
|
||||
* @param {string} color - Hex color code
|
||||
* @param {number} percent - Percentage to darken (0-100)
|
||||
* @returns {string} Darkened hex color
|
||||
*/
|
||||
export function darkenColor(color, percent) {
|
||||
const rgb = hexToRgb(color);
|
||||
const factor = 1 - (percent / 100);
|
||||
|
||||
const r = Math.max(0, rgb.r * factor);
|
||||
const g = Math.max(0, rgb.g * factor);
|
||||
const b = Math.max(0, rgb.b * factor);
|
||||
|
||||
return rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add opacity to a hex color (returns rgba)
|
||||
* @param {string} color - Hex color code
|
||||
* @param {number} opacity - Opacity (0-1)
|
||||
* @returns {string} RGBA color string
|
||||
*/
|
||||
export function addOpacity(color, opacity) {
|
||||
const rgb = hexToRgb(color);
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contrasting text color (black or white) for a background color
|
||||
* @param {string} bgColor - Background hex color
|
||||
* @returns {string} '#000000' or '#FFFFFF'
|
||||
*/
|
||||
export function getContrastColor(bgColor) {
|
||||
const rgb = hexToRgb(bgColor);
|
||||
|
||||
// Calculate relative luminance
|
||||
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
||||
|
||||
// Return black for light backgrounds, white for dark backgrounds
|
||||
return luminance > 0.5 ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
168
web/maplefile-frontend/src/utils/rfc9457Parser.js
Normal file
168
web/maplefile-frontend/src/utils/rfc9457Parser.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* RFC 9457 Problem Details Parser
|
||||
*
|
||||
* This utility parses RFC 9457 "Problem Details for HTTP APIs" responses
|
||||
* from the backend and converts them into a format suitable for display
|
||||
* in React components.
|
||||
*
|
||||
* RFC 9457 Specification: https://www.rfc-editor.org/rfc/rfc9457.html
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse RFC 9457 Problem Detail from API response or pre-parsed JSON data
|
||||
*
|
||||
* @param {Response|Object} responseOrData - Fetch API response object or pre-parsed JSON data
|
||||
* @returns {ParsedError|null} Parsed error object, or null if not RFC 9457 format
|
||||
*
|
||||
* @typedef {Object} ParsedError
|
||||
* @property {Object<string, string>} fieldErrors - Field-specific error messages (key = field name, value = error message)
|
||||
* @property {string} generalError - General error message to display
|
||||
* @property {Object} problem - Full RFC 9457 problem detail for debugging
|
||||
* @property {string} type - Problem type URI
|
||||
* @property {number} status - HTTP status code
|
||||
* @property {string} instance - Request path that caused the error
|
||||
* @property {string} timestamp - ISO 8601 timestamp
|
||||
* @property {string} traceId - Request trace ID for debugging
|
||||
*/
|
||||
export function parseRFC9457Error(responseOrData) {
|
||||
try {
|
||||
let problem;
|
||||
|
||||
// Check if this is already parsed JSON data (object) or a Response object
|
||||
if (responseOrData && typeof responseOrData === 'object' && !responseOrData.json) {
|
||||
// Already parsed JSON data
|
||||
problem = responseOrData;
|
||||
} else {
|
||||
// This shouldn't happen in normal usage anymore, but handle it for safety
|
||||
console.warn("[RFC9457Parser] Received Response object instead of parsed data");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this looks like an RFC 9457 problem detail
|
||||
// RFC 9457 requires at least 'type' and 'status' fields
|
||||
if (!problem.type && !problem.status && !problem.title && !problem.detail && !problem.errors) {
|
||||
// Not an RFC 9457 format, return null to let caller handle legacy format
|
||||
return null;
|
||||
}
|
||||
|
||||
// All errors from our API are RFC 9457 format
|
||||
return {
|
||||
fieldErrors: problem.errors || {},
|
||||
generalError: problem.detail || problem.title || "An error occurred",
|
||||
problem: problem, // Full problem detail for debugging/logging
|
||||
type: problem.type,
|
||||
status: problem.status,
|
||||
instance: problem.instance,
|
||||
timestamp: problem.timestamp,
|
||||
traceId: problem.trace_id,
|
||||
};
|
||||
} catch (err) {
|
||||
// If parsing fails, return null to let caller handle it
|
||||
console.error("[RFC9457Parser] Failed to parse error response:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if HTTP response is an error
|
||||
*
|
||||
* @param {Response} response - Fetch API response object
|
||||
* @returns {boolean} True if response indicates an error (status >= 400)
|
||||
*/
|
||||
export function isErrorResponse(response) {
|
||||
return !response.ok && response.status >= 400;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format field errors for display in forms
|
||||
*
|
||||
* Converts the fieldErrors object into an array suitable for mapping
|
||||
* in React components.
|
||||
*
|
||||
* @param {Object<string, string>} fieldErrors - Field errors from parsed error
|
||||
* @returns {Array<{field: string, message: string}>} Array of field error objects
|
||||
*
|
||||
* @example
|
||||
* const fieldErrorArray = formatFieldErrors({
|
||||
* email: "Email is required",
|
||||
* password: "Password must be at least 8 characters"
|
||||
* });
|
||||
* // Returns: [
|
||||
* // { field: "email", message: "Email is required" },
|
||||
* // { field: "password", message: "Password must be at least 8 characters" }
|
||||
* // ]
|
||||
*/
|
||||
export function formatFieldErrors(fieldErrors) {
|
||||
return Object.entries(fieldErrors || {}).map(([field, message]) => ({
|
||||
field,
|
||||
message,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error title based on problem type
|
||||
*
|
||||
* @param {string} type - Problem type URI
|
||||
* @returns {string} User-friendly title
|
||||
*/
|
||||
export function getProblemTypeTitle(type) {
|
||||
if (!type) return "Error";
|
||||
|
||||
// Extract the last part of the URI
|
||||
const parts = type.split("/");
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
||||
// Convert kebab-case to Title Case
|
||||
return lastPart
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error details for debugging
|
||||
*
|
||||
* Logs the full RFC 9457 problem detail to console for debugging purposes.
|
||||
* Should only be called in development mode.
|
||||
*
|
||||
* @param {ParsedError} parsedError - Parsed error from parseRFC9457Error
|
||||
*/
|
||||
export function logErrorDetails(parsedError) {
|
||||
if (import.meta.env.DEV && parsedError.problem) {
|
||||
console.group(
|
||||
`[RFC9457] ${parsedError.problem.title} (${parsedError.status})`,
|
||||
);
|
||||
console.log("Type:", parsedError.type);
|
||||
console.log("Detail:", parsedError.generalError);
|
||||
console.log("Instance:", parsedError.instance);
|
||||
console.log("Trace ID:", parsedError.traceId);
|
||||
console.log("Timestamp:", parsedError.timestamp);
|
||||
|
||||
if (parsedError.fieldErrors && Object.keys(parsedError.fieldErrors).length > 0) {
|
||||
console.log("Field Errors:", parsedError.fieldErrors);
|
||||
}
|
||||
|
||||
console.log("Full Problem Detail:", parsedError.problem);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a structured Error object from parsed RFC 9457 response
|
||||
*
|
||||
* This is useful for throwing errors that can be caught and displayed
|
||||
* in error boundaries or catch blocks.
|
||||
*
|
||||
* @param {ParsedError} parsedError - Parsed error from parseRFC9457Error
|
||||
* @returns {Error} Error object with additional properties
|
||||
*/
|
||||
export function createErrorFromProblem(parsedError) {
|
||||
const error = new Error(parsedError.generalError);
|
||||
error.fieldErrors = parsedError.fieldErrors;
|
||||
error.problem = parsedError.problem;
|
||||
error.status = parsedError.status;
|
||||
error.type = parsedError.type;
|
||||
error.instance = parsedError.instance;
|
||||
error.traceId = parsedError.traceId;
|
||||
return error;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue