docker setup

# Conflicts:
#	.gitignore
#	backend/vite.config.js
#	frontend/package-lock.json
This commit is contained in:
Kevin Adametz 2025-10-20 17:55:52 +02:00
parent f01a0a967f
commit c62234e1ca
546 changed files with 141382 additions and 757 deletions

View file

@ -0,0 +1,519 @@
// Define interfaces
export interface DotConfig {
id: number;
value: number;
x: number;
link?: string; // URL to navigate to 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 || 80;
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;
// 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.625;
// Calculate raw Y position
// height of the amplitude
const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7);
// Calculate minimum Y position to ensure tooltip fits
const minY = this.config.tooltipHeight + 30; // 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) {
const link = document.createElement("a");
link.href = dot.link;
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", () => {
if (dot.link) {
window.location.href = dot.link;
} else {
console.error("Dot has no link");
throw new Error("Dot has no link");
}
});
} 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
if (dot.link) {
circle.addEventListener("click", () => {
if (dot.link) {
window.location.href = dot.link;
} else {
console.error("Dot has no link");
throw new Error("Dot has no link");
}
});
}
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 {
this.config.height = window.innerHeight;
this.svg.setAttribute("height", `${this.config.height}`);
this.render();
}
}

320
dot-line-system/src/main.ts Normal file
View file

@ -0,0 +1,320 @@
import {
ConnectedDotsVisualization,
type DotConfig,
} from "./ConnectedDotsVisualization";
import '../src/style.css';
/*
* If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output.
*
* For Vite:
* - Place your images in the `public/images` directory instead of `src/images`.
* - Reference them as `/images/filename.png` in your code.
*
* For Webpack:
* - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory.
*
* For static hosting (e.g., GitHub Pages, Netlify):
* - Make sure the images are in the output directory (e.g., `dist/images`) after build.
*
* Example for Vite:
* Move images to `public/images` and update imageUrl paths to `/images/filename.png`.
*/
// Sample dot configurations
const sampleDots: DotConfig[] = [
{
id: 1,
value: -1.8,
x: -2,
imageUrl:
"/images/0_3.png",
title: "Beginn des neuen Abenteuers",
description: "01.10.2024",
link: "/page1",
},
{
id: 2,
value: 1.2,
x: 0,
imageUrl:
"/images/0_2.png",
title: "Omas Annis Geburtstag",
description: "02.10.2024",
link: "/page2",
},
{
id: 3,
value: -0.6,
x: 2,
imageUrl:
"/images/disco.png",
title: "Konzertbesuch mit Freunden",
description: "03.10.2024",
link: "/page3",
},
{
id: 4,
value:3,
x: 4,
imageUrl:
"/images/pferd.png",
title: "Wanderreiten in den Bergen",
description: "04.10.2024",
link: "/page4",
},
{
id: 5,
value: 1,
x: 6,
imageUrl:
"/images/gpt.png",
title: "Ruhiger Tag zu Hause",
description: "05.10.2024",
link: "/page5",
},
{
id: 6,
value: -3,
x: 8,
imageUrl:
"/images/oma.png",
title: "Oma Erna verstorben",
description: "06.10.2024",
link: "/page6",
},
{
id: 7,
value: 1.5,
x: 10,
imageUrl:
"/images/see.png",
title: "Erholungsausflug zum See",
description: "07.10.2024",
link: "/page7",
},
{
id: 8,
value: 0,
x: 12,
imageUrl:
"/images/feier.png",
title: "Kleine Wochenendsfeier",
description: "08.10.2024",
link: "/page8",
},
{
id: 9,
value: 3,
x: 14,
imageUrl:
"/images/hochzeit.png",
title: "Hochzeit von Cousine Tatjana",
description: "09.10.2024",
link: "/page9",
},
{
id: 10,
value: 1,
x: 16,
imageUrl:
"/images/work.png",
title: "Erster Tag im neuen Job",
description: "10.10.2024",
link: "/page10",
},
{
id: 11,
value: -1.2,
x: 18,
imageUrl:
"/images/klasse.png",
title: "Klassentreffen nach vielen Jahren",
description: "11.10.2024",
link: "/page11",
},
{
id: 12,
value: -0.6,
x: 20,
imageUrl:
"/images/familie.png",
title: "Familienabendessen",
description: "12.10.2024",
link: "/page12",
},
{
id: 13,
value: 2.7,
x: 22,
imageUrl:
"/images/kinobesuch.png",
title: "Kinobesuch mit der ganzen Familie",
description: "13.10.2024",
link: "/page13",
},
{
id: 14,
value: 0,
x: 24,
imageUrl:
"/images/entspannung.png",
title: "Entspannung",
description: "14.10.2024",
link: "/page14",
},
{
id: 15,
value: -2.9,
x: 26,
imageUrl: "/images/sonntag.png",
title: "Geruhsamer Sonntag",
description: "15.10.2024",
link: "/page15",
},
{
id: 16,
value: 1.5,
x: 28,
imageUrl:
"/images/kindergeburtstag.png",
title: "Kindergeburtstag",
description: "16.10.2024",
link: "/page16",
},
{
id: 17,
value: 0,
x: 30,
imageUrl:
"/images/familie2.png",
title: "Spaziergang mit der Familie",
description: "17.10.2024",
link: "/page17",
},
{
id: 18,
value: 2.1,
x: 32,
imageUrl:
"/images/grosseltern.png",
title: "Familienfeier bei den Großeltern",
description: "18.10.2024",
link: "/page18",
},
];
// Wait for DOM to be fully loaded
document.addEventListener("DOMContentLoaded", () => {
// Initialize the visualization with the sample dots
const visualization = new ConnectedDotsVisualization(
"scroll-container",
sampleDots,
{
// Optional custom configuration
dotRadius: 8,
// tooltipWidth: 100,
// tooltipHeight: 100,
}
);
// Handle window resize
window.addEventListener("resize", () => {
visualization.resize();
});
// Example of updating dots dynamically (if needed)
/*
const updateButton = document.createElement('button');
updateButton.textContent = 'Update Data';
updateButton.classList.add('button');
updateButton.style.marginTop = '10px';
document.body.appendChild(updateButton);
updateButton.addEventListener('click', () => {
// Generate some new random data with image tooltips
const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({
id: i + 1,
value: Math.random() * 6 - 3, // Random value between -3 and 3
x: i - 3,
imageUrl: `https://picsum.photos/200/150?random=${i+10}`,
title: `Point ${i+1}`,
description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}`
}));
visualization.updateDots(newDots);
});
*/
const scrollContainer = document.querySelector(".scroll-container") as HTMLElement;
let isDown = false;
let startX: number;
let scrollLeft: number;
// Mouse events
scrollContainer.addEventListener("mousedown", (e) => {
isDown = true;
scrollContainer.classList.add("active");
startX = e.pageX - scrollContainer.offsetLeft;
scrollLeft = scrollContainer.scrollLeft;
// Remove smooth scrolling while dragging
scrollContainer.classList.remove("smooth-scroll");
});
scrollContainer.addEventListener("mouseleave", () => {
if (!isDown) return;
isDown = false;
scrollContainer.classList.remove("active");
// Add smooth scrolling after dragging
scrollContainer.classList.add("smooth-scroll");
});
scrollContainer.addEventListener("mouseup", () => {
if (!isDown) return;
isDown = false;
scrollContainer.classList.remove("active");
// Add smooth scrolling after dragging
scrollContainer.classList.add("smooth-scroll");
});
scrollContainer.addEventListener("mousemove", (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - scrollContainer.offsetLeft;
const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling
scrollContainer.scrollLeft = scrollLeft - walk;
});
// Touch events
scrollContainer.addEventListener("touchstart", (e) => {
isDown = true;
scrollContainer.classList.add("active");
startX = e.touches[0].pageX - scrollContainer.offsetLeft;
scrollLeft = scrollContainer.scrollLeft;
// Remove smooth scrolling while dragging
scrollContainer.classList.remove("smooth-scroll");
});
scrollContainer.addEventListener("touchend", () => {
if (!isDown) return;
isDown = false;
scrollContainer.classList.remove("active");
// Add smooth scrolling after dragging
scrollContainer.classList.add("smooth-scroll");
});
scrollContainer.addEventListener("touchmove", (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.touches[0].pageX - scrollContainer.offsetLeft;
const walk = (x - startX) * 3;
scrollContainer.scrollLeft = scrollLeft - walk;
});
});

View file

@ -0,0 +1,203 @@
body {
font-family: "Barlow Condensed", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.controls {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 500px;
margin-bottom: 10px;
}
.button {
padding: 6px 12px;
background-color: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.visualization-container {
position: absolute;
width: 100vw;
height: 100vh;
left: 0;
right: 0;
margin-left: calc(-50vw + 50%);
}
.gradient-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
min-width: 100%;
z-index: -1;
background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2);
background-size: 200% 200%;
animation: gradientAnimation 20s ease infinite;
}
@keyframes gradientAnimation {
0% {
background-position: 0% 0%;
}
25% {
background-position: 100% 0%;
}
50% {
background-position: 100% 100%;
}
75% {
background-position: 0% 100%;
}
100% {
background-position: 0% 0%;
}
}
.median {
position: fixed;
top: 61.5%;
left: 0;
height: 1px;
border-top: 1px dashed rgba(255, 255, 255, 0.2);
width: 100%;
z-index: -1;
/* background-color: rgba(255, 255, 255, 0.2); */
}
.scroll-container {
position: relative;
height: 100%;
overflow-x: hidden;
overflow-y: hidden;
scroll-behavior: smooth;
box-sizing: border-box;
}
.scroll-container:active {
cursor: grabbing;
/* Change cursor when active */
}
.smooth-scroll {
transition: scroll-left 0.5s ease-out; /* Add easing on scroll-left */
}
.spacer {
height: 100vh;
}
.dot-tooltip .tooltip-background {
fill: rgba(0, 0, 0, 0.0);
}
.dot-tooltip .tooltip-content {
display: flex;
flex-direction: column;
justify-content: center;
/* Center vertically */
align-items: center;
/* Center horizontally */
width: 100%;
height: 100%;
color: white;
/* Text color */
}
.dot-tooltip .image_container {
margin-top: 8px;
/* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */
box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25);
transition: box-shadow 0.25s ease-in-out;
}
.dot-tooltip .image_container:hover {
box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8);
/* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */
transition: box-shadow 0.25s ease-in-out;
}
.dot-tooltip .tooltip-image {
width: 100%;
height: auto;
display: block;
pointer-events: auto;
}
.dot-tooltip .tooltip-title {
font-size: 14px;
font-weight: 400;
margin-bottom: 2px;
text-align: center;
text-wrap: balance;
hyphens: auto;
line-height: 1.1;
}
.dot-tooltip .tooltip-description {
font-size: 12px;
font-weight: 300;
}
.dot-tooltip .image_container {
width: 80px;
height: 80px;
overflow: hidden;
border-radius: 50%;
border: 2px solid white;
display: flex;
justify-content: center;
}
.dot-tooltip .tooltip-arrow {
width: 1px;
height: 30px;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent);
}
.dot {
transition: r 0.2s ease, fill 0.2s ease;
cursor: pointer;
}
.dot:hover {
fill: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
}
.dot-tooltip {
pointer-events: none;
opacity: 1;
/* Always visible */
}
.tooltip-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
dot-line-system/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />