diff --git a/frontend/src/features/clickgui/parts/modules.ts b/frontend/src/features/clickgui/parts/modules.ts index 80ea25c..10cbb8c 100644 --- a/frontend/src/features/clickgui/parts/modules.ts +++ b/frontend/src/features/clickgui/parts/modules.ts @@ -26,7 +26,7 @@ export const CATEGORIES: Category[] = [ id: 'player', name: 'Player', modules: list( - ['Auto Accept', 'Auto Leave', 'Auto Respawn', 'AutoTool', 'ClickPearl', 'Death Coords', 'FakeLag', 'Lock Slot', 'NoDelay', + ['Auto Accept', 'Auto Leave', 'Auto Respawn', 'AutoTool', 'ClickPearl', 'Death Coords', 'Lock Slot', 'NoDelay', 'NoInteract', 'Tape Mouse', 'Third Person', 'Use Tracker'], ['Auto Accept'], ), @@ -35,9 +35,9 @@ export const CATEGORIES: Category[] = [ id: 'visuals', name: 'Visuals', modules: list( - ['AlwaysVisibleHand', 'AmbientParticles', 'Animations', 'AspectRatio', 'AtmoDawnFog', 'BackSword', 'BlockESP', 'BlockHighlight', + ['AlwaysVisibleHand', 'AmbientParticles', 'Animations', 'AspectRatio', 'AtmoDawnFog', 'BackSword', 'BlockHighlight', 'CameraClip', 'Chams', 'ChinaHat', 'Crosshair', 'CrystalHoles', 'DropESP', 'EndCrystal', 'ESP', 'FovControl', 'FPS Optimizer', - 'Freecam', 'FullBright', 'Gps', 'Hitboxes', 'HitBubbles', 'HitEffect', 'InvisESP', 'Item Holder', 'Item Physic', 'JumpCircles', + 'Freecam', 'FullBright', 'Gps', 'Hitboxes', 'HitBubbles', 'HitEffect', 'Item Holder', 'Item Physic', 'JumpCircles', 'KillEffect', 'KineticLyrics', 'Light', 'Lightning Target ESP', 'MotionBlur', 'NameTags', 'NoRender', 'PostFX', 'Rain', 'ReimaginedVisual', 'ShaderSky', 'Shulker Preview', 'SoundESP', 'TazikHat', 'TotemFX', 'Trails', 'Trinket', 'Trollface Mask', 'ViewModel', 'Wet World', 'Wings', 'WorldParticles', 'WorldTweaks', 'Zoom'], diff --git a/frontend/src/features/landing/CommandHero.tsx b/frontend/src/features/landing/CommandHero.tsx index 723249f..7370beb 100644 --- a/frontend/src/features/landing/CommandHero.tsx +++ b/frontend/src/features/landing/CommandHero.tsx @@ -1,37 +1,39 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { prefersReducedMotion } from '@shared/motion/reducedMotion' import { ButtonLink } from '@shared/ui/Button' import { useSession } from '../auth/session' +import { useInView } from './effects/useInView' +import '@features/landing/styles/sections.css' -const COMMAND = '%config load 7KQ3M9XA' +// A placeholder code, not a plausible real one — this types out a UI demo, not live data. +const COMMAND = '%config load ••••••••' const TYPE_DELAY_MS = 60 - +/** Cloud configs, told through the one thing a player actually does: a command in the game chat. */ export function CommandHero() { const { t } = useTranslation('landing') const { status } = useSession() return ( -
- {t('cloud.eyebrow')} -

- {t('cloud.headlinePart1')} - {' '}
- {t('cloud.headlinePart2')} -

-

{t('cloud.subtitle')}

-
- {status === 'authenticated' ? ( - {t('cloud.ctaAuthenticated')} - ) : ( - {t('cloud.ctaAnonymous')} - )} - - {t('cloud.ctaShowcase')} - -
+
+
+

{t('cloud.title')}

+
+

{t('cloud.subtitle')}

+
+ {status === 'authenticated' ? ( + {t('cloud.ctaAuthenticated')} + ) : ( + {t('cloud.ctaAnonymous')} + )} + + {t('cloud.ctaShowcase')} + +
+
+
) } @@ -40,28 +42,30 @@ function ChatBar() { const { t } = useTranslation('landing') const [typed, setTyped] = useState(() => (prefersReducedMotion() ? COMMAND.length : 0)) const done = typed >= COMMAND.length + // Typing starts once the chat is on screen, so the player actually sees the command go in. + const ref = useRef(null) + const inView = useInView(ref, { once: true, rootMargin: '-80px' }) useEffect(() => { - if (done) return + if (done || !inView) return const t = setInterval(() => setTyped((n) => Math.min(n + 1, COMMAND.length)), TYPE_DELAY_MS) return () => clearInterval(t) - }, [done]) + }, [done, inView]) return (
- {done ? ( - - ) : null} +
) } diff --git a/frontend/src/features/landing/HowItWorks.tsx b/frontend/src/features/landing/HowItWorks.tsx index c2ab9d4..a18fcba 100644 --- a/frontend/src/features/landing/HowItWorks.tsx +++ b/frontend/src/features/landing/HowItWorks.tsx @@ -1,45 +1,56 @@ import { useTranslation } from 'react-i18next' import { Link } from 'react-router' +import '@features/landing/styles/sections.css' export function HowItWorks() { const { t } = useTranslation('landing') + // Placeholder-style codes (not plausible real share codes) — this is an illustration of + // the slot/code concept, not a real config, so it shouldn't look like fabricated data. + const placeholderCode = t('howItWorks.slotCodePlaceholder') const slots = [ - { name: t('howItWorks.slotNamePvp'), code: '7KQ3M9XA' }, - { name: t('howItWorks.slotNameStream'), code: 'R2WD8HNC' }, - { name: t('howItWorks.slotNameLazy'), code: 'L5TB4QZE' }, + { name: t('howItWorks.slotNamePvp'), code: placeholderCode }, + { name: t('howItWorks.slotNameStream'), code: placeholderCode }, + { name: t('howItWorks.slotNameLazy'), code: placeholderCode }, { name: null, code: null }, ] const linkSteps = [t('howItWorks.linkStep1'), t('howItWorks.linkStep2'), t('howItWorks.linkStep3')] return (
-
+

{t('howItWorks.slotsTitle')}

-

{t('howItWorks.slotsDesc')}

+

{t('howItWorks.slotsDesc')}

-
    + {/* The four slots drawn as a Minecraft hotbar; the first one is the "selected" slot. */} +
      {slots.map((slot, i) => ( -
    • +
    • {slot.name ? ( <> - {slot.name} - {slot.code} + {slot.name} + {slot.code} ) : ( - {t('howItWorks.emptySlot')} + {t('howItWorks.emptySlot')} )}
    • ))}
-
+

{t('howItWorks.linkTitle')}

-
    - {linkSteps.map((step) => ( -
  1. - {step} +
      + {linkSteps.map((step, i) => ( +
    1. + + {step}
    2. ))}
    @@ -47,7 +58,7 @@ export function HowItWorks() {

    {t('howItWorks.showcaseText')}{' '} - + {t('howItWorks.showcaseLink')}

    diff --git a/frontend/src/features/landing/effects/VoxelScene.tsx b/frontend/src/features/landing/effects/VoxelScene.tsx index 406d6d5..5330fa9 100644 --- a/frontend/src/features/landing/effects/VoxelScene.tsx +++ b/frontend/src/features/landing/effects/VoxelScene.tsx @@ -130,8 +130,12 @@ function drawSky(ctx: CanvasRenderingContext2D, w: number, h: number) { } } -/** Procedural blocky dusk landscape with layered scroll parallax. Purely decorative. */ -export function VoxelScene({ accent, className = '' }: { accent: string; className?: string }) { +/** + * Procedural blocky dusk landscape with layered scroll parallax. Purely decorative. + * The parallax is tuned for the top of the page (it follows window.scrollY); scenes further down + * pass `parallax={false}`, or the layers slide out of their frame by the time it is on screen. + */ +export function VoxelScene({ accent, className = '', parallax = true }: { accent: string; className?: string; parallax?: boolean }) { const rootRef = useRef(null) const canvases = useRef<(HTMLCanvasElement | null)[]>([]) @@ -160,7 +164,7 @@ export function VoxelScene({ accent, className = '' }: { accent: string; classNa }, [accent]) useEffect(() => { - if (prefersReducedMotion()) return + if (!parallax || prefersReducedMotion()) return let frame = 0 const lags = [0.5, ...LAYERS.map((l) => l.lag)] const onScroll = () => { @@ -178,7 +182,7 @@ export function VoxelScene({ accent, className = '' }: { accent: string; classNa window.removeEventListener('scroll', onScroll) cancelAnimationFrame(frame) } - }, []) + }, [parallax]) return (