Compare commits
5 Commits
b978a6bc64
...
ec264e2680
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec264e2680 | ||
|
|
21e01b1b24 | ||
|
|
304d25f202 | ||
|
|
a2305319e1 | ||
|
|
70ed890702 |
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>
|
||||
191
shop/components/AsciiShaderBackground.tsx
Normal file
191
shop/components/AsciiShaderBackground.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
interface OKLCHColor {
|
||||
l: number
|
||||
c: number
|
||||
h: number
|
||||
}
|
||||
|
||||
// Monochromes Farbkonzept im OKLCH-Raum
|
||||
const BASE_BG = { l: 0.145, c: 0.01, h: 148 }
|
||||
const BASE_DIM = { l: 0.50, c: 0.01, h: 148 }
|
||||
const HIGHLIGHT_BRIGHT = { l: 0.95, c: 0.01, h: 148 }
|
||||
|
||||
function lerp(a: number, b: number, t: number): number {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
function lerpOKLCH(c1: OKLCHColor, c2: OKLCHColor, t: number): OKLCHColor {
|
||||
return {
|
||||
l: lerp(c1.l, c2.l, t),
|
||||
c: lerp(c1.c, c2.c, t),
|
||||
h: lerp(c1.h, c2.h, t)
|
||||
}
|
||||
}
|
||||
|
||||
export function AsciiShaderBackground({ className = '' }: { className?: string }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
let animationFrameId: number
|
||||
|
||||
// Minimalistisches, ruhiges ASCII-Array
|
||||
const chars = ['•', '∘', '+', '▪', '▫', ' ']
|
||||
const fontSize = 18
|
||||
let cols = 0
|
||||
let rows = 0
|
||||
|
||||
const mouse = { x: 0, y: 0, targetX: 0, targetY: 0 }
|
||||
let scrollProgress = 0
|
||||
let targetScrollProgress = 0
|
||||
|
||||
const resize = () => {
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
cols = Math.ceil(width / fontSize)
|
||||
rows = Math.ceil(height / fontSize)
|
||||
|
||||
if (mouse.x === 0 && mouse.y === 0) {
|
||||
mouse.x = width / 2
|
||||
mouse.y = height / 2
|
||||
mouse.targetX = width / 2
|
||||
mouse.targetY = height / 2
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
mouse.targetX = e.clientX
|
||||
mouse.targetY = e.clientY
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
const maxScroll = Math.max(
|
||||
document.body.scrollHeight - window.innerHeight,
|
||||
1
|
||||
)
|
||||
targetScrollProgress = Math.min(Math.max(window.scrollY / maxScroll, 0), 1)
|
||||
}
|
||||
|
||||
resize()
|
||||
window.addEventListener('resize', resize)
|
||||
window.addEventListener('pointermove', onPointerMove)
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
|
||||
const render = (time: number) => {
|
||||
mouse.x += (mouse.targetX - mouse.x) * 0.05
|
||||
mouse.y += (mouse.targetY - mouse.y) * 0.05
|
||||
scrollProgress += (targetScrollProgress - scrollProgress) * 0.05
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Beim Scrollen sofort ausblenden (vollständig weg ab 150px Scroll-Tiefe)
|
||||
const scrollFade = Math.max(1 - (window.scrollY / 150), 0)
|
||||
if (scrollFade <= 0) {
|
||||
animationFrameId = requestAnimationFrame(render)
|
||||
return
|
||||
}
|
||||
|
||||
// Scroll-gesteuerter Dimmer
|
||||
const scrollDimFactor = (1 - scrollProgress * 0.4) * scrollFade
|
||||
const currentDimColor = {
|
||||
...BASE_DIM,
|
||||
l: BASE_DIM.l * scrollDimFactor
|
||||
}
|
||||
|
||||
ctx.font = `${fontSize}px monospace`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const timeSec = time * 0.001
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const x = c * fontSize + fontSize / 2
|
||||
const y = r * fontSize + fontSize / 2
|
||||
|
||||
// Abstandsvektor zur gedämpften Mausposition
|
||||
const dx = x - mouse.x
|
||||
const dy = y - mouse.y
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
// Maus-Störung (Vektorfeld-Ablenkung): Phase & Amplitude lokal beeinflussen
|
||||
const mouseInfluence = Math.exp(-dist / 250)
|
||||
const mouseDistortion = (Math.atan2(dy, dx) + (dx + dy) * 0.003) * mouseInfluence * 3.0
|
||||
|
||||
// Diagonale 2D-Strömung / Wellen-Synthese aus Sinus & Kosinus
|
||||
const waveX = Math.sin(c * 0.08 + timeSec * 0.8 + mouseDistortion)
|
||||
const waveY = Math.cos(r * 0.08 + timeSec * 0.6 + mouseDistortion)
|
||||
const flowValue = Math.sin(waveX + waveY + (c + r) * 0.04 + timeSec * 0.4)
|
||||
|
||||
// Zeichenauswahl basierend auf Strömungsfeld
|
||||
const normalizedFlow = (flowValue + 1) * 0.5 // Range 0..1
|
||||
const charIndex = Math.floor(normalizedFlow * chars.length) % chars.length
|
||||
const char = chars[charIndex]
|
||||
|
||||
// Leerzeichen für ruhiges Raster überspringen
|
||||
if (char === ' ') continue
|
||||
|
||||
// Helligkeits-Highlighting durch Maus-Störung und Wellenkamm
|
||||
const blendFactor = Math.min(Math.max(mouseInfluence * 0.85 + normalizedFlow * 0.15, 0), 1)
|
||||
const currentColor = lerpOKLCH(currentDimColor, HIGHLIGHT_BRIGHT, blendFactor)
|
||||
|
||||
// Stärkerer Kontrast: min 0.35, max 0.95 Deckkraft
|
||||
const alpha = (0.35 + blendFactor * 0.6) * scrollDimFactor
|
||||
|
||||
ctx.fillStyle = `oklch(${currentColor.l.toFixed(3)} ${currentColor.c.toFixed(3)} ${currentColor.h.toFixed(1)} / ${alpha.toFixed(2)})`
|
||||
ctx.fillText(char, x, y)
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(render)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize)
|
||||
window.removeEventListener('pointermove', onPointerMove)
|
||||
window.removeEventListener('scroll', onScroll)
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-0 left-0 right-0 h-[500px] z-0 overflow-hidden bg-[oklch(0.145_0.01_148)] pointer-events-none ${className}`}
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 250px, rgba(0,0,0,0) 500px)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 250px, rgba(0,0,0,0) 500px)'
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0 w-full h-full block will-change-transform pointer-events-none"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full pointer-events-none opacity-60"
|
||||
style={{
|
||||
backgroundSize: '40px 40px',
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, oklch(0.31 0.012 148 / 0.4) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, oklch(0.31 0.012 148 / 0.4) 1px, transparent 1px)
|
||||
`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,11 @@ import {
|
||||
DialogFooter,
|
||||
} 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 {
|
||||
initialUser: User | null
|
||||
}
|
||||
@@ -78,7 +83,8 @@ export function HomeClient({ initialUser }: HomeClientProps) {
|
||||
}
|
||||
|
||||
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>
|
||||
</Dialog>
|
||||
|
||||
{/* Grid of modules */}
|
||||
<section className="w-full py-16 md:py-24 border-t border-slate-900 bg-slate-950/50 relative">
|
||||
<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>
|
||||
{/* Grid of modules / Workspace-Zentrale */}
|
||||
<WorkspaceZentrale />
|
||||
|
||||
{/* Stepper info */}
|
||||
<section className="w-full py-16 md:py-24 border-t border-slate-900 bg-slate-950/30">
|
||||
<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>
|
||||
<ProcessSteps />
|
||||
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
155
shop/components/ProcessSteps.tsx
Normal file
155
shop/components/ProcessSteps.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface Step {
|
||||
number: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
number: '1',
|
||||
title: 'Kunde wählen',
|
||||
description: 'Gewünschten Kunden auswählen, der die Lizenz erhalten soll.'
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
title: 'Lizenz wählen',
|
||||
description: 'Gewünschte Lizenzen für den Kunden auswählen und die Bestellung abschließen.'
|
||||
},
|
||||
{
|
||||
number: '3',
|
||||
title: 'Live gehen',
|
||||
description: 'Nach dem Erhalt der Lizenz direkt live gehen und durchstarten.'
|
||||
}
|
||||
]
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
delayChildren: 0.2,
|
||||
staggerChildren: 0.35
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stepVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 20
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: 'spring' as const,
|
||||
stiffness: 70,
|
||||
damping: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ProcessSteps() {
|
||||
return (
|
||||
<section className="w-full py-16 px-4 relative z-10">
|
||||
<div className="container max-w-5xl mx-auto flex flex-col items-center">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ type: 'spring' as const, stiffness: 60, damping: 15 }}
|
||||
className="text-center space-y-3 mb-16 max-w-2xl"
|
||||
>
|
||||
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
Wie funktioniert die Bereitstellung?
|
||||
</h2>
|
||||
<p className="text-slate-400 text-base sm:text-lg leading-relaxed">
|
||||
In nur drei einfachen Schritten richten Sie CASPOS Lizenzen für Ihre Kunden ein.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Horizontale Timeline */}
|
||||
<div className="relative w-full">
|
||||
{/* Basis-Linie exakt zwischen Schritt 1 und 3 */}
|
||||
<div className="hidden md:block absolute top-6 left-[16.6%] right-[16.6%] h-[2px] bg-white/10 z-0 rounded-full" />
|
||||
|
||||
{/* Animierte, aufgeladene Linie (sky-500) */}
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
whileInView={{ scaleX: 1 }}
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
ease: 'easeInOut',
|
||||
delay: 0.2
|
||||
}}
|
||||
className="hidden md:block absolute top-6 left-[16.6%] right-[16.6%] h-[2px] bg-sky-500 origin-left z-0 rounded-full shadow-[0_0_12px_rgba(14,165,233,0.8)]"
|
||||
/>
|
||||
|
||||
{/* 3 Schritte Nebeneinander */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: '-50px' }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-6 w-full relative z-10"
|
||||
>
|
||||
{steps.map((step, index) => {
|
||||
const isLiveStep = index === 2
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={step.number}
|
||||
variants={stepVariants}
|
||||
className="group flex flex-col items-center text-center space-y-4 cursor-pointer relative"
|
||||
>
|
||||
{/* Nummer im runden Kreis auf der Linie */}
|
||||
<div className="relative">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.15 }}
|
||||
transition={{ type: 'spring' as const, stiffness: 300, damping: 20 }}
|
||||
className="w-12 h-12 rounded-full bg-slate-950 border-2 border-slate-700 text-slate-300 font-extrabold text-lg flex items-center justify-center group-hover:border-sky-400 group-hover:text-sky-400 group-hover:bg-slate-900 group-hover:shadow-[0_0_20px_rgba(56,189,248,0.4)] transition-all duration-300 z-10 relative"
|
||||
>
|
||||
{step.number}
|
||||
</motion.div>
|
||||
|
||||
{/* Pulsierender Ziel-Ring bei Schritt 3 */}
|
||||
{isLiveStep && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 1.6 }}
|
||||
className="absolute inset-0 rounded-full border-2 border-emerald-400/60 pointer-events-none animate-ping"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Text-Inhalt zentriert ohne Boxen-Hintergrund */}
|
||||
<div className="space-y-2 max-w-[280px]">
|
||||
<h3 className="text-xl font-bold text-white group-hover:text-sky-400 transition-colors duration-300 flex items-center justify-center gap-2">
|
||||
{step.title}
|
||||
{isLiveStep && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
||||
LIVE
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm leading-relaxed">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
20
shop/components/ScrollIndicator.tsx
Normal file
20
shop/components/ScrollIndicator.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { motion, useScroll, useSpring } from 'framer-motion'
|
||||
|
||||
export function ScrollIndicator() {
|
||||
const { scrollYProgress } = useScroll()
|
||||
const scaleX = useSpring(scrollYProgress, {
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
restDelta: 0.001
|
||||
})
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="fixed top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-blue-600 via-cyan-400 to-emerald-400 origin-left z-50 pointer-events-none shadow-[0_0_12px_rgba(59,130,246,0.8)]"
|
||||
style={{ scaleX }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
132
shop/components/WorkspaceZentrale.tsx
Normal file
132
shop/components/WorkspaceZentrale.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { MessageSquare, BookOpen, LifeBuoy, ExternalLink } from 'lucide-react'
|
||||
|
||||
interface WorkspaceItem {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
hoverText: string
|
||||
icon: React.ElementType
|
||||
link: string
|
||||
}
|
||||
|
||||
const workspaceItems: WorkspaceItem[] = [
|
||||
{
|
||||
id: 'zulip',
|
||||
title: 'Zulip Chat',
|
||||
subtitle: 'Treten Sie unserem Partner-Chat bei!',
|
||||
hoverText: 'Tauschen Sie sich direkt und unkompliziert mit anderen CASPOS Partnern aus.',
|
||||
icon: MessageSquare,
|
||||
link: 'https://caspos.zulipchat.com'
|
||||
},
|
||||
{
|
||||
id: 'wiki',
|
||||
title: 'CASPOS Wiki',
|
||||
subtitle: 'Ihr Wissens-Portal...',
|
||||
hoverText: '...für die gesamte CASPOS Produktfamilie. Hier finden Sie alle technischen Dokumentationen.',
|
||||
icon: BookOpen,
|
||||
link: 'https://wiki.caspos.de'
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
title: 'Support Portal',
|
||||
subtitle: 'Sie haben ein technisches Problem?',
|
||||
hoverText: 'Erstellen Sie jetzt ein Ticket in der CASPOS, damit sich unsere Experten darum kümmern.',
|
||||
icon: LifeBuoy,
|
||||
link: 'https://support.caspos.de'
|
||||
}
|
||||
]
|
||||
|
||||
export function WorkspaceZentrale() {
|
||||
const [hoveredId, setHoveredId] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<section className="w-full py-12 px-4 relative z-10">
|
||||
<div className="container max-w-6xl mx-auto space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">
|
||||
Workspace Zentrale
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* 3-Spalten Grid mit fester Kartenhöhe */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{workspaceItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isHovered = hoveredId === item.id
|
||||
|
||||
return (
|
||||
<motion.a
|
||||
key={item.id}
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
whileHover={{ y: -4 }}
|
||||
onMouseEnter={() => setHoveredId(item.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
className={`group relative p-6 rounded-2xl border transition-all duration-300 backdrop-blur-md flex flex-col justify-between h-[210px] overflow-hidden cursor-pointer ${
|
||||
isHovered
|
||||
? 'border-sky-500 bg-slate-900/90 shadow-[0_0_25px_rgba(14,165,233,0.2)]'
|
||||
: 'border-slate-800 bg-slate-900/60'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Header: Icon & Link */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className={`p-3 rounded-xl border transition-all duration-300 ${
|
||||
isHovered
|
||||
? 'bg-sky-500/20 border-sky-500/50 text-sky-300'
|
||||
: 'bg-slate-800/60 border-slate-700/50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
<ExternalLink
|
||||
className={`w-4 h-4 transition-all duration-300 ${
|
||||
isHovered
|
||||
? 'text-sky-400 translate-x-0.5 -translate-y-0.5'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-bold text-white group-hover:text-sky-300 transition-colors">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
{/* Subtitle / Hover-Text Überblendung ohne Höhenänderung */}
|
||||
<div className="relative h-[48px] overflow-hidden">
|
||||
<p
|
||||
className={`text-sm text-slate-300 font-medium transition-all duration-300 absolute inset-0 ${
|
||||
isHovered ? 'opacity-0 translate-y-[-8px]' : 'opacity-100 translate-y-0'
|
||||
}`}
|
||||
>
|
||||
{item.subtitle}
|
||||
</p>
|
||||
<p
|
||||
className={`text-xs text-slate-400 leading-relaxed transition-all duration-300 absolute inset-0 ${
|
||||
isHovered ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-[8px]'
|
||||
}`}
|
||||
>
|
||||
{item.hoverText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtile Hover-Linie unten */}
|
||||
<div className={`w-full h-0.5 rounded-full transition-all duration-300 ${
|
||||
isHovered ? 'bg-sky-500/80 shadow-[0_0_8px_rgba(14,165,233,0.8)]' : 'bg-transparent'
|
||||
}`} />
|
||||
</motion.a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user