diff --git a/public/images/Nighty_For_FAQ_Block_first_.webp b/public/images/Nighty_For_FAQ_Block_first_.webp index ccc35e6..a51b75a 100644 Binary files a/public/images/Nighty_For_FAQ_Block_first_.webp and b/public/images/Nighty_For_FAQ_Block_first_.webp differ diff --git a/public/images/Nighty_For_FAQ_Block_second_.webp b/public/images/Nighty_For_FAQ_Block_second_.webp index dad3ee5..45cfbe1 100644 Binary files a/public/images/Nighty_For_FAQ_Block_second_.webp and b/public/images/Nighty_For_FAQ_Block_second_.webp differ diff --git a/public/images/Webpage_Design_Parts_first_block_bg.webp b/public/images/Webpage_Design_Parts_first_block_bg.webp index 92c2ea4..a6a7165 100644 Binary files a/public/images/Webpage_Design_Parts_first_block_bg.webp and b/public/images/Webpage_Design_Parts_first_block_bg.webp differ diff --git a/public/images/Webpage_Design_Parts_header_block.webp b/public/images/Webpage_Design_Parts_header_block.webp index 0f8e62a..0b6dd23 100644 Binary files a/public/images/Webpage_Design_Parts_header_block.webp and b/public/images/Webpage_Design_Parts_header_block.webp differ diff --git a/public/images/Webpage_Design_Parts_second_block_bg.webp b/public/images/Webpage_Design_Parts_second_block_bg.webp index 78a6820..280de9a 100644 Binary files a/public/images/Webpage_Design_Parts_second_block_bg.webp and b/public/images/Webpage_Design_Parts_second_block_bg.webp differ diff --git a/public/images/butterfly1.webp b/public/images/butterfly1.webp index 48e493e..c7916f7 100644 Binary files a/public/images/butterfly1.webp and b/public/images/butterfly1.webp differ diff --git a/public/images/butterfly_dock.webp b/public/images/butterfly_dock.webp index 7536389..00a01ee 100644 Binary files a/public/images/butterfly_dock.webp and b/public/images/butterfly_dock.webp differ diff --git a/public/images/butterfly_tracker.webp b/public/images/butterfly_tracker.webp index 5803272..4456995 100644 Binary files a/public/images/butterfly_tracker.webp and b/public/images/butterfly_tracker.webp differ diff --git a/public/images/nighty_floating.webp b/public/images/nighty_floating.webp index cfe908f..c297d5a 100644 Binary files a/public/images/nighty_floating.webp and b/public/images/nighty_floating.webp differ diff --git a/public/images/og_slime.webp b/public/images/og_slime.webp index fc71bcb..c2abd60 100644 Binary files a/public/images/og_slime.webp and b/public/images/og_slime.webp differ diff --git a/public/images/purple_glow.webp b/public/images/purple_glow.webp index ec84c62..10cf682 100644 Binary files a/public/images/purple_glow.webp and b/public/images/purple_glow.webp differ diff --git a/public/images/slimevr_desktop.webp b/public/images/slimevr_desktop.webp index 5a4e151..8cf57cc 100644 Binary files a/public/images/slimevr_desktop.webp and b/public/images/slimevr_desktop.webp differ diff --git a/public/images/slimevr_mobile.webp b/public/images/slimevr_mobile.webp index 3d34ba1..9c6bf99 100644 Binary files a/public/images/slimevr_mobile.webp and b/public/images/slimevr_mobile.webp differ diff --git a/public/images/tracker_card_border.webp b/public/images/tracker_card_border.webp index c2311aa..300d65c 100644 Binary files a/public/images/tracker_card_border.webp and b/public/images/tracker_card_border.webp differ diff --git a/src/app.css b/src/app.css index 416ae19..3d56b68 100644 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,4 @@ -@import 'tailwindcss'; +@import "tailwindcss"; @plugin "@tailwindcss/typography"; :root { @@ -105,21 +105,23 @@ --animate-stars: stars 2s ease-in-out infinite; @keyframes stars { - 0%, 100% { + 0%, + 100% { opacity: 1; } 50% { - opacity: 0.5; + opacity: 0.5; } } --animate-stars2: stars2 2s ease-in-out infinite; @keyframes stars2 { - 0%, 100% { + 0%, + 100% { opacity: 0.5; } 50% { - opacity: 1; + opacity: 1; } } } @@ -206,6 +208,11 @@ a.link { -webkit-backface-visibility: hidden; } +.no-interact { + user-select: none; + pointer-events: none; +} + /* OverlayScrollbars styling */ .os-scrollbar-handle { background: rgba(255, 255, 255, 0.6) !important; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index f42ae1a..58148e8 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -32,7 +32,7 @@ "home": { "title": "SlimeVR Full-Body Trackers", "hero": { - "description": "An affordable, comfortable, wireless, 360° solution for full-body tracking in virtual reality", + "description": "The affordable VR full-body tracking solution.", "price": "From $219", "price-butterfly": "From $279", "order-btn": "Order on", @@ -40,6 +40,10 @@ "join-btn": "Join campaign at", "butterfly-slime": "Butterfly Trackers", "og-slime": "SlimeVR V1.2", + "buy-slime": { + "order": "Order SlimeVR 1.2 on", + "preorder": "Pre-order Butterfly Trackers on" + }, "download-server": { "title": "Download the server", "desc": "Available on all platforms!" @@ -51,7 +55,7 @@ }, "video-section": "Reviews and Demos", "use-cases": { - "title": " Use Cases", + "title": "Use Cases", "vr": { "title": "VR Games", "desc": "SlimeVR Trackers are compatible with any SteamVR or standalone games that support full-body tracking. That includes social games like VRChat." diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 1db77cc..e651e9a 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -2,6 +2,7 @@ import { A, useNavigate } from "@solidjs/router"; import { Component } from "solid-js"; import { Typography } from "./commons/Typography"; import { SlimeVRIcon } from "./commons/icons/SlimeVRIcon"; +import { scrollToSection } from "~/utils/dom"; export const Footer: Component = (props) => { const navigate = useNavigate(); @@ -20,7 +21,8 @@ export const Footer: Component = (props) => { class="mt-2 text-center sm:text-left text-xs text-background-30 link" href="/tm" > - SlimeVR and Butterfly Trackers are a trademark or a registered trademark of SlimeVR B.V. world-wide. + SlimeVR and Butterfly Trackers are a trademark or a registered + trademark of SlimeVR B.V. world-wide.
@@ -33,6 +35,7 @@ export const Footer: Component = (props) => { @@ -40,16 +43,8 @@ export const Footer: Component = (props) => { { - const pathname = window.location.pathname; e.preventDefault(); - if (pathname === "/") { - const el = document.getElementById("download"); - if (el) - el.scrollIntoView({ behavior: "smooth", block: "center" }); - } else { - // use location.state to pass scrollTo - navigate("/", { state: { scrollTo: "download" } }); - } + scrollToSection("download", window.location.pathname, navigate); }} class="link w-fit" > @@ -58,6 +53,7 @@ export const Footer: Component = (props) => { @@ -65,6 +61,7 @@ export const Footer: Component = (props) => { @@ -75,6 +72,7 @@ export const Footer: Component = (props) => { @@ -91,6 +89,7 @@ export const Footer: Component = (props) => { @@ -98,6 +97,7 @@ export const Footer: Component = (props) => { @@ -114,6 +114,7 @@ export const Footer: Component = (props) => { Bluesky @@ -121,6 +122,7 @@ export const Footer: Component = (props) => { YouTube @@ -128,6 +130,7 @@ export const Footer: Component = (props) => { TikTok @@ -135,6 +138,7 @@ export const Footer: Component = (props) => { Reddit diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index e96fb88..6de2ef9 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -2,6 +2,7 @@ import { A, useLocation, useNavigate } from "@solidjs/router"; import clsx from "clsx"; import { Component, createSignal } from "solid-js"; import { Localized } from "~/i18n"; +import { scrollToSection } from "~/utils/dom"; import { Button } from "./commons/Button"; import { BarsIcon } from "./commons/icons/BarsIcon"; import { SlimeVRIcon } from "./commons/icons/SlimeVRIcon"; @@ -17,33 +18,41 @@ export const NavItems: Component = () => { Home )} - + { - const pathname = window.location.pathname; e.preventDefault(); - if (pathname === "/") { - const el = document.getElementById("download"); - if (el) el.scrollIntoView({ behavior: "smooth", block: "center" }); - } else { - // use location.state to pass scrollTo - navigate("/", { state: { scrollTo: "download" } }); - } + scrollToSection("download", window.location.pathname, navigate); }} class="link px-2" > - + - + @@ -52,6 +61,7 @@ export const NavItems: Component = () => { @@ -84,6 +94,7 @@ export const Navbar: Component = (props) => { @@ -92,6 +103,7 @@ export const Navbar: Component = (props) => { diff --git a/src/components/Section.tsx b/src/components/Section.tsx index 9fddae9..7fccc53 100644 --- a/src/components/Section.tsx +++ b/src/components/Section.tsx @@ -10,13 +10,11 @@ export const Section: ParentComponent = (props) => {
{(gizmo) => ( -
+
)} diff --git a/src/components/commons/Container.tsx b/src/components/commons/Container.tsx index 0314899..231256d 100644 --- a/src/components/commons/Container.tsx +++ b/src/components/commons/Container.tsx @@ -2,8 +2,10 @@ import { ParentProps } from "solid-js"; export function Container(props: ParentProps & { class?: string }) { return ( -
- {props.children} -
+
+ {props.children} +
); } diff --git a/src/components/commons/Markdown.tsx b/src/components/commons/Markdown.tsx index 8106dd7..9105544 100644 --- a/src/components/commons/Markdown.tsx +++ b/src/components/commons/Markdown.tsx @@ -1,7 +1,12 @@ import { ComponentProps } from "solid-js"; export const MarkdownLink = (props: ComponentProps<"a">) => ( -
+ {props.children} ); diff --git a/src/components/commons/Typography.tsx b/src/components/commons/Typography.tsx index 8bf1c2a..0c7e0ae 100644 --- a/src/components/commons/Typography.tsx +++ b/src/components/commons/Typography.tsx @@ -62,7 +62,7 @@ export const Typography: Component = (initialProps) => { props.textAlign, props.italic && "italic", props.truncate && "leading-3 text-ellipsis line-clamp-2", - props.class + props.class, ]) ); diff --git a/src/components/commons/icons/ButterflySlimeIcon.tsx b/src/components/commons/icons/ButterflySlimeIcon.tsx deleted file mode 100644 index 4dc2171..0000000 --- a/src/components/commons/icons/ButterflySlimeIcon.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Component, mergeProps } from "solid-js"; - -export const ButterflySlimeIcon: Component = (initialProps) => { - return ; -}; diff --git a/src/components/commons/icons/DownloadIcon.tsx b/src/components/commons/icons/DownloadIcon.tsx index 3f61e98..46b1f37 100644 --- a/src/components/commons/icons/DownloadIcon.tsx +++ b/src/components/commons/icons/DownloadIcon.tsx @@ -1,6 +1,6 @@ import { Component } from "solid-js"; -export const DonwloadIcon: Component = (props) => { +export const DownloadIcon: Component = (props) => { return ( = (props) => { viewBox="0 0 25 35" xmlns="http://www.w3.org/2000/svg" > - + ); }; diff --git a/src/components/commons/icons/socials/SteamIcon.tsx b/src/components/commons/icons/socials/SteamIcon.tsx index 0a42e86..9667ba1 100644 --- a/src/components/commons/icons/socials/SteamIcon.tsx +++ b/src/components/commons/icons/socials/SteamIcon.tsx @@ -1,6 +1,10 @@ import { Component } from "solid-js"; -export const SteamIcon: Component = (props) => { +interface Props extends IconProps { + variant?: "default" | "download"; +} + +export const SteamIcon: Component = (props) => { return ( = (props) => { xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 65 65" - fill="#fff" + fill={props.variant === "download" ? "#fff" : "#fff"} > diff --git a/src/components/contributors/Card.tsx b/src/components/contributors/Card.tsx index c70c8e5..449e836 100644 --- a/src/components/contributors/Card.tsx +++ b/src/components/contributors/Card.tsx @@ -38,30 +38,43 @@ const CARD_TOLERANCE = 8; const FOCUSED_SCALE = 1.4; const NORMAL_SCALE = 1; -// transform utilities w/ caching -const transformCache = new Map(); -const createTransform = (rotateX = 0, rotateY = 0, scale = NORMAL_SCALE) => { - // round values to prevent fractional pixel issues - const roundedX = Math.round(rotateX * 100) / 100; - const roundedY = Math.round(rotateY * 100) / 100; - const roundedScale = Math.round(scale * 1000) / 1000; - - const key = `${roundedX},${roundedY},${roundedScale}`; - if (transformCache.has(key)) { - return transformCache.get(key)!; - } - - const transform = `perspective(1000px) rotateY(${roundedY}deg) rotateX(${roundedX}deg) scale(${roundedScale})`; - if (transformCache.size < 100) transformCache.set(key, transform); - - return transform; +const SOCIAL_ICONS: Record< + string, + { icon: any; size: number; class?: string } +> = { + github: { icon: GithubIcon, size: 20 }, + twitter: { icon: TwitterIcon, size: 20 }, + bluesky: { icon: BlueskyIcon, size: 18 }, + instagram: { icon: InstagramIcon, size: 22 }, + youtube: { icon: YoutubeIcon, size: 22 }, + twitch: { icon: TwitchIcon, size: 18, class: "mt-0.5 mr-0.5" }, + kofi: { icon: KofiIcon, size: 22 }, + discord: { icon: DiscordIcon, size: 20 }, + tiktok: { icon: TiktokIcon, size: 20 }, + printables: { icon: PrintablesIcon, size: 18 }, + steam: { icon: SteamIcon, size: 18 }, + matrix: { icon: MatrixIcon, size: 16 }, + website: { icon: WebsiteIcon, size: 20 }, + reddit: { icon: RedditIcon, size: 20 }, + patreon: { icon: PatreonIcon, size: 16 }, }; +const ROLE_ICONS: Record = { + dev: { icon: DevIcon, size: 22 }, + artist: { icon: ArtistIcon, size: 22 }, + community: { icon: PeopleIcon, size: 26 }, +}; + +// transform utilities +const createTransform = (rotateX = 0, rotateY = 0, scale = NORMAL_SCALE) => + `perspective(1000px) rotateY(${rotateY.toFixed(2)}deg) rotateX(${rotateX.toFixed(2)}deg) scale(${scale.toFixed(3)})`; + interface CardProps extends Contributor { class?: string; // additional classes for the card onClick?: () => void; isFocused?: boolean; cachedImage?: { src: string; classes: string; error: boolean }; + "data-card-name"?: string; } export const Card: ParentComponent = (props) => { @@ -69,9 +82,9 @@ export const Card: ParentComponent = (props) => { props; const borderColor = color || FALLBACK_COLOR; - let card: HTMLDivElement; - let placeholder: HTMLDivElement; // placeholder for the card when focused to keep its position in list - let innerDiv: HTMLDivElement; + let card: HTMLDivElement = null as any; + let placeholder: HTMLDivElement = null as any; // placeholder for the card when focused to keep its position in list + let innerDiv: HTMLDivElement = null as any; const [imageError, setImageError] = createSignal(cachedImage?.error ?? false); const [imageLoading, setImageLoading] = createSignal(!cachedImage); @@ -80,7 +93,7 @@ export const Card: ParentComponent = (props) => { ); const [imgClasses, setImgClasses] = createSignal( cachedImage?.classes ?? - "object-contain w-[calc(100%+16px)] scale-[103%] select-none pointer-events-none brightness-[0.01]" + "object-contain w-[calc(100%+16px)] scale-[103%] no-interact brightness-[0.01]" ); // TODO: allow hover/tilting during animation without it interrupting the animation - idk how to do this without breaking other things tbh const [transitioning, setTransitioning] = createSignal(false); // prevent tilting while transitioning (interrupting it) @@ -88,23 +101,27 @@ export const Card: ParentComponent = (props) => { let originalPosition: { top: number; left: number } | null = null; let transitionTimeout: ReturnType | null = null; // prevent multiple transitions / out of sync (from multiple clicks) - let lastMousePosition: { x: number; y: number } = { x: 0, y: 0 }; - - // cache rect to avoid recalculating on every frame - let cachedRect: DOMRect | null = null; - let resizeObserver: ResizeObserver | null = null; + let lastMousePosition = { x: 0, y: 0 }; /* * Tilt effect things for card */ - let framePending = false; + // cache rect to avoid recalculating on every frame + let cachedRect: DOMRect | null = null; + let rafId: number | null = null; let queuedTilt: { x: number; y: number; intensity: number; - glow: number; + glowOpacity: number; } | null = null; + const setCardVars = (mouseX = "0px", mouseY = "0px", glowOpacity = "0") => { + card.style.setProperty("--mouse-x", mouseX); + card.style.setProperty("--mouse-y", mouseY); + card.style.setProperty("--glow-opacity", glowOpacity); + }; + const doTilt = ( clientX: number, clientY: number, @@ -116,7 +133,6 @@ export const Card: ParentComponent = (props) => { const rect = cachedRect || card.getBoundingClientRect(); const x = clientX - rect.left; const y = clientY - rect.top; - const centerX = rect.width / 2; const centerY = rect.height / 2; @@ -128,10 +144,7 @@ export const Card: ParentComponent = (props) => { const scale = props.isFocused ? FOCUSED_SCALE : NORMAL_SCALE; - card.style.setProperty("--mouse-x", `${x}px`); - card.style.setProperty("--mouse-y", `${y}px`); - card.style.setProperty("--glow-opacity", glowOpacity.toString()); - + setCardVars(`${x / scale}px`, `${y / scale}px`, glowOpacity.toString()); card.style.transform = createTransform( percentY * intensity, percentX * intensity, @@ -146,19 +159,19 @@ export const Card: ParentComponent = (props) => { glowOpacity: number ) => { lastMousePosition = { x: clientX, y: clientY }; - queuedTilt = { x: clientX, y: clientY, intensity, glow: glowOpacity }; - if (framePending) return; - framePending = true; - requestAnimationFrame(() => { + queuedTilt = { x: clientX, y: clientY, intensity, glowOpacity }; + if (rafId) return; + rafId = requestAnimationFrame(() => { if (queuedTilt) { doTilt( queuedTilt.x, queuedTilt.y, queuedTilt.intensity, - queuedTilt.glow + queuedTilt.glowOpacity ); + queuedTilt = null; } - framePending = false; + rafId = null; }); }; @@ -180,9 +193,7 @@ export const Card: ParentComponent = (props) => { const cardHoverEnter = (e: PointerEvent) => { if (props.isFocused || transitioning() || e.pointerType === "touch") return; - cachedRect = card.getBoundingClientRect(); - card.style.transition = `transform ${HOVER_TRANSITION_DURATION}ms ease`; card.style.willChange = "transform"; document.addEventListener("pointermove", cardHoverTilt, { passive: true }); @@ -200,7 +211,6 @@ export const Card: ParentComponent = (props) => { const cardHoverTilt = (e: PointerEvent) => { if (props.isFocused || transitioning() || e.pointerType === "touch") return; - if (isMouseOverCard(e)) { queueTilt(e.clientX, e.clientY, 15, 0.45); } else { @@ -215,7 +225,7 @@ export const Card: ParentComponent = (props) => { const cardReset = () => { card.style.transition = `transform ${FOCUS_TRANSITION_DURATION}ms ease`; card.style.transform = createTransform(); - card.style.setProperty("--glow-opacity", "0"); + setCardVars(); }; /* @@ -226,26 +236,19 @@ export const Card: ParentComponent = (props) => { }; const cardFocus = () => { - if (transitionTimeout) { - clearTimeout(transitionTimeout); - transitionTimeout = null; - } - + if (transitionTimeout) clearTimeout(transitionTimeout); setTransitioning(true); setIsFocused(true); - card.style.willChange = "transform"; - // reset tilt effect to get actual position + card.style.willChange = "transform"; card.style.transition = "none"; card.style.transform = createTransform(); - card.style.setProperty("--glow-opacity", "0"); - - card.offsetHeight; + setCardVars(); + card.offsetHeight; // force reflow const rect = card.getBoundingClientRect(); originalPosition = { top: rect.top, left: rect.left }; - placeholder.style.display = "block"; // move card to body to ensure it's above all other elements @@ -256,18 +259,13 @@ export const Card: ParentComponent = (props) => { card.style.top = `${rect.top}px`; card.style.left = `${rect.left}px`; - card.offsetHeight; - card.style.transition = CARD_TRANSITION; + card.offsetHeight; // force reflow - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; + card.style.transition = CARD_TRANSITION; card.style.transformOrigin = "center center"; - const centerX = viewportWidth / 2 - card.clientWidth / 2; - const centerY = viewportHeight / 2 - card.clientHeight / 2; - + card.style.top = `${(window.innerHeight - card.clientHeight) / 2}px`; + card.style.left = `${(window.innerWidth - card.clientWidth) / 2}px`; card.style.transform = createTransform(0, 0, FOCUSED_SCALE); - card.style.top = `${centerY}px`; - card.style.left = `${centerX}px`; card.style.boxShadow = "0px 10px 20px 8px #02111db8"; document.removeEventListener("pointermove", cardHoverTilt); @@ -280,41 +278,33 @@ export const Card: ParentComponent = (props) => { }; const cardUnfocus = () => { - if (transitionTimeout) { - clearTimeout(transitionTimeout); - transitionTimeout = null; - } - + if (transitionTimeout) clearTimeout(transitionTimeout); setTransitioning(true); setIsFocused(false); card.style.zIndex = "998"; card.style.transition = CARD_TRANSITION; card.style.boxShadow = "none"; - + card.style.transform = createTransform(); + setCardVars(); if (originalPosition) { card.style.top = `${originalPosition.top}px`; card.style.left = `${originalPosition.left}px`; } - card.style.transform = createTransform(); document.removeEventListener("pointermove", cardTilt); - card.style.setProperty("--glow-opacity", "0"); - // switch back to relative positioning, hide placeholder, and allow scrolling after transition completes transitionTimeout = setTimeout(() => { card.style.transition = "none"; card.style.position = "relative"; card.style.top = ""; card.style.left = ""; card.style.zIndex = "0"; - card.style.transform = createTransform(); card.style.willChange = "auto"; // move card back to its original parent (placeholder's parent) placeholder.parentNode?.insertBefore(card, placeholder); placeholder.style.display = "none"; - setTransitioning(false); transitionTimeout = null; @@ -325,10 +315,8 @@ export const Card: ParentComponent = (props) => { clientY: lastMousePosition.y, pointerType: "mouse", } as PointerEvent; - - if (isMouseOverCard(mockEvent)) { + if (isMouseOverCard(mockEvent)) document.addEventListener("pointermove", cardHoverTilt); - } }, FOCUS_TRANSITION_DURATION); }; @@ -361,6 +349,8 @@ export const Card: ParentComponent = (props) => { /* * Reactivity and life cycle stuff */ + let resizeObserver: ResizeObserver | null = null; + onMount(() => { if (typeof window === "undefined" || !card) return; @@ -385,16 +375,13 @@ export const Card: ParentComponent = (props) => { document.removeEventListener("pointermove", cardHoverTilt); document.removeEventListener("pointermove", cardTilt); + if (transitionTimeout) clearTimeout(transitionTimeout); + if (rafId) cancelAnimationFrame(rafId); if (resizeObserver) { resizeObserver.disconnect(); resizeObserver = null; } - if (transitionTimeout) { - clearTimeout(transitionTimeout); - transitionTimeout = null; - } - if (card) { card.style.position = ""; card.style.top = ""; @@ -425,7 +412,7 @@ export const Card: ParentComponent = (props) => { setImgSrc(image.src); setImgClasses( clsx( - "object-contain w-[calc(100%+16px)] scale-[103%] select-none pointer-events-none", + "object-contain w-[calc(100%+16px)] scale-[103%] no-interact", classes ) ); @@ -469,9 +456,9 @@ export const Card: ParentComponent = (props) => { style={borderStyle()} data-card-name={props["data-card-name"]} > - {/* glow effect when hovering - optimized for performance */} + {/* glow effect when hovering */}
= (props) => { {/* roles */}
- {roles.includes("dev") && ( - - - - )} - {roles.includes("artist") && ( - - - - )} - {roles.includes("community") && ( - - - - )} + {roles.map((role, i) => { + const RoleConfig = ROLE_ICONS[role]; + if (!RoleConfig) return null; + const { icon: IconComponent, size } = RoleConfig; + return ( + + + + ); + })}
{/* card image - bg and slime photo */} @@ -536,10 +518,8 @@ export const Card: ParentComponent = (props) => { }} /> {(imageLoading() || imageError()) && ( -
- - ? - +
+ ?
)}
@@ -550,8 +530,13 @@ export const Card: ParentComponent = (props) => { {/* socials */} {Object.keys(socials).length > 0 && (
- {Object.entries(socials).map(([key, value], i) => { - if (!value) return null; + {Object.entries(socials).map(([key, value]) => { + if (!value || !SOCIAL_ICONS[key]) return null; + const { + icon: IconComponent, + size, + class: className, + } = SOCIAL_ICONS[key]; return ( = (props) => { target="_blank" rel="noopener noreferrer" > - {key === "github" && } - {key === "twitter" && } - {key === "bluesky" && } - {key === "instagram" && } - {key === "youtube" && } - {key === "twitch" && ( - - )} - {key === "kofi" && } - {key === "discord" && } - {key === "tiktok" && } - {key === "printables" && } - {key === "steam" && } - {key === "matrix" && } - {key === "website" && } - {key === "reddit" && } - {key === "patreon" && } + ); })} diff --git a/src/components/home/DownloadSection.tsx b/src/components/home/DownloadSection.tsx index c9fb3a3..7bd1572 100644 --- a/src/components/home/DownloadSection.tsx +++ b/src/components/home/DownloadSection.tsx @@ -3,12 +3,13 @@ import clsx from "clsx"; import { children, Component, + For, JSX, mergeProps, ParentComponent, splitProps, } from "solid-js"; -import { DonwloadIcon } from "../../components/commons/icons/DownloadIcon"; +import { DownloadIcon } from "../../components/commons/icons/DownloadIcon"; import { AndroidIcon, AppleIcon, @@ -17,12 +18,51 @@ import { WindowsIcon, } from "../../components/commons/icons/PlatformIcons"; import { Typography } from "../commons/Typography"; +import { SteamIcon } from "../commons/icons/socials/SteamIcon"; interface DownloadButtonProps extends AnchorProps { prefixIcon?: JSX.Element; variant?: "primary" | "default"; } +const DownloadLinks = { + desktop: [ + { + name: "Steam", + href: "https://store.steampowered.com/app/3245490/SlimeVR/", + icon: , + }, + { + name: "Windows", + href: "https://github.com/SlimeVR/SlimeVR-Installer/releases/latest/download/slimevr_web_installer.exe", + icon: , + }, + { + name: "Linux", + href: "https://flathub.org/apps/dev.slimevr.SlimeVR", + icon: , + }, + { + name: "MacOS", + href: "https://github.com/SlimeVR/SlimeVR-Installer/releases/latest/download/slimevr_web_installer.dmg", + icon: , + }, + ], + mobile: [ + { + name: "Android", + href: "https://play.google.com/store/apps/details?id=dev.slimevr.server.android", + icon: , + }, + { + name: "SideQuest", + href: "https://sidequestvr.com/app/45270/slimevr", + icon: , + }, + // and iOS would be here.. if it ever gets better + ], +}; + export const DownloadButton: ParentComponent = ( initialProps ) => { @@ -44,11 +84,13 @@ export const DownloadButton: ParentComponent = (
= (
{props.children}
- +
); @@ -72,8 +114,8 @@ export const DownloadSection: Component = () => { Download SlimeVR Server -
- +
+
@@ -82,71 +124,45 @@ export const DownloadSection: Component = () => { Desktop
- } - href="https://github.com/SlimeVR/SlimeVR-Installer/releases/latest/download/slimevr_web_installer.exe" - target="_blank" - > - - Windows - - - } - href="https://flathub.org/apps/dev.slimevr.SlimeVR" - target="_blank" - > - - Linux - - - } - href="https://github.com/SlimeVR/SlimeVR-Server/releases/latest/download/SlimeVR-mac.dmg" - target="_blank" - > - - MacOS - - -
-
- - Mobile - + + {(link) => ( + + + {link.name} + + + )} +
+ + Mobile +
- } - href="https://play.google.com/store/apps/details?id=dev.slimevr.server.android" - target="_blank" - > - - Android - - - } - href="https://sidequestvr.com/app/45270/slimevr" - target="_blank" - > - - SideQuest - - + + {(link) => ( + + + {link.name} + + + )} +
-
+ +
SlimeVR Server running on a PC
@@ -155,6 +171,7 @@ export const DownloadSection: Component = () => { class="w-full rounded-lg" loading="lazy" src="/images/slimevr_mobile.webp" + alt="SlimeVR Server running on a phone" >
diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx new file mode 100644 index 0000000..a0f7fa8 --- /dev/null +++ b/src/components/home/HeroSection.tsx @@ -0,0 +1,230 @@ +import { A } from "@solidjs/router"; +import { Component } from "solid-js"; +import { ArrowButton } from "../commons/ArrowButton"; +import { CartIcon } from "../commons/icons/CartIcon"; +import { CrowdSupplyIcon } from "../commons/icons/CrowdSupplyIcon"; +import { DiscordIcon } from "../commons/icons/DiscordIcon"; +import { DownloadIcon } from "../commons/icons/DownloadIcon"; +import { Typography } from "../commons/Typography"; + +const TrackerCard: Component<{ + titleKey: string; + mainImage: string; + priceKey: string; + orderKey?: string; + href: string; +}> = (props) => { + return ( +
+ + +
+ + + + SlimeVR tracker image +
+ +
+ + {/* regular version of order button */} + + + {/* smaller version for anything smaller than 1080p + } + href={props.href} + > +
+ + +
+
+
+
+ ); +}; + +export const HeroSection: Component = () => { + return ( +
+ {/* desktop hero */} + + + {/* mobile hero */} +
+ + + +
+ + {/* nighty showing trackers bg */} + + + {/* CTA stuff */} +
+
+
+ } + href="#download" + onClick={(e) => { + e.preventDefault(); + document + .getElementById("download") + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }} + > + + + +
+ +
+ } + href="https://discord.gg/SlimeVR" + > + + + +
+
+
+
+ ); +}; diff --git a/src/components/home/QASection.tsx b/src/components/home/QASection.tsx index 5804836..ef473c4 100644 --- a/src/components/home/QASection.tsx +++ b/src/components/home/QASection.tsx @@ -65,7 +65,11 @@ export const QASection: Component = () => {
diff --git a/src/components/home/VideoSection.tsx b/src/components/home/VideoSection.tsx index fdf70e1..fe2a65e 100644 --- a/src/components/home/VideoSection.tsx +++ b/src/components/home/VideoSection.tsx @@ -16,43 +16,51 @@ import { Typography } from "../commons/Typography"; const videos = [ { - src: "https://www.youtube.com/embed/2JqAj1_KYXU", + src: "https://www.youtube-nocookie.com/embed/2JqAj1_KYXU", thumbnail: "https://i.ytimg.com/vi/2JqAj1_KYXU/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/JHSXlTkXKbc", + src: "https://www.youtube-nocookie.com/embed/0fQlpzWBbXk", + thumbnail: "https://i.ytimg.com/vi/0fQlpzWBbXk/hqdefault.jpg", + }, + { + src: "https://www.youtube-nocookie.com/embed/vBZreCeOmio", + thumbnail: "https://i.ytimg.com/vi/vBZreCeOmio/hqdefault.jpg", + }, + { + src: "https://www.youtube-nocookie.com/embed/RC6hxWJvv9Q", + thumbnail: "https://i.ytimg.com/vi/RC6hxWJvv9Q/hqdefault.jpg", + }, + { + src: "https://www.youtube-nocookie.com/embed/JHSXlTkXKbc", thumbnail: "https://i.ytimg.com/vi/JHSXlTkXKbc/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/0NMxdY1G0kY", + src: "https://www.youtube-nocookie.com/embed/0NMxdY1G0kY", thumbnail: "https://i.ytimg.com/vi/0NMxdY1G0kY/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/_a124Y8y9UM", + src: "https://www.youtube-nocookie.com/embed/_a124Y8y9UM", thumbnail: "https://i.ytimg.com/vi/_a124Y8y9UM/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/Pbm2pK8iEAA", + src: "https://www.youtube-nocookie.com/embed/Pbm2pK8iEAA", thumbnail: "https://i.ytimg.com/vi/Pbm2pK8iEAA/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/G0du9c7doqs", + src: "https://www.youtube-nocookie.com/embed/G0du9c7doqs", thumbnail: "https://i.ytimg.com/vi/G0du9c7doqs/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/0fQlpzWBbXk?si=q_As-iKh4Lv_8l6P", - thumbnail: "https://i.ytimg.com/vi/0fQlpzWBbXk/hqdefault.jpg", - }, - { - src: "https://www.youtube.com/embed/s2zRppG0izI", + src: "https://www.youtube-nocookie.com/embed/s2zRppG0izI", thumbnail: "https://i.ytimg.com/vi/s2zRppG0izI/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/CzZhXpOiFh0?si=K7TJleoDlhsTxnR9", + src: "https://www.youtube-nocookie.com/embed/CzZhXpOiFh0", thumbnail: "https://i.ytimg.com/vi/CzZhXpOiFh0/hqdefault.jpg", }, { - src: "https://www.youtube.com/embed/2sIk4kmNwTQ?si=2tnHYa4oWbzAkvWA", + src: "https://www.youtube-nocookie.com/embed/2sIk4kmNwTQ", thumbnail: "https://i.ytimg.com/vi/2sIk4kmNwTQ/hqdefault.jpg", }, ]; @@ -65,10 +73,11 @@ const VideoThumbnail: Component<{ return ( + )}
{ active={shouldShowPrev()} >
diff --git a/src/components/sponsors/index.ts b/src/components/sponsors/index.ts index 7fe263c..18ec3cd 100644 --- a/src/components/sponsors/index.ts +++ b/src/components/sponsors/index.ts @@ -1,2 +1,2 @@ export * from "./SponsorCard"; -export * from "./PastSponsorAvatar"; \ No newline at end of file +export * from "./PastSponsorAvatar"; diff --git a/src/layouts/LoadingLayout.tsx b/src/layouts/LoadingLayout.tsx index db32ba3..ff208c7 100644 --- a/src/layouts/LoadingLayout.tsx +++ b/src/layouts/LoadingLayout.tsx @@ -1,5 +1,4 @@ import { Component } from "solid-js"; -import { LoaderIcon } from "../components/commons/icons/LoaderIcon"; export const LoadingLayout: Component = () => { return ( diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 9237dd1..9107861 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -14,8 +14,8 @@ const ANIMATION_TIME = 5500; const MIN_TIME = 6000; const RANDOM_TIME = 5000; const ExplosionDot: Component = () => { - let animationId; - let animationResetId; + let animationId: NodeJS.Timeout; + let animationResetId: NodeJS.Timeout; const [explodePos, setExplodePos] = createSignal<{ x: number; @@ -69,12 +69,12 @@ const ExplosionDot: Component = () => { }; interface MainLaoutProps { - variant?: 'default' | 'butterfly' | 'og-slime'; + variant?: "default" | "butterfly" | "og-slime"; } export const MainLayout: ParentComponent = (props) => { return ( -
+
@@ -82,8 +82,9 @@ export const MainLayout: ParentComponent = (props) => {
@@ -92,6 +93,6 @@ export const MainLayout: ParentComponent = (props) => {
-
+
); }; diff --git a/src/routes/[...404].tsx b/src/routes/[...404].tsx index 79ee417..5650a68 100644 --- a/src/routes/[...404].tsx +++ b/src/routes/[...404].tsx @@ -15,7 +15,7 @@ export default function NotFound() { Page not found
- + Sad slime
Go back to the home page diff --git a/src/routes/index.tsx b/src/routes/index.tsx index bc81648..6470678 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -2,18 +2,15 @@ import { Link, Meta } from "@solidjs/meta"; import { A, useLocation } from "@solidjs/router"; import { Component, ParentProps, createEffect } from "solid-js"; import { AppTitle } from "~/components/AppTitle"; -import { ArrowButton } from "~/components/commons/ArrowButton"; import { Container } from "~/components/commons/Container"; -import { CartIcon } from "~/components/commons/icons/CartIcon"; -import { CrowdSupplyIcon } from "~/components/commons/icons/CrowdSupplyIcon"; -import { DiscordIcon } from "~/components/commons/icons/DiscordIcon"; -import { DonwloadIcon } from "~/components/commons/icons/DownloadIcon"; import { Typography } from "~/components/commons/Typography"; import { DownloadSection } from "~/components/home/DownloadSection"; +import { HeroSection } from "~/components/home/HeroSection"; import { QASection } from "~/components/home/QASection"; import { VideoSection } from "~/components/home/VideoSection"; import { Section } from "~/components/Section"; import { MainLayout } from "~/layouts/MainLayout"; +import { scrollToSection } from "~/utils/dom"; const UseCaseCard: Component<{ title: string; image: string; desc: string }> = ( props @@ -28,11 +25,12 @@ const UseCaseCard: Component<{ title: string; image: string; desc: string }> = ( key={props.title} />
-
+
{props.title}
@@ -53,16 +51,14 @@ const UseCaseCard: Component<{ title: string; image: string; desc: string }> = ( export default function HomePage(props: ParentProps) { const location = useLocation(); - // scroll to a section after navigation, see Navbar.tsx + // scroll to a section after navigation (for dom.ts#scrollToSection) createEffect(() => { const scrollTo = (location.state as { scrollTo?: string } | undefined) ?.scrollTo; if (!scrollTo) return; requestAnimationFrame(() => { - const el = document.getElementById(scrollTo); - if (!el) return; - el.scrollIntoView({ behavior: "smooth", block: "center" }); + scrollToSection(scrollTo, location.pathname, () => {}); }); }); @@ -92,200 +88,12 @@ export default function HomePage(props: ParentProps) { href="/images/butterfly_tracker.webp" type="image/webp" /> +
-
- - - -
- - -
- -
-
- -
- } - href="#download" - onClick={(e) => { - e.preventDefault(); - document - .getElementById("download") - .scrollIntoView({ behavior: "smooth", block: "center" }); - }} - > - - - - } - href="https://discord.gg/SlimeVR" - > - - - -
-
+
-
-
-
-
- - - -
- - - - -
- -
-
- } - href="https://www.crowdsupply.com/slimevr/slimevr-butterfly-trackers" - > -
- - -
-
-
-
-
- - - -
- - - - -
- -
-
- } - href="https://www.crowdsupply.com/slimevr/slimevr-full-body-tracker" - > -
- - -
-
-
-
-
-
- +
diff --git a/src/routes/team.tsx b/src/routes/team.tsx index d6b850b..ca957c5 100644 --- a/src/routes/team.tsx +++ b/src/routes/team.tsx @@ -211,7 +211,7 @@ const preloadImage = (name: string, classes?: string): Promise => { imageCache.set(name, { src: image.src, classes: clsx( - "object-contain w-[calc(100%+16px)] scale-[103%] select-none", + "object-contain w-[calc(100%+16px)] scale-[103%] no-interact", classes ), error: false, @@ -222,7 +222,7 @@ const preloadImage = (name: string, classes?: string): Promise => { imageCache.set(name, { src: `/images/contributors/jovannmc.webp`, // fallback classes: - "object-contain w-[calc(100%+16px)] scale-[103%] select-none brightness-[0.01]", + "object-contain w-[calc(100%+16px)] scale-[103%] no-interact brightness-[0.01]", error: true, }); resolve(); @@ -428,7 +428,7 @@ export default function TeamPage(props: ParentProps) { class={clsx( "max-w-62.5 w-full transform", isShuffling() - ? "animate-pulse scale-95 opacity-80 rotate-1 pointer-events-none duration-200" + ? "animate-pulse scale-95 opacity-80 rotate-1 no-interact duration-200" : "scale-100 opacity-100 rotate-0" )} > diff --git a/src/utils/dom.ts b/src/utils/dom.ts index c17841f..9a25f88 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -1 +1,18 @@ +import { Navigator } from "@solidjs/router"; + export const getContentSize = (elem: Element) => elem.getBoundingClientRect(); + +export const scrollToSection = ( + sectionId: string, + pathname: string, + navigate: Navigator +) => { + if (pathname === "/") { + const el = document.getElementById(sectionId); + if (el) el.scrollIntoView({ behavior: "smooth", block: "center" }); + return; + } + + // use location.state to pass scrollTo + navigate("/", { state: { scrollTo: sectionId } }); +}; diff --git a/src/utils/scrollbar.ts b/src/utils/scrollbar.ts index ca78256..b287df8 100644 --- a/src/utils/scrollbar.ts +++ b/src/utils/scrollbar.ts @@ -12,6 +12,7 @@ export function getInstance(): OverlayScrollbars | null { export function setScroll(enabled: boolean) { if (!overlayScrollbar) return; - if (enabled) overlayScrollbar.options({ overflow: { x: "hidden", y: "scroll" } }); + if (enabled) + overlayScrollbar.options({ overflow: { x: "hidden", y: "scroll" } }); else overlayScrollbar.options({ overflow: { x: "hidden", y: "hidden" } }); } diff --git a/src/utils/store.ts b/src/utils/store.ts index f80a8b4..bb3070c 100644 --- a/src/utils/store.ts +++ b/src/utils/store.ts @@ -1,4 +1,4 @@ -import { createRenderEffect, createSignal, on } from "solid-js"; +import { createSignal } from "solid-js"; export const createStoredSignal = ( defaultValue: T, @@ -18,7 +18,7 @@ export const createStoredSignal = ( // }; const signal = createSignal( - (options.onBeforeSave ?? ((v) => v))(defaultValue) + (options?.onBeforeSave ?? ((v) => v))(defaultValue) ); // createRenderEffect(