10-04-2026
This commit is contained in:
parent
70a7776da5
commit
761b1156c1
50 changed files with 11997 additions and 150 deletions
550
frontend/_src/utils/ConnectedDotsVisualization.ts
Normal file
550
frontend/_src/utils/ConnectedDotsVisualization.ts
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
// Define interfaces
|
||||
export interface DotConfig {
|
||||
id: number;
|
||||
value: number;
|
||||
x: number;
|
||||
link?: string; // URL to navigate to when dot is clicked
|
||||
onClick?: () => void; // Function to call when dot is clicked
|
||||
imageUrl?: string; // Image to display in tooltip
|
||||
title?: string; // Optional title for the tooltip
|
||||
description?: string; // Optional description for the tooltip
|
||||
}
|
||||
export interface Config {
|
||||
totalWidth: number;
|
||||
height: number;
|
||||
dotRadius: number;
|
||||
xUnitSize: number;
|
||||
tension: number;
|
||||
showGrid: boolean;
|
||||
tooltipWidth: number;
|
||||
tooltipHeight: number;
|
||||
}
|
||||
interface ControlPoints {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
}
|
||||
interface TooltipEdges {
|
||||
leftmost: number;
|
||||
rightmost: number;
|
||||
}
|
||||
export class ConnectedDotsVisualization {
|
||||
private config: Config;
|
||||
private dots: DotConfig[];
|
||||
private preloadedImages: Map<string, HTMLImageElement> = new Map();
|
||||
// DOM Elements
|
||||
private scrollContainer: HTMLElement;
|
||||
private svg: SVGElement;
|
||||
private gridGroup: SVGGElement;
|
||||
private curvePath: SVGPathElement;
|
||||
private dotsGroup: SVGGElement;
|
||||
private tooltipGroup: SVGGElement;
|
||||
// Active tooltip
|
||||
private activeTooltip: SVGElement | null = null;
|
||||
constructor(
|
||||
containerId: string,
|
||||
dots: DotConfig[],
|
||||
config?: Partial<Config>
|
||||
) {
|
||||
// Use the provided dots or empty array
|
||||
this.dots = dots || [];
|
||||
// Calculate the total width based on dots data
|
||||
const xUnitSize = config?.xUnitSize || 100;
|
||||
let calculatedWidth = 0;
|
||||
if (this.dots.length > 0) {
|
||||
// Find the minimum and maximum x values
|
||||
const minX = Math.min(...this.dots.map((dot) => dot.x));
|
||||
const maxX = Math.max(...this.dots.map((dot) => dot.x));
|
||||
// Calculate width based on the range of x values
|
||||
// Add padding on both sides (3 units on each side)
|
||||
calculatedWidth = (maxX - minX + 6) * xUnitSize;
|
||||
} else {
|
||||
calculatedWidth = 6 * xUnitSize; // Default width if no dots
|
||||
}
|
||||
// Default configuration
|
||||
this.config = {
|
||||
totalWidth: calculatedWidth,
|
||||
height: window.innerHeight,
|
||||
dotRadius: 6,
|
||||
xUnitSize: xUnitSize,
|
||||
tension: 0.5,
|
||||
showGrid: false,
|
||||
tooltipWidth: 128,
|
||||
tooltipHeight: 128,
|
||||
...config,
|
||||
};
|
||||
// Initialize DOM elements
|
||||
this.scrollContainer = document.getElementById(containerId) as HTMLElement;
|
||||
|
||||
// Calculate the container height dynamically
|
||||
const containerHeight =
|
||||
this.scrollContainer.clientHeight ||
|
||||
this.scrollContainer.offsetHeight ||
|
||||
window.innerHeight;
|
||||
|
||||
// Default configuration
|
||||
this.config = {
|
||||
totalWidth: calculatedWidth,
|
||||
height: containerHeight, // Use the calculated container height
|
||||
dotRadius: 6,
|
||||
xUnitSize: xUnitSize,
|
||||
tension: 0.5,
|
||||
showGrid: false,
|
||||
tooltipWidth: 128,
|
||||
tooltipHeight: 128,
|
||||
...config,
|
||||
};
|
||||
|
||||
// Create SVG elements
|
||||
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
this.gridGroup = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"g"
|
||||
);
|
||||
this.curvePath = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"path"
|
||||
);
|
||||
this.dotsGroup = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"g"
|
||||
);
|
||||
this.tooltipGroup = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"g"
|
||||
);
|
||||
// Initialize the visualization
|
||||
this.addStyles();
|
||||
this.initializeSVG();
|
||||
this.setupEventListeners();
|
||||
this.preloadImages();
|
||||
this.render();
|
||||
}
|
||||
private preloadImages(): void {
|
||||
// Extract all unique image URLs from dots
|
||||
const imageUrls: string[] = this.dots
|
||||
.filter((dot) => dot.imageUrl)
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
.map((dot) => dot.imageUrl!)
|
||||
.filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
|
||||
// Create a loading indicator (optional)
|
||||
const loadingCount = { current: 0, total: imageUrls.length };
|
||||
if (imageUrls.length > 0) {
|
||||
console.log(`Preloading ${imageUrls.length} images...`);
|
||||
}
|
||||
// Preload each image
|
||||
for (const url of imageUrls) {
|
||||
const img = new Image();
|
||||
// Optional loading events
|
||||
img.onload = () => {
|
||||
loadingCount.current++;
|
||||
if (loadingCount.current === loadingCount.total) {
|
||||
console.log("All images preloaded successfully");
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
loadingCount.current++;
|
||||
console.error(`Failed to preload image: ${url}`);
|
||||
};
|
||||
// Set src to start loading
|
||||
img.src = url;
|
||||
// Store in map for potential later use
|
||||
this.preloadedImages.set(url, img);
|
||||
}
|
||||
}
|
||||
private addStyles(): void {
|
||||
// Add necessary styles for tooltips and interactions
|
||||
const styleId = "connected-dots-styles";
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = styleId;
|
||||
// style.textContent = `
|
||||
|
||||
// `;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
private initializeSVG(): void {
|
||||
// Configure SVG
|
||||
this.svg.setAttribute("width", `${this.config.totalWidth}`);
|
||||
this.svg.setAttribute("height", `${this.config.height}`);
|
||||
this.svg.style.overflow = "visible";
|
||||
this.scrollContainer.appendChild(this.svg);
|
||||
// Configure grid group
|
||||
this.gridGroup.classList.add("grid");
|
||||
this.svg.appendChild(this.gridGroup);
|
||||
// Configure curve path
|
||||
this.curvePath.setAttribute("fill", "none");
|
||||
this.curvePath.setAttribute("stroke", "white");
|
||||
this.curvePath.setAttribute("stroke-width", "2");
|
||||
this.curvePath.setAttribute("stroke-linecap", "round");
|
||||
this.curvePath.classList.add("curve-path");
|
||||
this.svg.appendChild(this.curvePath);
|
||||
// Configure dots group
|
||||
this.svg.appendChild(this.dotsGroup);
|
||||
// Configure tooltip group (always on top)
|
||||
this.tooltipGroup.classList.add("tooltips");
|
||||
this.svg.appendChild(this.tooltipGroup);
|
||||
}
|
||||
private setupEventListeners(): void {
|
||||
// Event listeners removed as the controls were removed
|
||||
}
|
||||
private getDotX(x: number): number {
|
||||
return (x + 3) * this.config.xUnitSize;
|
||||
}
|
||||
private getDotY(value: number): number {
|
||||
const centerY = this.config.height / 1.95;
|
||||
// Calculate raw Y position
|
||||
// height of the amplitude
|
||||
const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.6);
|
||||
// Calculate minimum Y position to ensure tooltip fits
|
||||
const minY = this.config.tooltipHeight + 40; // tooltip height + some padding
|
||||
// Ensure Y is never less than minimum (never too high on screen)
|
||||
return Math.max(rawY, minY);
|
||||
}
|
||||
private calculateBezierControlPoints(
|
||||
dots: DotConfig[],
|
||||
index: number
|
||||
): ControlPoints {
|
||||
const tension = this.config.tension * 500; // Scale tension for Bezier curve Rundung Kurve
|
||||
// Get current point and its neighbors
|
||||
const curr = dots[index];
|
||||
const next = dots[index + 1];
|
||||
// Calculate control points for a smooth bezier curve
|
||||
const x1 = this.getDotX(curr.x) + tension;
|
||||
const y1 = this.getDotY(curr.value);
|
||||
const x2 = this.getDotX(next.x) - tension;
|
||||
const y2 = this.getDotY(next.value);
|
||||
return { x1, y1, x2, y2 };
|
||||
}
|
||||
private generateBezierPath(): string {
|
||||
if (this.dots.length < 2) return "";
|
||||
let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
|
||||
this.dots[0].value
|
||||
)}`;
|
||||
for (let i = 0; i < this.dots.length - 1; i++) {
|
||||
const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
|
||||
this.dots,
|
||||
i
|
||||
);
|
||||
const nextX = this.getDotX(this.dots[i + 1].x);
|
||||
const nextY = this.getDotY(this.dots[i + 1].value);
|
||||
path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
private drawGrid(): void {
|
||||
// Clear previous grid
|
||||
while (this.gridGroup.firstChild) {
|
||||
this.gridGroup.removeChild(this.gridGroup.firstChild);
|
||||
}
|
||||
if (!this.config.showGrid) return;
|
||||
// Horizontal grid lines
|
||||
for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
|
||||
const line = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"line"
|
||||
);
|
||||
line.setAttribute("x1", "0");
|
||||
line.setAttribute("y1", this.getDotY(value).toString());
|
||||
line.setAttribute("x2", this.config.totalWidth.toString());
|
||||
line.setAttribute("y2", this.getDotY(value).toString());
|
||||
line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
|
||||
line.setAttribute("stroke-width", "1");
|
||||
this.gridGroup.appendChild(line);
|
||||
const text = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text"
|
||||
);
|
||||
text.setAttribute("x", "10");
|
||||
text.setAttribute("y", (this.getDotY(value) + 4).toString());
|
||||
text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
|
||||
text.setAttribute("font-size", "12");
|
||||
text.textContent = value.toString();
|
||||
this.gridGroup.appendChild(text);
|
||||
}
|
||||
// Vertical grid lines
|
||||
const numVertLines = Math.ceil(
|
||||
this.config.totalWidth / this.config.xUnitSize
|
||||
);
|
||||
for (let i = 0; i < numVertLines; i++) {
|
||||
const x = i * this.config.xUnitSize;
|
||||
const xValue = i - 3; // Starting from -3
|
||||
const line = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"line"
|
||||
);
|
||||
line.setAttribute("x1", x.toString());
|
||||
line.setAttribute("y1", "0");
|
||||
line.setAttribute("x2", x.toString());
|
||||
line.setAttribute("y2", this.config.height.toString());
|
||||
line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
|
||||
line.setAttribute("stroke-width", "1");
|
||||
this.gridGroup.appendChild(line);
|
||||
if (xValue !== 0) {
|
||||
const text = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text"
|
||||
);
|
||||
text.setAttribute("x", x.toString());
|
||||
text.setAttribute("y", (this.config.height / 2 + 20).toString());
|
||||
text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
|
||||
text.setAttribute("font-size", "12");
|
||||
text.setAttribute("text-anchor", "middle");
|
||||
text.textContent = xValue.toString();
|
||||
this.gridGroup.appendChild(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
|
||||
const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
tooltip.classList.add("dot-tooltip");
|
||||
tooltip.setAttribute("data-dot-id", dot.id.toString());
|
||||
|
||||
// Calculate tooltip dimensions and position
|
||||
const tooltipWidth = 128; // Base width for your tooltip
|
||||
const tooltipHeight = (4 / 3) * tooltipWidth;
|
||||
const tooltipX = x - tooltipWidth / 2;
|
||||
let tooltipY = y - tooltipHeight - 10; // Positioned above the dot
|
||||
tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
|
||||
|
||||
// Create background rectangle
|
||||
const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
bg.setAttribute("x", tooltipX.toString());
|
||||
bg.setAttribute("y", tooltipY.toString());
|
||||
bg.setAttribute("width", tooltipWidth.toString());
|
||||
bg.setAttribute("height", tooltipHeight.toString());
|
||||
bg.setAttribute("rx", "0"); // Rounded corners
|
||||
bg.classList.add("tooltip-background");
|
||||
tooltip.appendChild(bg);
|
||||
|
||||
// Create foreignObject for the content
|
||||
const contentContainer = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"foreignObject"
|
||||
);
|
||||
contentContainer.setAttribute("x", tooltipX.toString());
|
||||
contentContainer.setAttribute("y", tooltipY.toString());
|
||||
contentContainer.setAttribute("width", tooltipWidth.toString());
|
||||
contentContainer.setAttribute("height", tooltipHeight.toString());
|
||||
|
||||
// Create a div to contain the content
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("tooltip-content");
|
||||
|
||||
// Add title if available
|
||||
if (dot.title) {
|
||||
const title = document.createElement("div");
|
||||
title.textContent = dot.title;
|
||||
title.classList.add("tooltip-title");
|
||||
div.appendChild(title);
|
||||
}
|
||||
|
||||
// Add description if available
|
||||
if (dot.description) {
|
||||
const desc = document.createElement("div");
|
||||
desc.textContent = dot.description;
|
||||
desc.classList.add("tooltip-description");
|
||||
div.appendChild(desc);
|
||||
}
|
||||
|
||||
// Add image if available
|
||||
// Create a container div
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.classList.add("image_container"); // Add image_container class
|
||||
|
||||
// Define a variable for handling case with or without link
|
||||
let imgWrapper: HTMLElement;
|
||||
|
||||
// if (dot.imageUrl) {
|
||||
if (dot.link || dot.onClick) {
|
||||
const link = document.createElement("a");
|
||||
if (dot.link) {
|
||||
link.href = dot.link;
|
||||
} else {
|
||||
link.href = "#"; // Prevent default href for onClick
|
||||
}
|
||||
link.target = "_self"; // Opens in the same window
|
||||
|
||||
const imgElement = document.createElement("img");
|
||||
imgElement.src = dot.imageUrl;
|
||||
imgElement.classList.add("tooltip-image");
|
||||
|
||||
// Append the image element to the link
|
||||
link.appendChild(imgElement);
|
||||
imgWrapper = link; // Use the link as the wrapper
|
||||
|
||||
// Add the event listener to the link
|
||||
link.addEventListener("click", (e) => {
|
||||
if (dot.onClick) {
|
||||
e.preventDefault(); // Prevent default navigation
|
||||
dot.onClick();
|
||||
} else if (dot.link) {
|
||||
window.location.href = dot.link;
|
||||
} else {
|
||||
console.error("Dot has no link or onClick handler");
|
||||
throw new Error("Dot has no link or onClick handler");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const img = document.createElement("img");
|
||||
img.src = dot.imageUrl;
|
||||
img.classList.add("tooltip-image");
|
||||
imgWrapper = img; // Use the image directly as the wrapper
|
||||
}
|
||||
// } else {
|
||||
// console.error("Dot has no image URL");
|
||||
// throw new Error("Dot has no image URL");
|
||||
// }
|
||||
|
||||
// Append imageWrapper to the container
|
||||
imageContainer.appendChild(imgWrapper);
|
||||
|
||||
|
||||
// Append the image container to the main div
|
||||
div.appendChild(imageContainer);
|
||||
|
||||
const arrow = document.createElement("div");
|
||||
|
||||
arrow.classList.add("tooltip-arrow");
|
||||
|
||||
div.appendChild(arrow); // Append the arrow to the tooltip-content div
|
||||
|
||||
contentContainer.appendChild(div);
|
||||
tooltip.appendChild(contentContainer);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private showTooltip(dot: DotConfig, x: number, y: number): void {
|
||||
// Create tooltip
|
||||
const tooltip = this.createTooltip(dot, x, y);
|
||||
this.tooltipGroup.appendChild(tooltip);
|
||||
this.activeTooltip = tooltip;
|
||||
}
|
||||
private hideTooltip(): void {
|
||||
// This method is kept for compatibility but doesn't hide tooltips anymore
|
||||
}
|
||||
private drawCurve(): void {
|
||||
const pathData = this.generateBezierPath();
|
||||
this.curvePath.setAttribute("d", pathData);
|
||||
}
|
||||
private calculateTooltipEdges(): TooltipEdges {
|
||||
let leftmost = 0;
|
||||
let rightmost = 0;
|
||||
let firstTooltipFound = false;
|
||||
// If no dots with tooltips, return default values
|
||||
if (this.dots.length === 0) {
|
||||
return { leftmost: 0, rightmost: this.config.totalWidth };
|
||||
}
|
||||
// Calculate the leftmost and rightmost edges of all tooltips
|
||||
for (const dot of this.dots) {
|
||||
// Skip dots without tooltip content
|
||||
if (!dot.imageUrl && !dot.title && !dot.description) {
|
||||
continue;
|
||||
}
|
||||
const x = this.getDotX(dot.x);
|
||||
const tooltipWidth = this.config.tooltipWidth;
|
||||
const tooltipX = x - tooltipWidth / 2;
|
||||
if (!firstTooltipFound) {
|
||||
leftmost = tooltipX;
|
||||
rightmost = tooltipX + tooltipWidth;
|
||||
firstTooltipFound = true;
|
||||
} else {
|
||||
// Update leftmost and rightmost values
|
||||
leftmost = Math.min(leftmost, tooltipX);
|
||||
rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
|
||||
}
|
||||
}
|
||||
// If no dots with tooltips were found, use default values
|
||||
if (!firstTooltipFound) {
|
||||
return { leftmost: 0, rightmost: this.config.totalWidth };
|
||||
}
|
||||
return { leftmost, rightmost };
|
||||
}
|
||||
private drawDots(): void {
|
||||
// Clear previous dots
|
||||
while (this.dotsGroup.firstChild) {
|
||||
this.dotsGroup.removeChild(this.dotsGroup.firstChild);
|
||||
}
|
||||
// Clear previous tooltips
|
||||
while (this.tooltipGroup.firstChild) {
|
||||
this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
|
||||
}
|
||||
for (const dot of this.dots) {
|
||||
const x = this.getDotX(dot.x);
|
||||
const y = this.getDotY(dot.value);
|
||||
const circle = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"circle"
|
||||
);
|
||||
circle.setAttribute("cx", x.toString());
|
||||
circle.setAttribute("cy", y.toString());
|
||||
circle.setAttribute("r", this.config.dotRadius.toString());
|
||||
circle.setAttribute("fill", "white");
|
||||
circle.setAttribute("data-dot-id", dot.id.toString());
|
||||
circle.classList.add("dot");
|
||||
// Always show tooltip if it has content
|
||||
if (dot.imageUrl || dot.title || dot.description) {
|
||||
this.showTooltip(dot, x, y);
|
||||
}
|
||||
// Click event for navigation or custom function
|
||||
if (dot.link || dot.onClick) {
|
||||
circle.addEventListener("click", () => {
|
||||
if (dot.onClick) {
|
||||
dot.onClick();
|
||||
} else if (dot.link) {
|
||||
window.location.href = dot.link;
|
||||
} else {
|
||||
console.error("Dot has no link or onClick handler");
|
||||
throw new Error("Dot has no link or onClick handler");
|
||||
}
|
||||
});
|
||||
}
|
||||
this.dotsGroup.appendChild(circle);
|
||||
}
|
||||
}
|
||||
public render(): void {
|
||||
this.drawGrid();
|
||||
this.drawCurve();
|
||||
this.drawDots();
|
||||
// Calculate tooltip edges and set SVG width
|
||||
const { leftmost, rightmost } = this.calculateTooltipEdges();
|
||||
// Set the SVG width based on the rightmost tooltip edge
|
||||
if (rightmost > 0) {
|
||||
// Add some padding
|
||||
const padding = 40;
|
||||
this.config.totalWidth = rightmost + padding;
|
||||
this.svg.setAttribute("width", `${this.config.totalWidth}`);
|
||||
// Update grid width
|
||||
this.drawGrid();
|
||||
}
|
||||
}
|
||||
// Public API methods for external use
|
||||
public updateDots(newDots: DotConfig[]): void {
|
||||
this.dots = newDots;
|
||||
// Initial width calculation based on dot positions (for grid)
|
||||
if (this.dots.length > 0) {
|
||||
// Find the minimum and maximum x values
|
||||
const minX = Math.min(...this.dots.map((dot) => dot.x));
|
||||
const maxX = Math.max(...this.dots.map((dot) => dot.x));
|
||||
// Calculate width based on the range of x values
|
||||
// Add padding on both sides (3 units on each side)
|
||||
this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
|
||||
}
|
||||
// Render will calculate the tooltip edges and update the SVG width
|
||||
this.render();
|
||||
}
|
||||
public updateConfig(newConfig: Partial<Config>): void {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
this.render();
|
||||
}
|
||||
public resize(): void {
|
||||
const containerHeight = this.scrollContainer.clientHeight || this.scrollContainer.offsetHeight || window.innerHeight;
|
||||
this.config.height = containerHeight;
|
||||
this.svg.setAttribute("height", `${this.config.height}`);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
238
frontend/_src/utils/editFormOptions.js
Normal file
238
frontend/_src/utils/editFormOptions.js
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
// Form options and data for EditPage component
|
||||
|
||||
// Main categories with their subcategories
|
||||
export const categoryStructure = [
|
||||
{
|
||||
label: 'Career',
|
||||
value: 'career',
|
||||
subcategories: [
|
||||
{ label: 'Promotion', value: 'career-promotion' },
|
||||
{ label: 'Retirement', value: 'career-retirement' },
|
||||
{ label: 'Career Changes', value: 'career-changes' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Education',
|
||||
value: 'education',
|
||||
subcategories: [
|
||||
{ label: 'Graduation', value: 'education-graduation' },
|
||||
{ label: 'Schooling', value: 'education-schooling' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Awards',
|
||||
value: 'awards',
|
||||
subcategories: []
|
||||
},
|
||||
{
|
||||
label: 'Personal Celebrations',
|
||||
value: 'personal-celebrations',
|
||||
subcategories: [
|
||||
{ label: 'Birthday', value: 'birthday' },
|
||||
{ label: 'Anniversary', value: 'anniversary' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Relationships',
|
||||
value: 'relationships',
|
||||
subcategories: [
|
||||
{ label: 'Engagement', value: 'relationships-engagement' },
|
||||
{ label: 'Marriage', value: 'relationships-marriage' },
|
||||
{ label: 'Divorce', value: 'relationships-divorce' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Parenthood',
|
||||
value: 'parenthood',
|
||||
subcategories: [
|
||||
{ label: 'Pregnancy', value: 'parenthood-pregnancy' },
|
||||
{ label: 'Birth', value: 'parenthood-birth' },
|
||||
{ label: 'Adoption', value: 'parenthood-adoption' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Loss & Passing',
|
||||
value: 'passing',
|
||||
subcategories: [
|
||||
{ label: 'Funeral', value: 'passing-funeral' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Festivities',
|
||||
value: 'festivities',
|
||||
subcategories: [
|
||||
{ label: 'Christmas', value: 'festivities-christmas' },
|
||||
{ label: 'Thanksgiving', value: 'festivities-thanksgiving' },
|
||||
{ label: 'New Year', value: 'festivities-new-year' },
|
||||
{ label: 'Easter', value: 'festivities-easter' },
|
||||
{ label: 'Holidays', value: 'festivities-holidays' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Social Events',
|
||||
value: 'social-events',
|
||||
subcategories: [
|
||||
{ label: 'Reunions', value: 'reunions' },
|
||||
{ label: 'Concerts', value: 'concerts' },
|
||||
{ label: 'Sports', value: 'sports' },
|
||||
{ label: 'Festivals', value: 'festivals' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Community',
|
||||
value: 'community',
|
||||
subcategories: [
|
||||
{ label: 'Charity', value: 'charity' },
|
||||
{ label: 'Community Service', value: 'community-service' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Health',
|
||||
value: 'health',
|
||||
subcategories: [
|
||||
{ label: 'Surgery', value: 'health-surgery' },
|
||||
{ label: 'Illness', value: 'health-illness' },
|
||||
{ label: 'Recovery', value: 'health-recovery' },
|
||||
{ label: 'Transplants', value: 'health-transplants' },
|
||||
{ label: 'Mental Health', value: 'health-mental-health' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Religious & Spiritual',
|
||||
value: 'religious',
|
||||
subcategories: [
|
||||
{ label: 'Baptism', value: 'religious-baptism' },
|
||||
{ label: 'Bar/Bat Mitzvah', value: 'religious-bar-bat-mitzvah' },
|
||||
{ label: 'Communion', value: 'religious-communion' },
|
||||
{ label: 'Confirmation', value: 'religious-confirmation' },
|
||||
{ label: 'Pilgrimage', value: 'religious-pilgrimage' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Travel & Adventure',
|
||||
value: 'travel',
|
||||
subcategories: [
|
||||
{ label: 'Travel', value: 'travel-general' },
|
||||
{ label: 'Vacation', value: 'vacation' },
|
||||
{ label: 'Adventure', value: 'adventure' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Life Changes',
|
||||
value: 'life-changes',
|
||||
subcategories: [
|
||||
{ label: 'Moving', value: 'moving' },
|
||||
{ label: 'License', value: 'license' },
|
||||
{ label: 'Voting', value: 'voting' },
|
||||
{ label: 'Citizenship', value: 'citizenship' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Milestones',
|
||||
value: 'milestones',
|
||||
subcategories: []
|
||||
}
|
||||
]
|
||||
|
||||
// Flattened category options for backward compatibility and simple select usage
|
||||
export const categoryOptions = categoryStructure.reduce((acc, category) => {
|
||||
// Add main category if it has no subcategories
|
||||
if (category.subcategories.length === 0) {
|
||||
acc.push({ label: category.label, value: category.value })
|
||||
} else {
|
||||
// Add subcategories with main category prefix
|
||||
category.subcategories.forEach(sub => {
|
||||
acc.push({
|
||||
label: `${category.label} - ${sub.label}`,
|
||||
value: sub.value,
|
||||
mainCategory: category.value,
|
||||
subcategory: sub.value
|
||||
})
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// Helper functions for category management
|
||||
export const getCategoryStructure = () => categoryStructure
|
||||
|
||||
export const getMainCategories = () => {
|
||||
return categoryStructure.map(cat => ({
|
||||
label: cat.label,
|
||||
value: cat.value
|
||||
}))
|
||||
}
|
||||
|
||||
export const getSubcategories = (mainCategoryValue) => {
|
||||
const mainCategory = categoryStructure.find(cat => cat.value === mainCategoryValue)
|
||||
return mainCategory ? mainCategory.subcategories : []
|
||||
}
|
||||
|
||||
export const getCategoryByValue = (value) => {
|
||||
return categoryOptions.find(cat => cat.value === value)
|
||||
}
|
||||
|
||||
export const getMainCategoryFromValue = (value) => {
|
||||
const category = getCategoryByValue(value)
|
||||
return category ? category.mainCategory : null
|
||||
}
|
||||
|
||||
export const tagOptions = [
|
||||
// Emotions
|
||||
'happy', 'sad', 'exciting', 'stressful', 'memorable', 'important',
|
||||
'fun', 'challenging', 'rewarding', 'disappointing', 'surprising',
|
||||
'life-changing', 'routine', 'special', 'difficult', 'joyful',
|
||||
'overwhelming', 'peaceful', 'anxious', 'proud', 'grateful',
|
||||
'emotional', 'touching', 'inspiring', 'motivating', 'healing',
|
||||
|
||||
// Significance
|
||||
'milestone', 'achievement', 'breakthrough', 'turning-point',
|
||||
'first-time', 'last-time', 'once-in-a-lifetime', 'unexpected',
|
||||
'planned', 'spontaneous', 'tradition', 'new-experience',
|
||||
|
||||
// Social
|
||||
'family', 'friends', 'colleagues', 'community', 'solo',
|
||||
'group', 'intimate', 'public', 'private', 'celebration',
|
||||
|
||||
// Intensity
|
||||
'intense', 'mild', 'dramatic', 'subtle', 'overwhelming',
|
||||
'gradual', 'sudden', 'anticipated', 'shocking', 'gentle',
|
||||
|
||||
// Time-related
|
||||
'brief', 'extended', 'momentary', 'lasting', 'temporary',
|
||||
'permanent', 'seasonal', 'annual', 'weekly', 'daily'
|
||||
]
|
||||
|
||||
export const personOptions = [
|
||||
'Anna Mueller',
|
||||
'Max Schmidt',
|
||||
'Sarah Johnson',
|
||||
'Michael Weber',
|
||||
'Lisa Anderson',
|
||||
'Thomas Brown',
|
||||
'Julia Martinez',
|
||||
'David Wilson',
|
||||
'Emma Garcia',
|
||||
'Robert Davis'
|
||||
]
|
||||
|
||||
export const defaultFormData = {
|
||||
keyImage: null,
|
||||
keyImageUrl: '',
|
||||
additionalImages: [],
|
||||
additionalImageUrls: [],
|
||||
level: 0,
|
||||
categories: [],
|
||||
headline: '',
|
||||
subheadline: '',
|
||||
text: '',
|
||||
tags: [],
|
||||
location: '',
|
||||
date: '',
|
||||
time: '',
|
||||
audioFiles: [],
|
||||
audioRecordings: [],
|
||||
videoFiles: [],
|
||||
videoRecordings: [],
|
||||
relatedPersons: []
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue