feat(home): integrate motion components in home page
All checks were successful
Staging Build / build (push) Successful in 2m59s
All checks were successful
Staging Build / build (push) Successful in 2m59s
This commit is contained in:
249
AsciiShaderBackground.astro
Normal file
249
AsciiShaderBackground.astro
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
---
|
||||||
|
// AsciiShaderBackground.astro
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className = "" } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={`ascii-shader-container ${className}`}>
|
||||||
|
<canvas id="ascii-shader-canvas"></canvas>
|
||||||
|
<div class="grid-overlay"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ascii-shader-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: -1;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: oklch(0.145 0.01 148);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ascii-shader-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
will-change: transform;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: 40px 40px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, oklch(0.31 0.012 148 / 0.3) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, oklch(0.31 0.012 148 / 0.3) 1px, transparent 1px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// OKLCH Palette definition
|
||||||
|
interface OKLCHColor {
|
||||||
|
l: number;
|
||||||
|
c: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette defining color stops based on scroll progress
|
||||||
|
const PALETTE = {
|
||||||
|
yellow: { l: 0.88, c: 0.18, h: 96 },
|
||||||
|
green: { l: 0.76, c: 0.15, h: 155 },
|
||||||
|
blue: { l: 0.68, c: 0.18, h: 255 },
|
||||||
|
purple: { l: 0.64, c: 0.18, h: 302 },
|
||||||
|
pink: { l: 0.72, c: 0.20, h: 345 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper for shortest angle interpolation in OKLCH hue space
|
||||||
|
function interpolateHue(h1: number, h2: number, t: number): number {
|
||||||
|
let diff = (h2 - h1) % 360;
|
||||||
|
if (diff > 180) diff -= 360;
|
||||||
|
if (diff < -180) diff += 360;
|
||||||
|
return (h1 + diff * t + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate two OKLCH colors
|
||||||
|
function lerpOKLCH(c1: OKLCHColor, c2: OKLCHColor, t: number): OKLCHColor {
|
||||||
|
return {
|
||||||
|
l: c1.l + (c2.l - c1.l) * t,
|
||||||
|
c: c1.c + (c2.c - c1.c) * t,
|
||||||
|
h: interpolateHue(c1.h, c2.h, t)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate dynamic colors based on scroll progress (0.0 to 1.0)
|
||||||
|
function getScrollColors(progress: number): { primary: OKLCHColor; secondary: OKLCHColor } {
|
||||||
|
if (progress < 0.33) {
|
||||||
|
// Hero to stage 1: Full spectrum (Pink -> Yellow)
|
||||||
|
const t = progress / 0.33;
|
||||||
|
return {
|
||||||
|
primary: lerpOKLCH(PALETTE.pink, PALETTE.yellow, t),
|
||||||
|
secondary: lerpOKLCH(PALETTE.purple, PALETTE.green, t)
|
||||||
|
};
|
||||||
|
} else if (progress < 0.66) {
|
||||||
|
// Stage 1 to stage 2: Yellow/Cream to Green
|
||||||
|
const t = (progress - 0.33) / 0.33;
|
||||||
|
return {
|
||||||
|
primary: lerpOKLCH(PALETTE.yellow, PALETTE.green, t),
|
||||||
|
secondary: lerpOKLCH(PALETTE.green, PALETTE.blue, t)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Stage 2 to end: Green/Blue focus
|
||||||
|
const t = (progress - 0.66) / 0.34;
|
||||||
|
return {
|
||||||
|
primary: lerpOKLCH(PALETTE.green, PALETTE.blue, t),
|
||||||
|
secondary: lerpOKLCH(PALETTE.blue, PALETTE.purple, t)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsciiShaderEngine {
|
||||||
|
private canvas: HTMLCanvasElement;
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
private mouse = { x: 0, y: 0, targetX: 0, targetY: 0 };
|
||||||
|
private scrollProgress = 0;
|
||||||
|
private targetScrollProgress = 0;
|
||||||
|
private chars = ['/', '\\', '-', '|', '+', '='];
|
||||||
|
private fontSize = 16;
|
||||||
|
private cols = 0;
|
||||||
|
private rows = 0;
|
||||||
|
private animationFrameId = 0;
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) throw new Error('Could not get 2D context');
|
||||||
|
this.ctx = context;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(): void {
|
||||||
|
this.resize();
|
||||||
|
this.bindEvents();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private resize = (): void => {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
this.canvas.width = width * dpr;
|
||||||
|
this.canvas.height = height * dpr;
|
||||||
|
|
||||||
|
this.ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
this.cols = Math.ceil(width / this.fontSize);
|
||||||
|
this.rows = Math.ceil(height / this.fontSize);
|
||||||
|
|
||||||
|
if (this.mouse.x === 0 && this.mouse.y === 0) {
|
||||||
|
this.mouse.x = width / 2;
|
||||||
|
this.mouse.y = height / 2;
|
||||||
|
this.mouse.targetX = width / 2;
|
||||||
|
this.mouse.targetY = height / 2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private bindEvents(): void {
|
||||||
|
window.addEventListener('resize', this.resize);
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', (e: PointerEvent) => {
|
||||||
|
this.mouse.targetX = e.clientX;
|
||||||
|
this.mouse.targetY = e.clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
const maxScroll = Math.max(
|
||||||
|
document.body.scrollHeight - window.innerHeight,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
this.targetScrollProgress = Math.min(Math.max(window.scrollY / maxScroll, 0), 1);
|
||||||
|
}, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private render = (time: number = 0): void => {
|
||||||
|
// Dampened mouse easing (lerp factor 0.05)
|
||||||
|
this.mouse.x += (this.mouse.targetX - this.mouse.x) * 0.05;
|
||||||
|
this.mouse.y += (this.mouse.targetY - this.mouse.y) * 0.05;
|
||||||
|
|
||||||
|
// Dampened scroll progress easing
|
||||||
|
this.scrollProgress += (this.targetScrollProgress - this.scrollProgress) * 0.05;
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
// Get current OKLCH interpolated colors
|
||||||
|
const { primary, secondary } = getScrollColors(this.scrollProgress);
|
||||||
|
|
||||||
|
this.ctx.font = `${this.fontSize}px monospace`;
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
|
const timeSec = time * 0.001;
|
||||||
|
|
||||||
|
for (let r = 0; r < this.rows; r++) {
|
||||||
|
for (let c = 0; c < this.cols; c++) {
|
||||||
|
const x = c * this.fontSize + this.fontSize / 2;
|
||||||
|
const y = r * this.fontSize + this.fontSize / 2;
|
||||||
|
|
||||||
|
// Vector field calculations based on distance to cursor & wave function
|
||||||
|
const dx = x - this.mouse.x;
|
||||||
|
const dy = y - this.mouse.y;
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// Dynamic angle field
|
||||||
|
const angle = Math.atan2(dy, dx) + Math.sin(dist * 0.01 - timeSec) * 0.5;
|
||||||
|
const charIndex = Math.floor(Math.abs(Math.sin(angle + dist * 0.005)) * this.chars.length) % this.chars.length;
|
||||||
|
const char = this.chars[charIndex];
|
||||||
|
|
||||||
|
// Color blend based on vector field intensity
|
||||||
|
const blendFactor = Math.min(Math.max(1 - dist / 400, 0), 1);
|
||||||
|
const currentColor = lerpOKLCH(secondary, primary, blendFactor);
|
||||||
|
|
||||||
|
// Render character with OKLCH styling
|
||||||
|
this.ctx.fillStyle = `oklch(${currentColor.l.toFixed(3)} ${currentColor.c.toFixed(3)} ${currentColor.h.toFixed(1)})`;
|
||||||
|
this.ctx.fillText(char, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.render);
|
||||||
|
};
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
window.removeEventListener('resize', this.resize);
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle handler for Astro / SPA transitions
|
||||||
|
function initShader() {
|
||||||
|
const canvas = document.getElementById('ascii-shader-canvas') as HTMLCanvasElement;
|
||||||
|
if (canvas) {
|
||||||
|
new AsciiShaderEngine(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initShader);
|
||||||
|
} else {
|
||||||
|
initShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Astro page swap support
|
||||||
|
document.addEventListener('astro:page-load', initShader);
|
||||||
|
</script>
|
||||||
@@ -31,6 +31,11 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
import { AsciiShaderBackground } from '@/components/AsciiShaderBackground'
|
||||||
|
import { ProcessSteps } from '@/components/ProcessSteps'
|
||||||
|
import { ScrollIndicator } from '@/components/ScrollIndicator'
|
||||||
|
import { WorkspaceZentrale } from '@/components/WorkspaceZentrale'
|
||||||
|
|
||||||
interface HomeClientProps {
|
interface HomeClientProps {
|
||||||
initialUser: User | null
|
initialUser: User | null
|
||||||
}
|
}
|
||||||
@@ -78,7 +83,8 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-950 text-slate-100 overflow-hidden relative selection:bg-blue-500/30 selection:text-blue-200">
|
<div className="min-h-screen bg-transparent text-slate-100 overflow-hidden relative selection:bg-blue-500/30 selection:text-blue-200">
|
||||||
|
<AsciiShaderBackground />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -249,123 +255,11 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Grid of modules */}
|
{/* Grid of modules / Workspace-Zentrale */}
|
||||||
<section className="w-full py-16 md:py-24 border-t border-slate-900 bg-slate-950/50 relative">
|
<WorkspaceZentrale />
|
||||||
<div className="container max-w-6xl mx-auto px-4 relative">
|
|
||||||
|
|
||||||
<div className="absolute top-1/4 right-0 w-[400px] h-[400px] bg-indigo-600/5 blur-[120px] rounded-full -z-10 pointer-events-none" />
|
|
||||||
|
|
||||||
<div className="text-center space-y-3 mb-16">
|
|
||||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-white">
|
|
||||||
Workspace-Zentrale
|
|
||||||
</h2>
|
|
||||||
<p className="text-slate-400 max-w-[600px] mx-auto text-sm sm:text-base">
|
|
||||||
Die zentralen Plattformen für unsere tägliche Zusammenarbeit, Dokumentation und den Support.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{/* Card 1 */}
|
|
||||||
<a href="https://support.teamzone.softengine.de" target="_blank" rel="noopener noreferrer"><motion.div
|
|
||||||
whileHover={{ y: -4 }}
|
|
||||||
className="group relative flex flex-col p-6 rounded-2xl border border-slate-800 bg-slate-900/40 hover:bg-slate-900/80 hover:border-slate-700 transition-all duration-200 shadow-md overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="space-y-4 relative">
|
|
||||||
<div className="inline-flex p-3 bg-blue-500/10 rounded-xl text-blue-400 group-hover:bg-blue-500/20 group-hover:scale-110 transition-all duration-300">
|
|
||||||
<MessageSquare className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Zulip</h3>
|
|
||||||
<p className="text-slate-400 text-sm leading-relaxed">
|
|
||||||
Treten Sie unserem Partner-Chat bei! tauschen Sie sich mit anderen CASPOS Partnern aus.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</motion.div></a>
|
|
||||||
|
|
||||||
{/* Card 2 */}
|
|
||||||
<a href="https://wiki.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
|
|
||||||
whileHover={{ y: -4 }}
|
|
||||||
className="group relative flex flex-col p-6 rounded-2xl border border-slate-800 bg-slate-900/40 hover:bg-slate-900/80 hover:border-slate-700 transition-all duration-200 shadow-md overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="space-y-4 relative">
|
|
||||||
<div className="inline-flex p-3 bg-indigo-500/10 rounded-xl text-indigo-400 group-hover:bg-indigo-500/20 group-hover:scale-110 transition-all duration-300">
|
|
||||||
<BookOpen className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Wiki</h3>
|
|
||||||
<p className="text-slate-400 text-sm leading-relaxed">
|
|
||||||
Ihr Wissens-Portal für die CASPOS Produktfamilie.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</motion.div></a>
|
|
||||||
|
|
||||||
{/* Card 3 */}
|
|
||||||
<a href="https://support.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
|
|
||||||
whileHover={{ y: -4 }}
|
|
||||||
className="group relative flex flex-col p-6 rounded-2xl border border-slate-800 bg-slate-900/40 hover:bg-slate-900/80 hover:border-slate-700 transition-all duration-200 shadow-md overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="space-y-4 relative">
|
|
||||||
<div className="inline-flex p-3 bg-purple-500/10 rounded-xl text-purple-400 group-hover:bg-purple-500/20 group-hover:scale-110 transition-all duration-300">
|
|
||||||
<LifeBuoy className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Support</h3>
|
|
||||||
<p className="text-slate-400 text-sm leading-relaxed">
|
|
||||||
Sie haben ein technisches Problem in der CASPOS? Erstellen Sie jetzt ein Ticket!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</motion.div></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Stepper info */}
|
{/* Stepper info */}
|
||||||
<section className="w-full py-16 md:py-24 border-t border-slate-900 bg-slate-950/30">
|
<ProcessSteps />
|
||||||
<div className="container max-w-5xl mx-auto px-4">
|
|
||||||
<div className="text-center space-y-3 mb-16">
|
|
||||||
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-center text-white">
|
|
||||||
Wie funktioniert die Bereitstellung?
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
{/* Connection line for Desktop */}
|
|
||||||
<div className="hidden md:block absolute top-[28px] left-[15%] right-[15%] h-[1px] border-t border-dashed border-slate-800 z-0" />
|
|
||||||
|
|
||||||
<div className="grid gap-8 md:grid-cols-3 relative z-10">
|
|
||||||
{/* Step 1 */}
|
|
||||||
<div className="flex flex-col items-center text-center space-y-4 p-4 group">
|
|
||||||
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-slate-900 border border-slate-800 text-blue-400 font-black text-lg shadow-lg group-hover:border-blue-500/40 group-hover:shadow-[0_0_15px_rgba(59,130,246,0.15)] transition-all duration-300">
|
|
||||||
1
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-bold text-slate-200 group-hover:text-white transition-colors">Kunde wählen</h4>
|
|
||||||
<p className="text-sm text-slate-400 leading-relaxed max-w-[250px]">
|
|
||||||
Gewünschten Kunden auswählen, der die Lizenz erhalten soll.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step 2 */}
|
|
||||||
<div className="flex flex-col items-center text-center space-y-4 p-4 group">
|
|
||||||
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-slate-900 border border-slate-800 text-blue-400 font-black text-lg shadow-lg group-hover:border-blue-500/40 group-hover:shadow-[0_0_15px_rgba(59,130,246,0.15)] transition-all duration-300">
|
|
||||||
2
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-bold text-slate-200 group-hover:text-white transition-colors">Lizenz wählen</h4>
|
|
||||||
<p className="text-sm text-slate-400 leading-relaxed max-w-[250px]">
|
|
||||||
Gewünschte Lizenzen für den Kunden auswählen und den Bestellung abschließen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step 3 */}
|
|
||||||
<div className="flex flex-col items-center text-center space-y-4 p-4 group">
|
|
||||||
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-slate-900 border border-slate-800 text-blue-400 font-black text-lg shadow-lg group-hover:border-blue-500/40 group-hover:shadow-[0_0_15px_rgba(59,130,246,0.15)] transition-all duration-300">
|
|
||||||
3
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-bold text-slate-200 group-hover:text-white transition-colors">Live gehen</h4>
|
|
||||||
<p className="text-sm text-slate-400 leading-relaxed max-w-[250px]">
|
|
||||||
Nach dem Erhalt der Lizenz Live gehen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|||||||
Reference in New Issue
Block a user