diff --git a/ui/public/android.svg b/ui/public/android.svg new file mode 100644 index 0000000..080c6a5 --- /dev/null +++ b/ui/public/android.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ui/public/apple.svg b/ui/public/apple.svg new file mode 100644 index 0000000..3f676bb --- /dev/null +++ b/ui/public/apple.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/public/assets/classic-1.webp b/ui/public/assets/classic-1.webp new file mode 100644 index 0000000..b7a3d97 Binary files /dev/null and b/ui/public/assets/classic-1.webp differ diff --git a/ui/public/assets/classic-2.webp b/ui/public/assets/classic-2.webp new file mode 100644 index 0000000..33b4a97 Binary files /dev/null and b/ui/public/assets/classic-2.webp differ diff --git a/ui/public/assets/classic-3.webp b/ui/public/assets/classic-3.webp new file mode 100644 index 0000000..1c6e4c8 Binary files /dev/null and b/ui/public/assets/classic-3.webp differ diff --git a/ui/public/assets/classic-4.webp b/ui/public/assets/classic-4.webp new file mode 100644 index 0000000..185a27f Binary files /dev/null and b/ui/public/assets/classic-4.webp differ diff --git a/ui/public/assets/classic-5.webp b/ui/public/assets/classic-5.webp new file mode 100644 index 0000000..9a58a3a Binary files /dev/null and b/ui/public/assets/classic-5.webp differ diff --git a/ui/public/assets/element-download-1.webp b/ui/public/assets/element-download-1.webp new file mode 100644 index 0000000..95bc40a Binary files /dev/null and b/ui/public/assets/element-download-1.webp differ diff --git a/ui/public/assets/element-download-2.webp b/ui/public/assets/element-download-2.webp new file mode 100644 index 0000000..904cbc2 Binary files /dev/null and b/ui/public/assets/element-download-2.webp differ diff --git a/ui/public/assets/element-download-3.webp b/ui/public/assets/element-download-3.webp new file mode 100644 index 0000000..0d74162 Binary files /dev/null and b/ui/public/assets/element-download-3.webp differ diff --git a/ui/public/assets/element-download-4.webp b/ui/public/assets/element-download-4.webp new file mode 100644 index 0000000..fcc094d Binary files /dev/null and b/ui/public/assets/element-download-4.webp differ diff --git a/ui/public/assets/element-download-5.webp b/ui/public/assets/element-download-5.webp new file mode 100644 index 0000000..929f21c Binary files /dev/null and b/ui/public/assets/element-download-5.webp differ diff --git a/ui/public/assets/matrix-credentials.webp b/ui/public/assets/matrix-credentials.webp new file mode 100644 index 0000000..244a241 Binary files /dev/null and b/ui/public/assets/matrix-credentials.webp differ diff --git a/ui/public/assets/matrix-intro.webp b/ui/public/assets/matrix-intro.webp new file mode 100644 index 0000000..1110474 Binary files /dev/null and b/ui/public/assets/matrix-intro.webp differ diff --git a/ui/public/linux.svg b/ui/public/linux.svg new file mode 100644 index 0000000..b61e8fb --- /dev/null +++ b/ui/public/linux.svg @@ -0,0 +1 @@ + diff --git a/ui/public/windows.svg b/ui/public/windows.svg new file mode 100644 index 0000000..4d92379 --- /dev/null +++ b/ui/public/windows.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/ElementDownload.tsx b/ui/src/components/ElementDownload.tsx new file mode 100644 index 0000000..82a083f --- /dev/null +++ b/ui/src/components/ElementDownload.tsx @@ -0,0 +1,21 @@ +import { PRODUCT_SHORTNAME } from "@/App"; +import { useTranslation } from "react-i18next"; +import { Button } from "./ui/button"; + +export function ElementDownload() { + const { t } = useTranslation(PRODUCT_SHORTNAME); + + return ( +
+ +
+ ); +} diff --git a/ui/src/components/OnboardingGuide.tsx b/ui/src/components/OnboardingGuide.tsx index 61b0a14..40cb1e1 100644 --- a/ui/src/components/OnboardingGuide.tsx +++ b/ui/src/components/OnboardingGuide.tsx @@ -1,16 +1,24 @@ -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { Drawer, DrawerContent } from "@/components/ui/drawer"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; -import { PRODUCT_SHORTNAME } from "@/App"; import { Button } from "@/components/ui/button"; -import { ChevronRight, ChevronLeft, ImageOff, Info } from "lucide-react"; +import { ChevronRight, ChevronLeft, Info } from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import useHealthCheck from "@/hooks/helpers/useHealthcheck"; import { cn } from "@/lib/utils"; -import { useLocation } from "@tanstack/react-router"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { detectPlatform, Platform } from "@/lib/detectPlatform"; import { useMeta } from "@/lib/metadata"; +import { PRODUCT_SHORTNAME } from "@/App"; +import { ElementDownload } from "./ElementDownload"; const hashString = (str: string): string => { let hash = 0; @@ -28,362 +36,409 @@ interface OnboardingStep { description: string; image: string; mobileImage?: string; + customComponent?: React.ComponentType; } -function HOME_PAGE_ONBOARDING_STEPS(theme: string): OnboardingStep[] { - return [ - { - id: "welcome", - title: "onboarding.steps.home.welcome.title", - description: "onboarding.steps.home.welcome.description", - image: ``, - mobileImage: ``, - }, - { - id: "test", - title: "onboarding.steps.home.test.title", - description: "onboarding.steps.home.test.description", - image: ``, - mobileImage: `/`, - }, - ]; +interface OnboardingGroup { + platforms: Platform[]; + steps: OnboardingStep[]; } -const getOnboardingImage = ( - step: OnboardingStep, - isMobile: boolean, - forceDesktop: boolean = false, -): string => { - const imagePath = forceDesktop - ? step.image - : isMobile && step.mobileImage - ? step.mobileImage - : step.image; - - return imagePath; +const ONBOARDING_GROUPS: OnboardingGroup[] = [ + { + platforms: [Platform.Windows, Platform.Linux], + steps: [ + { + id: "desktop-intro", + title: "onboarding.steps.desktop-intro.title", + description: "onboarding.steps.desktop-intro.description", + image: "/ui/matrix/assets/matrix-intro.webp", + mobileImage: "/ui/matrix/assets/matrix-intro.webp", + customComponent: ElementDownload, + }, + { + id: "desktop-setup", + title: "onboarding.steps.setup.title", + description: "onboarding.steps.setup.description", + image: "/ui/matrix/assets/matrix-credentials.webp", + mobileImage: "/ui/matrix/assets/matrix-credentials.webp", + }, + { + id: "desktop-step-1", + title: "onboarding.steps.desktop-step-1.title", + description: "onboarding.steps.desktop-step-1.description", + image: "/ui/matrix/assets/element-download-1.webp", + mobileImage: "/ui/matrix/assets/element-download-1.webp", + }, + { + id: "desktop-step-2", + title: "onboarding.steps.desktop-step-2.title", + description: "onboarding.steps.desktop-step-2.description", + image: "/ui/matrix/assets/element-download-2.webp", + mobileImage: "/ui/matrix/assets/element-download-2.webp", + }, + { + id: "desktop-step-3", + title: "onboarding.steps.desktop-step-3.title", + description: "onboarding.steps.desktop-step-3.description", + image: "/ui/matrix/assets/element-download-3.webp", + mobileImage: "/ui/matrix/assets/element-download-3.webp", + }, + { + id: "desktop-step-4", + title: "onboarding.steps.desktop-step-4.title", + description: "onboarding.steps.desktop-step-4.description", + image: "/ui/matrix/assets/element-download-4.webp", + mobileImage: "/ui/matrix/assets/element-download-4.webp", + }, + { + id: "desktop-step-5", + title: "onboarding.steps.desktop-step-5.title", + description: "onboarding.steps.desktop-step-5.description", + image: "/ui/matrix/assets/element-download-5.webp", + mobileImage: "/ui/matrix/assets/element-download-5.webp", + }, + ], + }, + { + platforms: [Platform.Android, Platform.iOS], + steps: [ + { + id: "mobile-intro", + title: "onboarding.steps.mobile-intro.title", + description: "onboarding.steps.mobile-intro.description", + image: "/ui/matrix/assets/matrix-intro.webp", + mobileImage: "/ui/matrix/assets/matrix-intro.webp", + customComponent: ElementDownload, + }, + { + id: "mobile-setup", + title: "onboarding.steps.setup.title", + description: "onboarding.steps.setup.description", + image: "/ui/matrix/assets/matrix-credentials.webp", + mobileImage: "/ui/matrix/assets/matrix-credentials.webp", + }, + { + id: "mobile-step-1", + title: "onboarding.steps.mobile-step-1.title", + description: "onboarding.steps.mobile-step-1.description", + image: "/ui/matrix/assets/classic-1.webp", + mobileImage: "/ui/matrix/assets/classic-1.webp", + }, + { + id: "mobile-step-2", + title: "onboarding.steps.mobile-step-2.title", + description: "onboarding.steps.mobile-step-2.description", + image: "/ui/matrix/assets/classic-2.webp", + mobileImage: "/ui/matrix/assets/classic-2.webp", + }, + { + id: "mobile-step-3", + title: "onboarding.steps.mobile-step-3.title", + description: "onboarding.steps.mobile-step-3.description", + image: "/ui/matrix/assets/classic-3.webp", + mobileImage: "/ui/matrix/assets/classic-3.webp", + }, + { + id: "mobile-step-4", + title: "onboarding.steps.mobile-step-4.title", + description: "onboarding.steps.mobile-step-4.description", + image: "/ui/matrix/assets/classic-4.webp", + mobileImage: "/ui/matrix/assets/classic-4.webp", + }, + { + id: "mobile-step-5", + title: "onboarding.steps.mobile-step-5.title", + description: "onboarding.steps.mobile-step-5.description", + image: "/ui/matrix/assets/classic-5.webp", + mobileImage: "/ui/matrix/assets/classic-5.webp", + }, + ], + }, +]; + +const getStepsForPlatform = (platform: Platform): OnboardingStep[] => { + const group = ONBOARDING_GROUPS.find((g) => g.platforms.includes(platform)); + return group?.steps || []; }; -// Image cache to store preloaded images -const imageCache = new Map(); - -const preloadImage = (src: string): Promise => { - if (imageCache.has(src)) { - return Promise.resolve(); - } +export function OnboardingHandler() { + const { t } = useTranslation(PRODUCT_SHORTNAME); + const { deployment } = useHealthCheck(); + const isMobile = useIsMobile(); + const metadata = useMeta(); - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - imageCache.set(src, { loaded: true, error: false }); - resolve(); - }; - img.onerror = () => { - imageCache.set(src, { loaded: true, error: true }); - resolve(); - }; - img.src = src; - }); -}; + const defaultPlatform = detectPlatform(); + const [platform, setPlatform] = useState(defaultPlatform); -export function OnboardingGuide() { const [open, setOpen] = useState(false); const [currentStep, setCurrentStep] = useState(0); const [completed, setCompleted] = useState([]); - const [showCompletion, setShowCompletion] = useState(false); - const [canReview, setCanReview] = useState(false); - const [reviewMode, setReviewMode] = useState(false); + const [initialized, setInitialized] = useState(false); + const [imageEnlarged, setImageEnlarged] = useState(false); const [imageError, setImageError] = useState(false); const [imageLoading, setImageLoading] = useState(true); - const preloadedForDeviceRef = useRef(null); - const isMobile = useIsMobile(); - const { t } = useTranslation(PRODUCT_SHORTNAME); - const { deployment } = useHealthCheck(); - const location = useLocation(); - const meta = useMeta(); - - let relevantSteps = HOME_PAGE_ONBOARDING_STEPS(meta.theme); - switch (location.pathname) { - case "/": - relevantSteps = HOME_PAGE_ONBOARDING_STEPS(meta.theme); - break; - } - - // Preload all relevant images on mount (device-appropriate images) - useEffect(() => { - // Skip if isMobile is not yet determined - if (isMobile === undefined || relevantSteps.length === 0) return; - - // Skip if we already preloaded for this device type - if (preloadedForDeviceRef.current === isMobile) return; - preloadedForDeviceRef.current = isMobile; + const relevantSteps = useMemo( + () => getStepsForPlatform(platform), + [platform], + ); - const preloadAllImages = async () => { - // Preload device-appropriate images for inline display - const inlineImages = relevantSteps.map((step) => - getOnboardingImage(step, isMobile, false), - ); - // Also preload desktop images for enlarged modal - const desktopImages = relevantSteps.map((step) => - getOnboardingImage(step, isMobile, true), - ); - await Promise.all([...inlineImages, ...desktopImages].map(preloadImage)); + const storageKeys = useMemo(() => { + console.log("Storage key change"); + if (!metadata.callsign || !deployment) return null; + const deploymentHash = hashString(deployment); + const base = `${deploymentHash}-${PRODUCT_SHORTNAME}-onboarding-${metadata.callsign}-${platform}`; + return { + finished: `${base}-finished`, + steps: `${base}-steps`, + session: `${base}-session`, + platform: `${deploymentHash}-${PRODUCT_SHORTNAME}-onboarding-${metadata.callsign}-platform`, }; + }, [deployment, metadata.callsign, platform]); - preloadAllImages(); - }, [relevantSteps, isMobile]); + useEffect(() => { + if (!storageKeys || initialized) return; + + const finished = localStorage.getItem(storageKeys.finished) === "true"; + const stepsRaw = localStorage.getItem(storageKeys.steps); + const sessionRaw = localStorage.getItem(storageKeys.session); + + let savedSteps: string[] = []; + if (stepsRaw) { + try { + savedSteps = JSON.parse(stepsRaw) as string[]; + setCompleted(savedSteps); + } catch (e) { + console.error(e); + } + } - // Check cache and set loading/error state when step changes - const checkImageCache = useCallback((url: string) => { - const cached = imageCache.get(url); - if (cached) { - setImageLoading(false); - setImageError(cached.error); + const firstIncomplete = relevantSteps.findIndex( + (s) => !savedSteps.includes(s.id), + ); + const targetStep = + firstIncomplete !== -1 + ? firstIncomplete + : Math.max(0, relevantSteps.length - 1); + + if (sessionRaw) { + try { + const { stepIndex } = JSON.parse(sessionRaw) as { stepIndex: number }; + const shouldRestoreStep = + stepIndex >= 0 && + stepIndex < relevantSteps.length && + !savedSteps.includes(relevantSteps[stepIndex].id); + setCurrentStep(shouldRestoreStep ? stepIndex : targetStep); + } catch (e) { + console.error(e); + setCurrentStep(targetStep); + } } else { - setImageLoading(true); - setImageError(false); + setCurrentStep(targetStep); } - }, []); - - useEffect(() => { - if (!meta.callsign || !deployment) return; - const deploymentHash = hashString(deployment); - const storageKey = `${deploymentHash}-mtx-onboarding-${meta.callsign}-${location.pathname}`; - const seenOnboarding = localStorage.getItem(storageKey); + if (!finished && firstIncomplete !== -1) { + setOpen(true); + } - const completedSteps = localStorage.getItem( - `${deploymentHash}-mtx-onboarding-steps-${meta.callsign}-${location.pathname}`, - ); + setInitialized(true); + }, [storageKeys, initialized, relevantSteps]); - if (!seenOnboarding) { - setOpen(true); + useEffect(() => { + if (storageKeys && initialized) { + localStorage.setItem( + storageKeys.session, + JSON.stringify({ + stepIndex: currentStep, + }), + ); } + }, [currentStep, storageKeys, initialized]); + + useEffect(() => { + if (!initialized || !storageKeys) return; + + setImageLoading(true); + setImageError(false); - if (completedSteps) { - const parsedCompleted = JSON.parse(completedSteps) as string[]; - setCompleted(parsedCompleted); + const stepsRaw = localStorage.getItem(storageKeys.steps); + if (stepsRaw) { + try { + const savedSteps = JSON.parse(stepsRaw) as string[]; + setCompleted(savedSteps); - // Restore progress: find the first incomplete step - if (parsedCompleted.length > 0 && !seenOnboarding) { - const firstIncompleteIndex = relevantSteps.findIndex( - (step) => !parsedCompleted.includes(step.id), + const firstIncomplete = relevantSteps.findIndex( + (s) => !savedSteps.includes(s.id), ); - if (firstIncompleteIndex !== -1) { - setCurrentStep(firstIncompleteIndex); + if (firstIncomplete !== -1) { + setCurrentStep(firstIncomplete); } else { - // All steps completed, go to last step - setCurrentStep(relevantSteps.length - 1); + setCurrentStep(0); } - setCanReview(true); + } catch (e) { + console.error(e); + setCurrentStep(0); + setCompleted([]); } + } else { + setCurrentStep(0); + setCompleted([]); } - }, [meta.callsign, deployment]); + }, [platform, initialized, storageKeys, relevantSteps]); - useEffect(() => { - if ( - relevantSteps.length > 0 && - isMobile !== undefined && - relevantSteps[currentStep] - ) { - const url = getOnboardingImage( - relevantSteps[currentStep], - isMobile, - false, - ); - if (url) { - checkImageCache(url); - } - } - }, [currentStep, relevantSteps, checkImageCache, isMobile]); + const handleOpenChange = useCallback((newOpen: boolean) => { + setOpen(newOpen); + }, []); const handleNext = () => { if (currentStep < relevantSteps.length - 1) { - setCurrentStep(currentStep + 1); - setImageError(false); setImageLoading(true); - } - }; - - const handlePrev = () => { - if (currentStep > 0) { - setCurrentStep(currentStep - 1); setImageError(false); - setImageLoading(true); - } - }; - - const handleReviewClick = () => { - setReviewMode(true); - setCurrentStep(0); - setOpen(true); - }; - - const handleOpenChange = (newOpen: boolean) => { - setOpen(newOpen); - if (!newOpen && reviewMode) { - setReviewMode(false); - } else if (!newOpen && !reviewMode && !showCompletion) { - if (deployment) { - const deploymentHash = hashString(deployment); - const newCompleted = [...completed]; - const step = relevantSteps[currentStep]; - if (!newCompleted.includes(step.id)) { - newCompleted.push(step.id); - } - localStorage.setItem( - `${deploymentHash}-mtx-onboarding-steps-${meta.callsign}-${location.pathname}`, - JSON.stringify(newCompleted), - ); - setCompleted(newCompleted); - } - setCanReview(true); - toast.success(t("onboarding.progressSaved") || "Progress saved", { - duration: 2000, - }); + setImageEnlarged(false); + setCurrentStep((prev) => prev + 1); } }; const handleComplete = () => { const step = relevantSteps[currentStep]; - if (!completed.includes(step.id)) { - const newCompleted = [...completed, step.id]; - setCompleted(newCompleted); - if (deployment) { - const deploymentHash = hashString(deployment); - localStorage.setItem( - `${deploymentHash}-mtx-onboarding-steps-${meta.callsign}-${location.pathname}`, - JSON.stringify(newCompleted), - ); + const nextCompleted = Array.from(new Set([...completed, step.id])); + setCompleted(nextCompleted); + + if (storageKeys) { + localStorage.setItem(storageKeys.steps, JSON.stringify(nextCompleted)); + if (currentStep === relevantSteps.length - 1) { + localStorage.setItem(storageKeys.finished, "true"); + setOpen(false); + toast.success(t("onboarding.completion")); + } else { + handleNext(); } } - - if (currentStep === relevantSteps.length - 1) { - if (deployment) { - const deploymentHash = hashString(deployment); - localStorage.setItem( - `${deploymentHash}-mtx-onboarding-${meta.callsign}-${location.pathname}`, - "true", - ); - } - setOpen(false); - setShowCompletion(false); - setCanReview(true); - setReviewMode(false); - toast.success(t("onboarding.completion"), { duration: 3000 }); - } else { - handleNext(); - } }; - if (relevantSteps.length === 0) return null; - if (isMobile === undefined) return null; + if (!initialized || !storageKeys || isMobile === undefined) return null; const step = relevantSteps[currentStep]; + if (!step) return null; + + const CustomComponent = step.customComponent; + const progress = ((currentStep + 1) / relevantSteps.length) * 100; - const imageUrl = getOnboardingImage(step, isMobile, false); - const enlargedImageUrl = getOnboardingImage(step, isMobile, false); - - if (!open) { - return ( - - ); - } + const imageUrl = isMobile && step.mobileImage ? step.mobileImage : step.image; const contentComponent = ( -
-
-
-
-
-

{t(step.title)}

-
-

- {t("onboarding.step")} {currentStep + 1} {t("onboarding.of")}{" "} - {relevantSteps.length} -

+
+
+
+

{t(step.title)}

+

+ {t("onboarding.step")} {currentStep + 1} / {relevantSteps.length} +

+
+
+
-
!imageError && !imageLoading && setImageEnlarged(true)} + > + {imageLoading && !imageError && ( +
+
+
+ )} + Onboarding - !imageError && !imageLoading && setImageEnlarged(true) - } - > - {imageLoading && !imageError && ( -
-
-
+ "w-full h-full object-contain transition-opacity duration-300", + imageLoading ? "opacity-0" : "opacity-100", )} - {imageError ? ( -
- - - {t("onboarding.imageMissing") || "Image not available"} - -
- ) : ( - <> - {t(step.title)} { - setImageLoading(false); - imageCache.set(imageUrl, { loaded: true, error: false }); - }} - onError={() => { - setImageError(true); - setImageLoading(false); - imageCache.set(imageUrl, { loaded: true, error: true }); - }} - /> - {!imageLoading && ( -
- - {t("onboarding.clickToEnlarge") || "Click to enlarge"} - -
- )} - - )} -
-

- {t(step.description)} -

+ onLoad={() => setImageLoading(false)} + onError={(e) => { + const el = e.currentTarget as HTMLImageElement; + const filename = el.src.split("/").pop() || ""; + const fallback = `/assets/${filename}`; + if (!el.src.includes("/assets/")) { + el.src = fallback; + } else { + setImageError(true); + } + setImageLoading(false); + }} + /> +
+ +
+ {t(step.description)} + {CustomComponent && ( +
+ +
+ )}
-
+
-
-
+
); - const enlargedImageModal = ( - - setImageEnlarged(false)} - > -
- {t(step.title)} + return ( + <> + {!open && ( + + )} + + + + {t(step.title)} { + const el = e.currentTarget as HTMLImageElement; + const filename = el.src.split("/").pop() || ""; + const fallback = `/assets/${filename}`; + if (!el.src.includes("/assets/")) { + el.src = fallback; + } + }} /> -
-
-
- ); + + - if (isMobile) { - return ( - <> - {enlargedImageModal} + {isMobile ? ( - -
- - {t("onboarding.title") || "Onboarding Guide"} - - {contentComponent} -
-
+ {contentComponent}
- - ); - } - - return ( - <> - {enlargedImageModal} - - -
- - {t("onboarding.title") || "Onboarding Guide"} - + ) : ( + + {contentComponent} -
-
-
+ + + )} ); } diff --git a/ui/src/components/ui/select.tsx b/ui/src/components/ui/select.tsx new file mode 100644 index 0000000..1d5c5e5 --- /dev/null +++ b/ui/src/components/ui/select.tsx @@ -0,0 +1,188 @@ +import * as React from "react"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { Select as SelectPrimitive } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +function Select({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default"; +}) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/ui/src/lib/detectPlatform.ts b/ui/src/lib/detectPlatform.ts new file mode 100644 index 0000000..a8236a1 --- /dev/null +++ b/ui/src/lib/detectPlatform.ts @@ -0,0 +1,23 @@ +export enum Platform { + Android = "android", + iOS = "ios", + Windows = "windows", + Linux = "linux", +} + +export const detectPlatform = (): Platform => { + if (typeof window === "undefined") return Platform.Android; + + const ua = + window.navigator.userAgent || + window.navigator.vendor || + (window as any).opera; + + if (/android/i.test(ua)) return Platform.Android; + if (/iPad|iPhone|iPod/.test(ua)) return Platform.iOS; + if (/Windows NT/.test(ua)) return Platform.Windows; + if (/Linux/.test(ua) && !/android/i.test(ua)) return Platform.Linux; + + //Return most likely platform + return Platform.Android; +}; diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index 9723791..cf66f99 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -21,17 +21,69 @@ "review": "Review onboarding", "step": "Step", "steps": { - "home": { - "test": { - "description": "You are about to leave the Matrix.... :(", - "title": "Second page for test" - }, - "welcome": { - "description": "There is no going back after this one...", - "title": "Enter The Matrix.." - } + "desktop-intro": { + "title": "Welcome to Matrix", + "description": "This onboarding uses the Element-client, but you can use almost any Matrix Client. The steps you need to take should be somewhat similar in other clients." + }, + "setup": { + "title": "Prerequisites", + "description": "Copy the homeserver address (1). You can return to the guide always from the bottom right corner.(2)" + }, + "desktop-step-1": { + "title": "Log into Element - 1", + "description": "Open Element and select \"Sign in\"." + }, + "desktop-step-2": { + "title": "Log into Element - 2", + "description": "Press \"Edit\" on the homeserver address." + }, + "desktop-step-3": { + "title": "Log into Element - 3", + "description": "Paste the server address you copied earlier. Press \"Continue\"." + }, + "desktop-step-4": { + "title": "Log into Element - 4", + "description": "Press \"Continue with Keycloak\". Continue in your browser, which should ask your for your mTLS-certificate. Authenticate with mTLS." + }, + "desktop-step-5": { + "title": "Element Homepage", + "description": "You are now logged in and able to use Matrix. You can see or create spaces on the left (1). You can see and create your direct messages near the middle (2)." + }, + "mobile-intro": { + "title": "Welcome to Matrix", + "description": "This onboarding uses the Element Classic-client, but you can use almost any Matrix Client. The steps you need to take should be somewhat similar in other clients." + }, + "mobile-step-1": { + "title": "Log into Element Classic - 1", + "description": "Open Element and select \"Sign in\"." + }, + "mobile-step-2": { + "title": "Log into Element Classic - 2", + "description": "Press \"Edit\" on the homeserver address." + }, + "mobile-step-3": { + "title": "Log into Element Classic - 3", + "description": "Paste the server address you copied earlier. Press \"Next\"." + }, + "mobile-step-4": { + "title": "Log into Element Classic - 4", + "description": "Press \"Continue with Keycloak\". Continue in your browser, which should ask your for your mTLS-certificate. Authenticate with mTLS." + }, + "mobile-step-5": { + "title": "Element Classic Homepage", + "description": "You are now logged in and able to use Matrix. You search users and rooms at the top right corner (1). You can start a chat from the bottom right corner (2)." } }, - "title": "Onboard to Matrix" + "title": "Onboard to Matrix", + "downloads": { + "open_website": "Open download website" + } + }, + "platform": { + "select_placeholder": "Select platform", + "android": "Android", + "ios": "iOS", + "linux": "Linux", + "windows": "Windows" } } diff --git a/ui/src/locales/fi.json b/ui/src/locales/fi.json index f0349d5..4c65979 100644 --- a/ui/src/locales/fi.json +++ b/ui/src/locales/fi.json @@ -11,27 +11,79 @@ "onboarding": { "back": "Takaisin", "clickToEnlarge": "Klikkaa suurentaaksesi", - "completion": "Perehdyttäminen valmis!", - "completionDesc": "Olet valmis ja voit nyt työskennellä täysillä teholla.", - "finish": "Valmis", - "imageMissing": "Kuva ei ole saatavilla", + "completion": "Perehdytys valmis!", + "completionDesc": "Kaikki on valmista ja olet valmis aloittamaan.", + "finish": "Lopeta", + "imageMissing": "Kuvaa ei ole saatavilla", "next": "Seuraava", "of": "/", - "progressSaved": "Perehdyttämisen edistyminen on tallennettu.", - "review": "Tarkista perehdyttäminen", + "progressSaved": "Perehdytyksen edistyminen on tallennettu.", + "review": "Tarkastele perehdytystä", "step": "Vaihe", "steps": { - "home": { - "test": { - "description": "You are about to leave the Matrix.... :(", - "title": "Second page for test" - }, - "welcome": { - "description": "There is no going back after this one...", - "title": "Enter The Matrix.." - } + "desktop-intro": { + "title": "Tervetuloa Matrixiin", + "description": "Tämä opas käyttää Element-sovellusta, mutta voit käyttää mitä tahansa Matrix-sovellusta. Vaiheet ovat samankaltaisia muissakin sovelluksissa." + }, + "setup": { + "title": "Esivaatimukset", + "description": "Kopioi kotipalvelimen osoite (1). Voit palata oppaaseen milloin vain oikeasta alakulmasta (2)." + }, + "desktop-step-1": { + "title": "Kirjaudu Elementiin - 1", + "description": "Avaa Element ja valitse \"Log in\"." + }, + "desktop-step-2": { + "title": "Kirjaudu Elementiin - 2", + "description": "Paina \"Edit\" kotipalvelimen osoitteen kohdalta." + }, + "desktop-step-3": { + "title": "Kirjaudu Elementiin - 3", + "description": "Liitä aiemmin kopioimasi palvelimen osoite. Paina \"Continue\"." + }, + "desktop-step-4": { + "title": "Kirjaudu Elementiin - 4", + "description": "Paina \"Continue with Keycloak\". Jatka selaimessasi, jonka pitäisi pyytää mTLS-varmennettasi muutaman kerran. Tunnistaudu mTLS-varmenteella." + }, + "desktop-step-5": { + "title": "Elementin kotinäkymä", + "description": "Olet nyt kirjautunut sisään ja voit käyttää Matrixia. Voit tarkastella tai luoda tiloja (Spaces) sivupalkista (1). Näet ja voit luoda suoraviestejä vasemmasta reunasta (2)." + }, + "mobile-intro": { + "title": "Tervetuloa Matrixiin", + "description": "Tämä opas käyttää Element Classic -sovellusta, mutta voit käyttää lähes mitä tahansa Matrix-sovellusta. Vaiheet ovat samankaltaisia muissakin sovelluksissa." + }, + "mobile-step-1": { + "title": "Kirjaudu Element Classiciin - 1", + "description": "Avaa Element ja valitse \"Sign in\"." + }, + "mobile-step-2": { + "title": "Kirjaudu Element Classiciin - 2", + "description": "Paina \"Edit\" kotipalvelimen osoitteen kohdalta." + }, + "mobile-step-3": { + "title": "Kirjaudu Element Classiciin - 3", + "description": "Liitä aiemmin kopioimasi palvelimen osoite. Paina \"Next\"." + }, + "mobile-step-4": { + "title": "Kirjaudu Element Classiciin - 4", + "description": "Paina \"Continue with Keycloak\". Jatka selaimessasi, jonka pitäisi pyytää mTLS-varmennettasi. Tunnistaudu mTLS-varmenteella." + }, + "mobile-step-5": { + "title": "Element Classicin kotinäkymä", + "description": "Olet nyt kirjautunut sisään ja voit käyttää Matrixia. Voit etsiä käyttäjiä ja huoneita oikeasta yläkulmasta (1). Voit aloittaa keskustelun oikeasta alakulmasta (2)." } }, - "title": "Matrix -sovelluksen perehdyttäminen" + "title": "Matrix-perehdytys", + "downloads": { + "open_website": "Avaa lataussivusto" + } + }, + "platform": { + "select_placeholder": "Valitse alusta", + "android": "Android", + "ios": "iOS", + "linux": "Linux", + "windows": "Windows" } } diff --git a/ui/src/locales/sv.json b/ui/src/locales/sv.json index c3bfbdf..58e79d9 100644 --- a/ui/src/locales/sv.json +++ b/ui/src/locales/sv.json @@ -9,29 +9,81 @@ "copyButton": "Kopiera" }, "onboarding": { - "back": "Tillbaka", + "back": "Bakåt", "clickToEnlarge": "Klicka för att förstora", - "completion": "Introduktionen är slutförd!", - "completionDesc": "Du är redo och kan nu arbeta i full kapacitet.", + "completion": "Introduktionen är klar!", + "completionDesc": "Allt är klart och du är redo att köra igång.", "finish": "Slutför", - "imageMissing": "Bild inte tillgänglig", + "imageMissing": "Bild saknas", "next": "Nästa", "of": "av", - "progressSaved": "Dina introduktionsframsteg har sparats.", + "progressSaved": "Dina framsteg i introduktionen har sparats.", "review": "Granska introduktion", "step": "Steg", "steps": { - "home": { - "test": { - "description": "You are about to leave the Matrix.... :(", - "title": "Second page for test" - }, - "welcome": { - "description": "There is no going back after this one...", - "title": "Enter The Matrix.." - } + "desktop-intro": { + "title": "Välkommen till Matrix", + "description": "Denna introduktion använder Element-klienten, men du kan använda vilken Matrix-klient som helst. Stegen bör vara liknande i andra klienter." + }, + "setup": { + "title": "Förutsättningar", + "description": "Kopiera adressen till hemservern (1). Du kan alltid återvända till guiden via det nedre högra hörnet (2)." + }, + "desktop-step-1": { + "title": "Logga in i Element - 1", + "description": "Öppna Element och välj \"Logga in\"." + }, + "desktop-step-2": { + "title": "Logga in i Element - 2", + "description": "Tryck på \"Redigera\" vid hemserverns adress." + }, + "desktop-step-3": { + "title": "Logga in i Element - 3", + "description": "Klistra in serveradressen du kopierade tidigare. Tryck på \"Fortsätt\"." + }, + "desktop-step-4": { + "title": "Logga in i Element - 4", + "description": "Tryck på \"Fortsätt med Keycloak\". Fortsätt i din webbläsare, som bör fråga efter ditt mTLS-certifikat några gånger. Autentisera med mTLS." + }, + "desktop-step-5": { + "title": "Elements startsida", + "description": "Du är nu inloggad och kan använda Matrix. Du kan se eller skapa ytor (Spaces) till vänster (1). Du kan se och skapa direktmeddelanden i mitten (2)." + }, + "mobile-intro": { + "title": "Välkommen till Matrix", + "description": "Denna introduktion använder Element Classic-klienten, men du kan använda nästan vilken Matrix-klient som helst. Stegen bör vara liknande i andra klienter." + }, + "mobile-step-1": { + "title": "Logga in i Element Classic - 1", + "description": "Öppna Element och välj \"Sign in\"." + }, + "mobile-step-2": { + "title": "Logga in i Element Classic - 2", + "description": "Tryck på \"Edit\" vid hemserverns adress." + }, + "mobile-step-3": { + "title": "Logga in i Element Classic - 3", + "description": "Klistra in serveradressen du kopierade tidigare. Tryck på \"Next\"." + }, + "mobile-step-4": { + "title": "Logga in i Element Classic - 4", + "description": "Tryck på \"Continue with Keycloak\". Fortsätt i din webbläsare, som bör fråga efter ditt mTLS-certifikat. Autentisera med mTLS." + }, + "mobile-step-5": { + "title": "Element Classics startsida", + "description": "Du är nu inloggad och kan använda Matrix. Du kan söka efter användare och rum i det övre högra hörnet (1). Du kan starta en chatt från det nedre högra hörnet (2)." } }, - "title": "Introduktion till Matrix" + "title": "Introduktion till Matrix", + "downloads": { + "open_website": "Öppna webbplatsen för nedladdning" + } + }, + "platform": { + "select_placeholder": "Välj plattform", + "android": "Android", + "ios": "iOS", + "linux": "Linux", + "windows": "Windows" } } diff --git a/ui/src/pages/HomePage.tsx b/ui/src/pages/HomePage.tsx index 1429776..11ff893 100644 --- a/ui/src/pages/HomePage.tsx +++ b/ui/src/pages/HomePage.tsx @@ -1,12 +1,12 @@ import { useTranslation } from "react-i18next"; import { PRODUCT_SHORTNAME } from "@/App"; -import { OnboardingGuide } from "@/components/OnboardingGuide"; import { Toaster } from "@/components/ui/sonner"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Copy } from "lucide-react"; import { copyToClipboard } from "@/lib/clipboard"; +import { OnboardingHandler } from "@/components/OnboardingGuide"; export const HomePage = () => { const { t } = useTranslation(PRODUCT_SHORTNAME); @@ -44,7 +44,7 @@ export const HomePage = () => {
- +
);