diff --git a/.env b/.env index 5ebaa652..f69e5bcc 100644 --- a/.env +++ b/.env @@ -9,7 +9,6 @@ PUBLIC_TURNSTILE_DEV_BYPASS_VALUE=INVALID PUBLIC_DEVELOPMENT_BANNER=false -PUBLIC_DISABLE_ONBOARDING=false PUBLIC_DISABLE_SHOCKER_MAP=true diff --git a/package.json b/package.json index 62e34f30..c1e4b055 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "@opentelemetry/sdk-logs": "^0.223.0", "@opentelemetry/sdk-trace-node": "^2.12.0", "@opentelemetry/sdk-trace-web": "^2.12.0", - "driver.js": "^1.10.1", "temporal-polyfill": "^1.0.5", "vite": "^8.3.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b63160a7..4a445d54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,9 +207,6 @@ importers: '@opentelemetry/sdk-trace-web': specifier: ^2.12.0 version: 2.12.0(@opentelemetry/api@1.9.1) - driver.js: - specifier: ^1.10.1 - version: 1.10.1 temporal-polyfill: specifier: ^1.0.5 version: 1.0.5 @@ -2309,9 +2306,6 @@ packages: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} - driver.js@1.10.1: - resolution: {integrity: sha512-mpcJ15eIRWnGMykeIWjr0CdQF00/JzImFOEXy/VponYgMbP/y46t24MPBDO3PV/rH6CjdSe3T7WQ9cP3GvEPdQ==} - electron-to-chromium@1.5.440: resolution: {integrity: sha512-SghDzqdJokdz8zP8YNlvS74+CwLRoUPDGvP/gFA+HrKVw+pOshUAgx8pXR37xl/u16zGae746rFNWvZ/22kU2Q==} @@ -5058,8 +5052,6 @@ snapshots: dotenv@17.4.2: {} - driver.js@1.10.1: {} - electron-to-chromium@1.5.440: {} enhanced-resolve@5.25.1: diff --git a/src/app.css b/src/app.css index 78be2016..5e77f2a5 100644 --- a/src/app.css +++ b/src/app.css @@ -1,54 +1,3 @@ @import 'tailwindcss'; @import 'tw-animate-css'; @import '@openshock/svelte-core/theme.css'; - -/* driver.js theme override — match the dark UI */ -.driver-popover { - background: #13131a; - color: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 0.75rem; -} -.driver-popover-title { - color: #fff; - font-weight: 600; -} -.driver-popover-description { - color: rgba(255, 255, 255, 0.7); -} -.driver-popover-arrow-side-left.driver-popover-arrow, -.driver-popover-arrow-side-right.driver-popover-arrow, -.driver-popover-arrow-side-top.driver-popover-arrow, -.driver-popover-arrow-side-bottom.driver-popover-arrow { - border-color: #13131a; -} -.driver-popover-progress-text { - color: rgba(255, 255, 255, 0.5); - font-size: 0.75rem; -} -.driver-popover-footer button { - background: rgba(255, 255, 255, 0.08) !important; - color: #fff !important; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 0.5rem; - padding: 0.375rem 0.875rem; - font-size: 0.875rem; - text-shadow: none !important; -} -.driver-popover-footer button:hover { - background: rgba(255, 255, 255, 0.14) !important; -} -.driver-popover-footer .driver-popover-next-btn { - background: #fff !important; - color: #000 !important; - border-color: transparent; -} -.driver-popover-footer .driver-popover-next-btn:hover { - background: rgba(255, 255, 255, 0.9) !important; -} -.driver-popover-close-btn { - color: rgba(255, 255, 255, 0.5); -} -.driver-popover-close-btn:hover { - color: #fff; -} diff --git a/src/env.ts b/src/env.ts index 3cc0a5d6..37667279 100644 --- a/src/env.ts +++ b/src/env.ts @@ -155,12 +155,6 @@ export const variables = defineEnvVars({ description: 'Shows the "development deployment" banner above the app shell.', schema: isTruthy, }, - PUBLIC_DISABLE_ONBOARDING: { - public: true, - static: true, - description: 'Suppresses the first-run onboarding tour.', - schema: isTruthy, - }, PUBLIC_DISABLE_SHOCKER_MAP: { public: true, static: true, diff --git a/src/lib/state/auth-state.svelte.ts b/src/lib/state/auth-state.svelte.ts index 612a1a97..87950d78 100644 --- a/src/lib/state/auth-state.svelte.ts +++ b/src/lib/state/auth-state.svelte.ts @@ -1,13 +1,5 @@ import { registerOnUnauthorized } from '#lib/errorhandling/apiErrorHandling.js'; import { destroySignalR, initializeSignalR } from '#lib/signalr/user.svelte.js'; -import { - hasCompletedTour, - hasSeenWelcome, - isOnboardingDisabled, - markTourCompleted, - startWelcomeTour, -} from '#lib/tour/welcome-tour.js'; -import { dialog } from '@openshock/svelte-core/components/dialog-manager'; import { userState } from './user-state.svelte'; export const AuthStatus = { @@ -33,18 +25,6 @@ export const authState = { }, }; -async function maybeTourPrompt() { - if (isOnboardingDisabled() || hasCompletedTour() || !hasSeenWelcome()) return; - const result = await dialog.confirm({ - title: 'Take the quick tour?', - desc: "You skipped it earlier. It only takes a minute and shows you what's new.", - confirmButtonText: 'Sure, show me', - cancelButtonText: 'No thanks', - }); - if (result.confirmed) await startWelcomeTour(); - else markTourCompleted(); -} - /** * Wires up the reactive side-effects of auth state: * - SignalR connects/disconnects as the user logs in/out. @@ -61,7 +41,6 @@ export function startAuthLifecycle() { $effect(() => { const self = userState.self; if (self && !prevSelf) { - void maybeTourPrompt(); void initializeSignalR(); } else if (!self) { void destroySignalR(); diff --git a/src/lib/tour/onboarding-state.ts b/src/lib/tour/onboarding-state.ts deleted file mode 100644 index 5c0bd744..00000000 --- a/src/lib/tour/onboarding-state.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { PUBLIC_DISABLE_ONBOARDING } from '$app/env/public'; -import { isTruthy } from '@openshock/svelte-core/utils'; - -export const WELCOME_COOKIE_NAME = 'os.welcomed'; -// Pre-cookie builds stored "welcome seen" under this localStorage key. Kept only -// for the migration fallback in hasSeenWelcome(). -const LEGACY_WELCOME_STORAGE_KEY = 'os.welcomeVersion'; -const WELCOME_COOKIE_EXPIRES = new Date('2026-07-10'); -// Retire the welcome screen one day before the cookie expires, so it can't briefly -// reappear for everyone in the window where the cookie lapses. -const WELCOME_SUNSET = new Date(WELCOME_COOKIE_EXPIRES.getTime() - 24 * 60 * 60 * 1000); - -export function isWelcomeSunset(): boolean { - return Date.now() >= WELCOME_SUNSET.getTime(); -} - -const CURRENT_TOUR_VERSION = 1; -const TOUR_VERSION_KEY = 'os.tourCompletedVersion'; - -export function isOnboardingDisabled(): boolean { - return PUBLIC_DISABLE_ONBOARDING; -} - -function readWelcomeCookie(): boolean { - try { - const escapedName = WELCOME_COOKIE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escapedName}=([^;]*)`)); - return match ? isTruthy(decodeURIComponent(match[1])) : false; - } catch { - return false; - } -} - -export function hasSeenWelcome(): boolean { - // Check cookie (new path) then fall back to localStorage (migration). isTruthy - // also accepts the legacy version value "1", so pre-existing state still counts. - if (readWelcomeCookie()) return true; - try { - return isTruthy(localStorage.getItem(LEGACY_WELCOME_STORAGE_KEY)); - } catch { - return false; - } -} - -export function markWelcomed(): void { - try { - // Synchronous document.cookie write. The async cookieStore API would also work - // (Chrome 87+, Firefox 140+, Safari 18.4+), but for a one-shot write it buys - // nothing and drops older browsers, so the plain string write is preferred. - document.cookie = `${WELCOME_COOKIE_NAME}=true; path=/; expires=${WELCOME_COOKIE_EXPIRES.toUTCString()}; SameSite=Lax`; - } catch { - // ignore - } -} - -export function shouldShowWelcome(): boolean { - if (isOnboardingDisabled() || isWelcomeSunset()) return false; - return !hasSeenWelcome(); -} - -export function hasCompletedTour(): boolean { - if (isOnboardingDisabled()) return true; - try { - const raw = localStorage.getItem(TOUR_VERSION_KEY); - const n = raw ? parseInt(raw, 10) : 0; - return Number.isFinite(n) && n >= CURRENT_TOUR_VERSION; - } catch { - return false; - } -} - -export function markTourCompleted(): void { - try { - localStorage.setItem(TOUR_VERSION_KEY, String(CURRENT_TOUR_VERSION)); - } catch { - // ignore - } -} diff --git a/src/lib/tour/welcome-tour.ts b/src/lib/tour/welcome-tour.ts deleted file mode 100644 index efaf9e88..00000000 --- a/src/lib/tour/welcome-tour.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { browser } from '$app/env'; -import { goto } from '$app/navigation'; -import { resolve as resolvePath } from '$app/paths'; -import { driver, type Driver, type DriveStep } from 'driver.js'; -import 'driver.js/dist/driver.css'; -import { isOnboardingDisabled, markTourCompleted } from './onboarding-state'; - -export { - hasCompletedTour, - hasSeenWelcome, - isOnboardingDisabled, - markTourCompleted, - markWelcomed, - shouldShowWelcome, -} from './onboarding-state'; - -const SIDEBAR_TOGGLE_SEL = 'button[title="Toggle Sidebar"]'; -const SIDEBAR_ROOT_SEL = '[data-slot="sidebar"]'; -const SIDEBAR_LINK = (href: string) => `[data-slot="sidebar-content"] a[href$="${href}"]`; -const USER_MENU_SEL = 'header button:has(> img.rounded-full)'; -const TOUR = (tag: string) => `[data-tour="${tag}"]`; - -type SidebarState = 'desktop-expanded' | 'desktop-collapsed' | 'mobile-open' | 'mobile-closed'; - -function isMobileViewport(): boolean { - return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches; -} - -function readSidebarState(): SidebarState { - if (isMobileViewport()) { - return document.querySelector('[data-slot="sidebar"][data-mobile="true"]') - ? 'mobile-open' - : 'mobile-closed'; - } - return document.querySelector('[data-slot="sidebar"][data-state="expanded"]') - ? 'desktop-expanded' - : 'desktop-collapsed'; -} - -function waitForElement(selector: string, timeoutMs = 5000): Promise { - const existing = document.querySelector(selector); - if (existing) return Promise.resolve(existing); - return new Promise((resolve) => { - const obs = new MutationObserver(() => { - const el = document.querySelector(selector); - if (el) { - obs.disconnect(); - clearTimeout(timer); - resolve(el); - } - }); - obs.observe(document.body, { childList: true, subtree: true }); - const timer = setTimeout(() => { - obs.disconnect(); - resolve(document.querySelector(selector)); - }, timeoutMs); - }); -} - -function waitForMobileSidebar(): Promise { - return new Promise((resolve) => setTimeout(resolve, 250)); -} - -interface TourStep { - // `action` advances on a real click of `element` (no Next button shown). - // `info` advances on the Next button. - kind: 'info' | 'action'; - // `sidebar` = element lives inside the sidebar drawer (must be open on mobile). - // `page` = element lives in the page content (sidebar should be closed on mobile). - // `any` = no sidebar management needed (default). - mobileContext?: 'sidebar' | 'page' | 'any'; - element?: string; - title: string; - description: string; - side?: DriveStep['popover'] extends infer P ? (P extends { side?: infer S } ? S : never) : never; - // If set, the step is skipped when the predicate returns true at tour-start. - skipIf?: () => boolean; -} - -function sidebarOverviewSteps(): TourStep[] { - return [ - { - kind: 'info', - mobileContext: 'sidebar', - element: SIDEBAR_LINK('/home'), - title: 'Home', - description: 'Your dashboard, with quick access to the shockers you control most often.', - side: 'right', - }, - { - kind: 'info', - mobileContext: 'sidebar', - element: SIDEBAR_LINK('/hubs'), - title: 'Hubs', - description: 'Your hubs live here. Pair new ones and check their status from this page.', - side: 'right', - }, - { - kind: 'info', - mobileContext: 'sidebar', - element: SIDEBAR_LINK('/shares/public'), - title: 'Public shares', - description: 'Generate links anyone can use, with limits and expiry you set.', - side: 'right', - }, - ]; -} - -function shockersDeepDive(): TourStep[] { - return [ - { - kind: 'action', - mobileContext: 'sidebar', - element: SIDEBAR_LINK('/shockers/own'), - title: 'Your shockers', - description: 'This is where your own shockers live. Click to open the page.', - side: 'right', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('shockers-add'), - title: 'Add a shocker', - description: - 'Pair a new shocker to one of your hubs. You can set its RF protocol, name, and limits when you add it.', - side: 'bottom', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('shockers-viewmode'), - title: 'Pick a control layout', - description: - 'Classic, Rich, or Simple. Switch between layouts depending on how you like to trigger shockers.', - side: 'bottom', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('shockers-layout'), - title: 'Group & layout options', - description: 'Group cards by hub and tweak how the grid is organised.', - side: 'bottom', - }, - ]; -} - -function userSharesDeepDive(): TourStep[] { - return [ - { - kind: 'action', - mobileContext: 'sidebar', - element: SIDEBAR_LINK('/shares/user'), - title: 'User shares', - description: 'Permanent shares between you and another OpenShock user. Click to take a look.', - side: 'right', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('user-shares-tabs'), - title: 'Three tabs', - description: - 'The tabs let you navigate between shares you give out, shares you receive, and pending invites.', - side: 'bottom', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('user-shares-tab-outgoing'), - title: 'Shares — outgoing', - description: - 'Everything you have shared with others. Each entry shows which shockers you granted and the permissions you set.', - side: 'bottom', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('user-shares-tab-incoming'), - title: 'Shared with Me', - description: - 'Shockers other users have granted you access to. They appear here once the owner accepts your request or sends you a share link.', - side: 'bottom', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('user-shares-tab-invites'), - title: 'Invites', - description: - 'Pending share requests waiting for the other party to accept. Once accepted they move to the Shares or Shared with Me tab.', - side: 'bottom', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('user-shares-new'), - title: 'Create a share', - description: - 'Pick a user, choose which shockers, and configure exactly what they can do: shock, vibrate, sound, intensity caps, durations.', - side: 'bottom', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('user-shares-redeem'), - title: 'Redeem a code', - description: - "Got a share code from someone else? Paste it here to accept the shockers they've shared with you.", - side: 'bottom', - }, - ]; -} - -function connectionsDeepDive(): TourStep[] { - return [ - { - kind: 'action', - mobileContext: 'sidebar', - element: SIDEBAR_LINK('/settings/connections'), - title: 'Connections', - description: - 'Link third-party accounts so you can sign in faster next time. Click to open it.', - side: 'right', - }, - { - kind: 'info', - mobileContext: 'page', - element: TOUR('connections-link'), - title: 'OAuth connections', - description: - "Link a Discord, GitHub, or other supported account and you'll be able to sign in with one click. No password needed.", - side: 'bottom', - }, - ]; -} - -function desktopSteps(initial: SidebarState): TourStep[] { - const startsExpanded = initial === 'desktop-expanded'; - - const steps: TourStep[] = []; - - if (startsExpanded) { - steps.push({ - kind: 'info', - element: SIDEBAR_ROOT_SEL, - title: 'Your navigation', - description: 'Everything you can reach in the app lives in this sidebar.', - side: 'right', - }); - steps.push(...sidebarOverviewSteps()); - steps.push({ - kind: 'action', - element: SIDEBAR_TOGGLE_SEL, - title: 'Collapse it', - description: 'Click to collapse the sidebar. Even collapsed, every icon is still navigable.', - side: 'bottom', - }); - } else { - steps.push({ - kind: 'info', - element: SIDEBAR_ROOT_SEL, - title: 'The icon rail', - description: 'When collapsed, the sidebar is a compact icon rail.', - side: 'right', - }); - steps.push({ - kind: 'action', - element: SIDEBAR_TOGGLE_SEL, - title: 'Expand the sidebar', - description: 'Click to open it up so we can walk through what each section does.', - side: 'bottom', - }); - steps.push(...sidebarOverviewSteps()); - } - - steps.push(...shockersDeepDive()); - steps.push(...userSharesDeepDive()); - steps.push(...connectionsDeepDive()); - - steps.push({ - kind: 'info', - element: USER_MENU_SEL, - title: 'Your account menu', - description: 'Profile, account settings, and logout are tucked in here.', - side: 'bottom', - skipIf: () => !document.querySelector(USER_MENU_SEL), - }); - - steps.push({ - kind: 'info', - title: "You're all set", - description: - 'Explore at your own pace. If we add a major new feature later, we may show you a fresh tour the next time you visit.', - }); - - return steps; -} - -function mobileSteps(): TourStep[] { - const steps: TourStep[] = []; - - steps.push({ - kind: 'info', - mobileContext: 'sidebar', - element: SIDEBAR_ROOT_SEL, - title: 'Your navigation', - description: 'Everything you can reach in the app lives here.', - side: 'right', - }); - - steps.push(...shockersDeepDive()); - steps.push(...userSharesDeepDive()); - steps.push(...connectionsDeepDive()); - - steps.push({ - kind: 'info', - title: "You're all set", - description: 'Tap any icon in the menu to navigate.', - }); - - return steps; -} - -function toDriverStep(step: TourStep): DriveStep { - return { - element: step.element, - // Highlighted element is only interactive on action steps (where the user - // is explicitly asked to click it). On info steps we don't want a stray - // click on a sidebar link to navigate away from the tour. - disableActiveInteraction: step.kind !== 'action', - popover: { - title: step.title, - description: step.description, - side: step.side ?? 'bottom', - showButtons: step.kind === 'action' ? ['previous', 'close'] : ['next', 'previous', 'close'], - }, - }; -} - -/** - * Starts an interactive welcome tour. Steps adapt to the current sidebar - * state and require the user to click into pages themselves; we wait for - * each next highlight target to render before advancing. - */ -export async function startWelcomeTour(): Promise { - if (!browser || isOnboardingDisabled()) return; - const initial = readSidebarState(); - const allSteps = isMobileViewport() ? mobileSteps() : desktopSteps(initial); - const tourSteps = allSteps.filter((s) => !s.skipIf?.()); - const driverSteps = tourSteps.map(toDriverStep); - - // On mobile, ensure the sidebar is open before the first step. - if (isMobileViewport() && tourSteps[0]?.mobileContext === 'sidebar') { - if (readSidebarState() === 'mobile-closed') { - (document.querySelector(SIDEBAR_TOGGLE_SEL) as HTMLElement)?.click(); - await waitForMobileSidebar(); - } - } - - return new Promise((resolve) => { - const ctx: { d: Driver | null; cleanup: (() => void) | null; completed: boolean } = { - d: null, - cleanup: null, - completed: false, - }; - - const wireUpActionAdvance = async () => { - ctx.cleanup?.(); - ctx.cleanup = null; - - const idx = ctx.d!.getActiveIndex(); - if (idx === undefined) return; - const step = tourSteps[idx]; - - if (step?.kind !== 'action' || !step.element) return; - - const target = document.querySelector(step.element); - if (!target) return; - - const handler = async () => { - ctx.cleanup?.(); - ctx.cleanup = null; - const next = tourSteps[idx + 1]; - // On mobile, close sidebar before showing page content steps. - if (isMobileViewport() && next?.mobileContext === 'page') { - if (readSidebarState() === 'mobile-open') { - (document.querySelector(SIDEBAR_TOGGLE_SEL) as HTMLElement)?.click(); - await waitForMobileSidebar(); - } - } - if (next?.element) { - await waitForElement(next.element); - } else { - await new Promise((r) => setTimeout(r, 80)); - } - ctx.d!.moveNext(); - }; - target.addEventListener('click', handler, { once: true }); - ctx.cleanup = () => target.removeEventListener('click', handler); - }; - - const handleNextClick = async () => { - const idx = ctx.d!.getActiveIndex(); - if (idx === undefined) return ctx.d!.moveNext(); - const next = tourSteps[idx + 1]; - if (!next) ctx.completed = true; // last step — Done button - // On mobile, open sidebar before showing sidebar steps reached via Next. - if (isMobileViewport() && next?.mobileContext === 'sidebar') { - if (readSidebarState() === 'mobile-closed') { - (document.querySelector(SIDEBAR_TOGGLE_SEL) as HTMLElement)?.click(); - await waitForMobileSidebar(); - if (next.element) await waitForElement(next.element); - } - } - ctx.d!.moveNext(); - }; - - const handlePrevClick = async () => { - const idx = ctx.d!.getActiveIndex(); - if (idx === undefined) return ctx.d!.movePrevious(); - const prev = tourSteps[idx - 1]; - // On mobile, open sidebar when going back to a sidebar step. - if (isMobileViewport() && prev?.mobileContext === 'sidebar') { - if (readSidebarState() === 'mobile-closed') { - (document.querySelector(SIDEBAR_TOGGLE_SEL) as HTMLElement)?.click(); - await waitForMobileSidebar(); - if (prev.element) await waitForElement(prev.element); - } - } - // On mobile, close sidebar when going back to a page step. - if (isMobileViewport() && prev?.mobileContext === 'page') { - if (readSidebarState() === 'mobile-open') { - (document.querySelector(SIDEBAR_TOGGLE_SEL) as HTMLElement)?.click(); - await waitForMobileSidebar(); - } - } - ctx.d!.movePrevious(); - }; - - ctx.d = driver({ - showProgress: true, - allowClose: true, - overlayClickBehavior: () => {}, - progressText: '{{current}} of {{total}}', - nextBtnText: 'Next', - prevBtnText: 'Back', - doneBtnText: 'Done', - steps: driverSteps, - onHighlighted: wireUpActionAdvance, - onNextClick: handleNextClick, - onPrevClick: handlePrevClick, - onCloseClick: (_el, _step, { driver: d }) => { - ctx.cleanup?.(); - markTourCompleted(); - d.destroy(); - resolve(); - }, - onDeselected: () => { - ctx.cleanup?.(); - ctx.cleanup = null; - }, - onDestroyed: () => { - ctx.cleanup?.(); - if (ctx.completed) goto(resolvePath('home')); - resolve(); - }, - }); - ctx.d.drive(); - }); -} diff --git a/src/routes/(app)/settings/connections/+page.svelte b/src/routes/(app)/settings/connections/+page.svelte index 7e16545e..f46c98c8 100644 --- a/src/routes/(app)/settings/connections/+page.svelte +++ b/src/routes/(app)/settings/connections/+page.svelte @@ -100,7 +100,7 @@ {#snippet child({ props })} - diff --git a/src/routes/(app)/shares/user/+layout.svelte b/src/routes/(app)/shares/user/+layout.svelte index ce54670f..09f46c35 100644 --- a/src/routes/(app)/shares/user/+layout.svelte +++ b/src/routes/(app)/shares/user/+layout.svelte @@ -80,35 +80,21 @@ - (redeemDialogOpen = true)} + >Redeem Code - +
- + - navigateTo('outgoing')} - data-tour="user-shares-tab-outgoing">Shares - navigateTo('incoming')} - data-tour="user-shares-tab-incoming">Shared with Me - navigateTo('invites')} - data-tour="user-shares-tab-invites">Invites navigateTo('outgoing')}>Shares + navigateTo('incoming')} + >Shared with Me + navigateTo('invites')}>Invites
diff --git a/src/routes/(app)/shockers/own/+page.svelte b/src/routes/(app)/shockers/own/+page.svelte index 400936fe..f1cfe393 100644 --- a/src/routes/(app)/shockers/own/+page.svelte +++ b/src/routes/(app)/shockers/own/+page.svelte @@ -195,12 +195,7 @@ {:else}
- {/snippet} @@ -259,13 +248,7 @@ {#snippet child({ props })} - {/snippet} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 21e3c8b0..d7eec530 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,10 +1,4 @@ -import { - isOnboardingDisabled, - isWelcomeSunset, - WELCOME_COOKIE_NAME, -} from '#lib/tour/onboarding-state.js'; import { SIDEBAR_COOKIE_NAME } from '@openshock/svelte-core/components/ui/sidebar/constants.js'; -import { isTruthy } from '@openshock/svelte-core/utils'; import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = ({ cookies }) => { @@ -13,25 +7,5 @@ export const load: LayoutServerLoad = ({ cookies }) => { // state and we avoid a collapse-then-expand flash on hydration. const sidebarOpen = cookies.get(SIDEBAR_COOKIE_NAME) === 'true'; - // When onboarding is disabled the client unmounts the screen immediately on - // hydration, so SSR-ing it just hurts LCP — skip it server-side too. - if (isOnboardingDisabled()) { - return { sidebarOpen, showWelcome: false }; - } - - // Past the sunset date the welcome screen is retired — never SSR it, and clear - // the cookie if a returning visitor still has it set. - if (isWelcomeSunset()) { - if (cookies.get(WELCOME_COOKIE_NAME) !== undefined) { - cookies.delete(WELCOME_COOKIE_NAME, { path: '/' }); - } - return { sidebarOpen, showWelcome: false }; - } - - // SSR the welcome screen for visitors who haven't seen it, so the logo is in - // the initial HTML rather than loaded after JS runs. Any non-truthy/missing - // cookie (including malformed) counts as "not seen". - const showWelcome = !isTruthy(cookies.get(WELCOME_COOKIE_NAME)); - - return { sidebarOpen, showWelcome }; + return { sidebarOpen }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e7eb1c65..592b975d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -14,7 +14,6 @@ import Footer from './Footer.svelte'; import Header from './Header.svelte'; import Sidebar from './Sidebar.svelte'; - import WelcomeScreen from './WelcomeScreen.svelte'; import '../app.css'; import { IsMobile } from '@openshock/svelte-core/hooks'; import { DialogManager } from '@openshock/svelte-core/components/dialog-manager'; @@ -36,8 +35,6 @@ let sidebarOpen = $state(untrack(() => data.sidebarOpen)); const isOpen = $derived(mobile.current ? false : sidebarOpen); - let welcomeOpen = $state(untrack(() => data.showWelcome)); - onMount(() => maybePromptTelemetryConsent()); @@ -50,14 +47,6 @@ - {#if welcomeOpen} - { - welcomeOpen = false; - }} - /> - {/if} - (sidebarOpen = v)}> - - - -
- {#each steps as _, i (i)} - - {/each} -
- -
-
- {#key step} -
- {#if isFirst} -

Welcome to OpenShock

- {:else} -

- Step {step + 1} of {steps.length} -

-

- {steps[step].title} -

- {/if} -
- {@render steps[step].body()} -
-
- {/key} -
- -
-
- -
- - {#if isLast && canStartTour} - - {:else} - - {/if} -
-
-
-
-
- -{#snippet stepWelcome()} -
-

- Welcome to the new -

- - OpenShock - -

- frontend -

- -

- Take the quick tour, or skip ahead and dive in. -

-
-{/snippet} - -{#snippet stepFeatures()} -

- Over 2 years of work. Faster, fully mobile-friendly, - and a much better base for everything we want to build next. -

-
    - {#each [{ t: 'Reworked shocker dashboard', d: 'Groundwork for customizable layouts.' }, { t: 'Web Terminal', d: 'Configure your hub and flash firmware straight from the browser. No installs.' }, { t: 'OAuth sign-in', d: 'Log in with accounts you already have.' }, { t: 'Better sharing', d: 'Public links, user shares, invite tracking, and per-shocker shortcuts.' }] as item, i (item.t)} -
  • -
    {item.t}
    -
    {item.d}
    -
  • - {/each} -
-{/snippet} - -{#snippet stepRedirects()} -

- Old bookmarks like - - openshock.app/#/dashboard/home - - forward automatically. Nothing for you to do. -

-

- The legacy site stays online during the transition. We'll announce a sunset date once the new - site has full feature parity. -

-{/snippet} - -{#snippet stepFeedback()} -

Something broken or missing? The faster you report it, the faster we can fix it.

- - - -

Lots more is landing soon. Thanks for sticking with us.

-{/snippet} diff --git a/src/worker-configuration.d.ts b/src/worker-configuration.d.ts index 3fb9b032..5cf534ff 100644 --- a/src/worker-configuration.d.ts +++ b/src/worker-configuration.d.ts @@ -9,7 +9,6 @@ interface __BaseEnv_Env { PUBLIC_DISCORD_INVITE_URL: string; PUBLIC_TURNSTILE_DEV_BYPASS_VALUE: string; PUBLIC_DEVELOPMENT_BANNER: string; - PUBLIC_DISABLE_ONBOARDING: string; PUBLIC_DISABLE_SHOCKER_MAP: string; PUBLIC_SITE_URL: string; PUBLIC_SITE_SHORT_URL: string; @@ -31,5 +30,5 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} }