2025-10-30 21:08:08 +01:00
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
import type React from "react"
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react"
|
|
|
|
|
import { Button } from "./ui/button"
|
2025-11-21 19:49:42 +01:00
|
|
|
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
|
2025-10-30 21:08:08 +01:00
|
|
|
import {
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
X,
|
|
|
|
|
Sparkles,
|
|
|
|
|
LayoutDashboard,
|
|
|
|
|
HardDrive,
|
|
|
|
|
Network,
|
|
|
|
|
Box,
|
|
|
|
|
Cpu,
|
|
|
|
|
FileText,
|
|
|
|
|
Rocket,
|
|
|
|
|
} from "lucide-react"
|
|
|
|
|
import Image from "next/image"
|
2025-11-04 18:07:13 +01:00
|
|
|
import { Checkbox } from "./ui/checkbox"
|
2026-08-04 17:01:19 +02:00
|
|
|
import { useT } from "../lib/i18n/provider"
|
2025-10-30 21:08:08 +01:00
|
|
|
|
|
|
|
|
interface OnboardingSlide {
|
|
|
|
|
id: number
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: string
|
|
|
|
|
descriptionKey: string
|
2025-10-30 21:08:08 +01:00
|
|
|
image?: string
|
|
|
|
|
icon: React.ReactNode
|
|
|
|
|
gradient: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const slides: OnboardingSlide[] = [
|
|
|
|
|
{
|
|
|
|
|
id: 0,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.welcome.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.welcome.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
icon: <Sparkles className="h-16 w-16" />,
|
|
|
|
|
gradient: "from-blue-500 via-purple-500 to-pink-500",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 1,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.overview.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.overview.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
image: "/images/onboarding/imagen1.png",
|
|
|
|
|
icon: <LayoutDashboard className="h-12 w-12" />,
|
|
|
|
|
gradient: "from-blue-500 to-cyan-500",
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-11 19:36:37 +01:00
|
|
|
id: 2,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.storage.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.storage.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
image: "/images/onboarding/imagen2.png",
|
|
|
|
|
icon: <HardDrive className="h-12 w-12" />,
|
|
|
|
|
gradient: "from-cyan-500 to-teal-500",
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-11 19:36:37 +01:00
|
|
|
id: 3,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.network.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.network.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
image: "/images/onboarding/imagen3.png",
|
|
|
|
|
icon: <Network className="h-12 w-12" />,
|
|
|
|
|
gradient: "from-teal-500 to-green-500",
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-11 19:36:37 +01:00
|
|
|
id: 4,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.virtualMachines.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.virtualMachines.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
image: "/images/onboarding/imagen4.png",
|
|
|
|
|
icon: <Box className="h-12 w-12" />,
|
|
|
|
|
gradient: "from-green-500 to-emerald-500",
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-11 19:36:37 +01:00
|
|
|
id: 5,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.hardware.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.hardware.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
image: "/images/onboarding/imagen5.png",
|
|
|
|
|
icon: <Cpu className="h-12 w-12" />,
|
|
|
|
|
gradient: "from-emerald-500 to-blue-500",
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-11 19:36:37 +01:00
|
|
|
id: 6,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.logs.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.logs.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
image: "/images/onboarding/imagen6.png",
|
|
|
|
|
icon: <FileText className="h-12 w-12" />,
|
|
|
|
|
gradient: "from-blue-500 to-indigo-500",
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-11 19:36:37 +01:00
|
|
|
id: 7,
|
2026-08-04 17:01:19 +02:00
|
|
|
titleKey: "onboarding.slides.future.title",
|
|
|
|
|
descriptionKey: "onboarding.slides.future.description",
|
2025-10-30 21:08:08 +01:00
|
|
|
icon: <Rocket className="h-16 w-16" />,
|
|
|
|
|
gradient: "from-indigo-500 via-purple-500 to-pink-500",
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
export function OnboardingCarousel() {
|
2026-08-04 17:01:19 +02:00
|
|
|
const t = useT()
|
2025-10-30 21:08:08 +01:00
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
const [currentSlide, setCurrentSlide] = useState(0)
|
|
|
|
|
const [direction, setDirection] = useState<"next" | "prev">("next")
|
2025-11-04 18:07:13 +01:00
|
|
|
const [dontShowAgain, setDontShowAgain] = useState(false)
|
2025-10-30 21:08:08 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
|
|
|
|
|
if (!hasSeenOnboarding) {
|
|
|
|
|
setOpen(true)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const handleNext = () => {
|
|
|
|
|
if (currentSlide < slides.length - 1) {
|
|
|
|
|
setDirection("next")
|
|
|
|
|
setCurrentSlide(currentSlide + 1)
|
|
|
|
|
} else {
|
2025-11-04 18:07:13 +01:00
|
|
|
if (dontShowAgain) {
|
|
|
|
|
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
|
|
|
|
}
|
2025-10-31 17:02:09 +01:00
|
|
|
setOpen(false)
|
2025-10-30 21:08:08 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handlePrev = () => {
|
|
|
|
|
if (currentSlide > 0) {
|
|
|
|
|
setDirection("prev")
|
|
|
|
|
setCurrentSlide(currentSlide - 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSkip = () => {
|
2025-11-04 18:07:13 +01:00
|
|
|
if (dontShowAgain) {
|
|
|
|
|
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
|
|
|
|
}
|
2025-10-30 21:08:08 +01:00
|
|
|
setOpen(false)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 18:45:54 +01:00
|
|
|
const handleClose = () => {
|
|
|
|
|
if (dontShowAgain) {
|
|
|
|
|
localStorage.setItem("proxmenux-onboarding-seen", "true")
|
|
|
|
|
}
|
|
|
|
|
setOpen(false)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 21:08:08 +01:00
|
|
|
const handleDotClick = (index: number) => {
|
|
|
|
|
setDirection(index > currentSlide ? "next" : "prev")
|
|
|
|
|
setCurrentSlide(index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const slide = slides[currentSlide]
|
2026-08-04 17:01:19 +02:00
|
|
|
const slideTitle = t(slide.titleKey)
|
|
|
|
|
const slideDescription = t(slide.descriptionKey)
|
2025-10-30 21:08:08 +01:00
|
|
|
|
|
|
|
|
return (
|
2025-11-04 18:45:54 +01:00
|
|
|
<Dialog open={open} onOpenChange={handleClose}>
|
2025-10-30 21:08:08 +01:00
|
|
|
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden border-0 bg-transparent">
|
2026-08-04 17:01:19 +02:00
|
|
|
<DialogTitle className="sr-only">{t("onboarding.dialogTitle")}</DialogTitle>
|
2025-10-30 21:08:08 +01:00
|
|
|
<div className="relative bg-card rounded-lg overflow-hidden shadow-2xl">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
|
2025-11-04 18:45:54 +01:00
|
|
|
onClick={handleClose}
|
2025-10-30 21:08:08 +01:00
|
|
|
>
|
|
|
|
|
<X className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<div
|
2025-10-30 23:30:58 +01:00
|
|
|
className={`relative h-48 md:h-64 bg-gradient-to-br ${slide.gradient} flex items-center justify-center overflow-hidden`}
|
2025-10-30 21:08:08 +01:00
|
|
|
>
|
|
|
|
|
<div className="absolute inset-0 bg-black/10" />
|
|
|
|
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
|
|
|
|
|
|
|
|
|
|
<div className="relative z-10 text-white">
|
|
|
|
|
{slide.image ? (
|
2025-10-30 23:30:58 +01:00
|
|
|
<div className="relative w-full h-36 md:h-48 flex items-center justify-center px-4">
|
2025-10-30 21:08:08 +01:00
|
|
|
<Image
|
|
|
|
|
src={slide.image || "/placeholder.svg"}
|
2026-08-04 17:01:19 +02:00
|
|
|
alt={slideTitle}
|
2025-10-30 21:08:08 +01:00
|
|
|
width={600}
|
|
|
|
|
height={400}
|
2025-10-30 23:30:58 +01:00
|
|
|
className="rounded-lg shadow-2xl object-cover max-h-36 md:max-h-48"
|
2025-10-30 21:08:08 +01:00
|
|
|
onError={(e) => {
|
|
|
|
|
const target = e.target as HTMLImageElement
|
|
|
|
|
target.style.display = "none"
|
|
|
|
|
const fallback = target.parentElement?.querySelector(".fallback-icon")
|
|
|
|
|
if (fallback) {
|
|
|
|
|
fallback.classList.remove("hidden")
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<div className="fallback-icon hidden">{slide.icon}</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="animate-pulse">{slide.icon}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
|
|
|
|
|
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-04 18:45:54 +01:00
|
|
|
<div className="p-4 md:p-8 space-y-3 md:space-y-6 max-h-[60vh] md:max-h-none overflow-y-auto">
|
2025-10-30 23:30:58 +01:00
|
|
|
<div className="space-y-2 md:space-y-3">
|
2026-08-04 17:01:19 +02:00
|
|
|
<h2 className="text-xl md:text-3xl font-bold text-foreground text-balance">{slideTitle}</h2>
|
2025-11-04 18:45:54 +01:00
|
|
|
<p className="text-sm md:text-lg text-muted-foreground leading-relaxed text-pretty">
|
2026-08-04 17:01:19 +02:00
|
|
|
{slideDescription}
|
2025-10-30 23:30:58 +01:00
|
|
|
</p>
|
2025-10-30 21:08:08 +01:00
|
|
|
</div>
|
|
|
|
|
|
2025-10-30 23:30:58 +01:00
|
|
|
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
|
2025-10-30 21:08:08 +01:00
|
|
|
{slides.map((_, index) => (
|
|
|
|
|
<button
|
|
|
|
|
key={index}
|
|
|
|
|
onClick={() => handleDotClick(index)}
|
|
|
|
|
className={`transition-all duration-300 rounded-full ${
|
|
|
|
|
index === currentSlide
|
2025-10-30 23:30:58 +01:00
|
|
|
? "w-8 h-2.5 bg-blue-500 shadow-lg shadow-blue-500/50"
|
|
|
|
|
: "w-2.5 h-2.5 bg-muted-foreground/60 hover:bg-muted-foreground/80 border border-muted-foreground/40"
|
2025-10-30 21:08:08 +01:00
|
|
|
}`}
|
2026-08-04 17:01:19 +02:00
|
|
|
aria-label={t("onboarding.goToSlide", { number: index + 1 })}
|
2025-10-30 21:08:08 +01:00
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-04 18:45:54 +01:00
|
|
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 md:gap-4">
|
2025-10-30 23:30:58 +01:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={handlePrev}
|
|
|
|
|
disabled={currentSlide === 0}
|
2025-11-04 18:45:54 +01:00
|
|
|
className="gap-2 w-full sm:w-auto text-sm"
|
2025-10-30 23:30:58 +01:00
|
|
|
>
|
2025-10-30 21:08:08 +01:00
|
|
|
<ChevronLeft className="h-4 w-4" />
|
2026-08-04 17:01:19 +02:00
|
|
|
{t("onboarding.previous")}
|
2025-10-30 21:08:08 +01:00
|
|
|
</Button>
|
|
|
|
|
|
2025-10-30 23:30:58 +01:00
|
|
|
<div className="flex gap-2 w-full sm:w-auto">
|
2025-10-30 21:08:08 +01:00
|
|
|
{currentSlide < slides.length - 1 ? (
|
|
|
|
|
<>
|
2025-11-04 18:45:54 +01:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={handleSkip}
|
|
|
|
|
className="flex-1 sm:flex-none bg-transparent text-sm"
|
|
|
|
|
>
|
2026-08-04 17:01:19 +02:00
|
|
|
{t("onboarding.skip")}
|
2025-10-30 21:08:08 +01:00
|
|
|
</Button>
|
2025-11-04 18:45:54 +01:00
|
|
|
<Button
|
|
|
|
|
onClick={handleNext}
|
|
|
|
|
className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none text-sm"
|
|
|
|
|
>
|
2026-08-04 17:01:19 +02:00
|
|
|
{t("onboarding.next")}
|
2025-10-30 21:08:08 +01:00
|
|
|
<ChevronRight className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
2025-10-31 17:02:09 +01:00
|
|
|
onClick={handleNext}
|
2025-11-04 18:45:54 +01:00
|
|
|
className="gap-2 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 w-full sm:w-auto text-sm"
|
2025-10-30 21:08:08 +01:00
|
|
|
>
|
2026-08-04 17:01:19 +02:00
|
|
|
{t("onboarding.getStarted")}
|
2025-10-30 21:08:08 +01:00
|
|
|
<Sparkles className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-04 18:45:54 +01:00
|
|
|
<div className="flex items-center justify-center gap-2 pt-2 pb-1">
|
2025-11-04 18:07:13 +01:00
|
|
|
<Checkbox
|
|
|
|
|
id="dont-show-again"
|
|
|
|
|
checked={dontShowAgain}
|
|
|
|
|
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
|
|
|
|
|
/>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="dont-show-again"
|
2025-11-04 18:45:54 +01:00
|
|
|
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
|
2025-11-04 18:07:13 +01:00
|
|
|
>
|
2026-08-04 17:01:19 +02:00
|
|
|
{t("onboarding.dontShowAgain")}
|
2025-11-04 18:07:13 +01:00
|
|
|
</label>
|
|
|
|
|
</div>
|
2025-10-30 21:08:08 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)
|
|
|
|
|
}
|