mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-14 01:35:37 +02:00
✨ feat: Add qwik-react (#103)
This adds the following pages: The landing page (/) The pricing page (/pricing) The contact page (/contact) The changelog page (/changelog) Terms Of Service page (/terms) Privacy Policy (/privacy)
This commit is contained in:
112
packages/ui/src/react/cursor.tsx
Normal file
112
packages/ui/src/react/cursor.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/** @jsxImportSource react */
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { cn } from '@/design'
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
|
||||
export const CursorSVG = ({ flip }: { flip?: boolean }) => (
|
||||
<svg fill="none" height="18"
|
||||
className={cn(flip ? 'scale-x-[-1]' : '', 'mb-9')}
|
||||
viewBox="0 0 17 18" width="17">
|
||||
<path
|
||||
d="M15.5036 3.11002L12.5357 15.4055C12.2666 16.5204 10.7637 16.7146 10.22 15.7049L7.4763 10.6094L2.00376 8.65488C0.915938 8.26638 0.891983 6.73663 1.96711 6.31426L13.8314 1.65328C14.7729 1.28341 15.741 2.12672 15.5036 3.11002ZM7.56678 10.6417L7.56645 10.6416C7.56656 10.6416 7.56667 10.6416 7.56678 10.6417L7.65087 10.4062L7.56678 10.6417Z"
|
||||
fill="currentColor"
|
||||
style={{
|
||||
stroke: `var(--cursor-color)`,
|
||||
fill: `var(--cursor-color-light)`,
|
||||
}}
|
||||
stroke="currentColor"
|
||||
// className={cn(color ? `stroke-${color}-400 text-${color}-500` : 'stroke-primary-400 text-primary-500')}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
type CursorProps = { class?: string; text: string, color?: string, flip?: boolean }
|
||||
|
||||
const hexToRGBA = (hex: string, alpha: number = 1) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
export const ReactCursor: React.FC<CursorProps> = ({
|
||||
class: className,
|
||||
text,
|
||||
color = "#3B82F6",
|
||||
flip = false,
|
||||
}) => {
|
||||
const [randomVariant, setRandomVariant] = React.useState({});
|
||||
|
||||
React.useEffect(() => {
|
||||
const generateRandomVariant = () => {
|
||||
const randomX = Math.random() * 40 - 20; // Random value between -20 and 20
|
||||
const randomY = Math.random() * 40 - 20; // Random value between -40 and 40
|
||||
const randomDuration = 3 + Math.random() * 7 ; // Random duration between 3 and 5 seconds
|
||||
|
||||
return {
|
||||
animate: {
|
||||
translateX: [0, randomX, 0],
|
||||
translateY: [0, randomY, 0],
|
||||
},
|
||||
transition: {
|
||||
duration: randomDuration,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse" as const,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
setRandomVariant(generateRandomVariant());
|
||||
}, []);
|
||||
|
||||
const cursorElement = <CursorSVG flip={flip} />;
|
||||
const textElement = (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: `var(--cursor-color)`,
|
||||
borderColor: `var(--cursor-color-dark)`,
|
||||
}}
|
||||
className={cn(
|
||||
'w-fit rounded-full py-1 px-2 text-white',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const colorStyles = {
|
||||
'--cursor-color': color,
|
||||
'--cursor-color-light': hexToRGBA(color, 0.7),
|
||||
'--cursor-color-dark': hexToRGBA(color, 0.8),
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ translateX: 0, translateY: 0 }}
|
||||
//
|
||||
{...randomVariant}
|
||||
style={colorStyles}
|
||||
className="flex items-center"
|
||||
>
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{flip ? (
|
||||
<>
|
||||
{cursorElement}
|
||||
{textElement}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{textElement}
|
||||
{cursorElement}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Cursor = qwikify$(ReactCursor)
|
||||
58
packages/ui/src/react/display.tsx
Normal file
58
packages/ui/src/react/display.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
display,
|
||||
type DisplayProps as DisplayVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp
|
||||
} from "@/design"
|
||||
import * as ReactBalancer from "react-wrap-balancer"
|
||||
import { cn } from "@/design"
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
|
||||
type DisplaySize = DisplayVariants["size"]
|
||||
type DisplaySizeProp = DisplaySize | {
|
||||
initial?: DisplaySize,
|
||||
sm?: DisplaySize,
|
||||
md?: DisplaySize,
|
||||
lg?: DisplaySize,
|
||||
xl?: DisplaySize,
|
||||
xxl?: DisplaySize,
|
||||
}
|
||||
|
||||
export interface DisplayProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "span",
|
||||
className?: string,
|
||||
size?: DisplaySizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp
|
||||
}
|
||||
|
||||
export const ReactDisplay = ({
|
||||
size,
|
||||
as = "h1",
|
||||
weight,
|
||||
align,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: DisplayProps) => {
|
||||
const DisplayElement = as
|
||||
return (
|
||||
<DisplayElement className={display({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
className: cn("font-title font-extrabold", className)
|
||||
})} {...props}>
|
||||
<ReactBalancer.Balancer>
|
||||
{children}
|
||||
</ReactBalancer.Balancer>
|
||||
</DisplayElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDisplay.displayName = "Display"
|
||||
|
||||
export const Display = qwikify$(ReactDisplay)
|
||||
142
packages/ui/src/react/hero-section.tsx
Normal file
142
packages/ui/src/react/hero-section.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import { motion } from "framer-motion"
|
||||
import { ReactDisplay } from "@/react/display"
|
||||
import * as React from "react"
|
||||
// type Props = {
|
||||
// children?: React.ReactElement[]
|
||||
// }
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ReactHeroSection({ children }: Props) {
|
||||
return (
|
||||
<>
|
||||
<section className="px-4" >
|
||||
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-1">
|
||||
<motion.img
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 120
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
...transition
|
||||
}}
|
||||
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
|
||||
<div className="my-4 sm:mt-8">
|
||||
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.1,
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
Your games.
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 80
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.2,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
Your rules.
|
||||
</motion.span>
|
||||
</ReactDisplay>
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 50
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="dark:text-primary-50/70 text-primary-950/70 text-lg font-normal tracking-tight sm:text-xl"
|
||||
>
|
||||
Nestri lets you play games on your own terms — invite friends to join your gaming sessions, share your game library, and take even more control by hosting your own server.
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 60
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.4,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="flex items-center justify-center mt-4 w-full"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// export const ReactGpadAnimation = ({ index, children, className }: { className?: string, index: number, children?: React.ReactElement }) => {
|
||||
// return (
|
||||
// <motion.div
|
||||
// className={className}
|
||||
// animate={{
|
||||
// scale: [1, 1.1, 1],
|
||||
// opacity: [0.5, 1, 0.5],
|
||||
// }}
|
||||
// transition={{
|
||||
// duration: 3,
|
||||
// repeat: Infinity,
|
||||
// delay: index * 0.2,
|
||||
// }}
|
||||
// >
|
||||
// {children}
|
||||
// </motion.div>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export const GpadAnimation = qwikify$(ReactGpadAnimation)
|
||||
export const HeroSection = qwikify$(ReactHeroSection)
|
||||
7
packages/ui/src/react/index.ts
Normal file
7
packages/ui/src/react/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./hero-section"
|
||||
export * from "./react-example"
|
||||
export * from "./cursor"
|
||||
export * from "./title-section"
|
||||
export * from "./motion"
|
||||
export * from "./title"
|
||||
export * from "./text"
|
||||
98
packages/ui/src/react/marquee.tsx
Normal file
98
packages/ui/src/react/marquee.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { cn } from "@/design";
|
||||
import { component$, useStore, type Component } from "@builder.io/qwik";
|
||||
|
||||
|
||||
interface MarqueeProps<T> {
|
||||
parentClass?: string;
|
||||
itemClass?: string;
|
||||
items: T[];
|
||||
pauseOnHover?: boolean;
|
||||
renderItem: Component<{ item: T; index: number }>;
|
||||
reverse?: boolean;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
pad?: boolean;
|
||||
translate?: 'track' | 'items';
|
||||
state?: 'running' | 'paused';
|
||||
spill?: boolean;
|
||||
diff?: boolean;
|
||||
inset?: number;
|
||||
outset?: number;
|
||||
speed?: number;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
const renderStamp = Date.now()
|
||||
|
||||
export const Marquee = component$(<T,>({
|
||||
parentClass,
|
||||
itemClass,
|
||||
reverse = false,
|
||||
items,
|
||||
renderItem: RenderItem,
|
||||
direction = 'horizontal',
|
||||
translate = 'items',
|
||||
state = 'running',
|
||||
pad = false,
|
||||
spill = false,
|
||||
diff = false,
|
||||
inset = 0,
|
||||
outset = 0,
|
||||
pauseOnHover = false,
|
||||
speed = 10,
|
||||
scale = 1,
|
||||
}: MarqueeProps<T>) => {
|
||||
const store = useStore({
|
||||
indices: Array.from({ length: items.length }, (_, i) => i),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn("marquee-container", parentClass)}
|
||||
data-direction={direction}
|
||||
data-pad={pad}
|
||||
data-pad-diff={diff}
|
||||
data-pause-on-hover={pauseOnHover ? 'true' : 'false'}
|
||||
data-translate={translate}
|
||||
data-play-state={state}
|
||||
data-spill={spill}
|
||||
data-reverse={reverse}
|
||||
style={{ '--speed': speed, '--count': items.length, '--scale': scale, '--inset': inset, '--outset': outset }}
|
||||
>
|
||||
<ul>
|
||||
{pad && translate === 'track'
|
||||
? store.indices.map((index) => {
|
||||
return (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class={cn("pad pad--negative", itemClass)}
|
||||
key={`pad-negative-${renderStamp}--${index}`}
|
||||
>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
{store.indices.map((index) => {
|
||||
return (
|
||||
<li key={`index-${renderStamp}--${index}`} style={{ '--index': index }}>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{pad && translate === 'track'
|
||||
? store.indices.map((index) => {
|
||||
return (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class={cn("pad pad--positive", itemClass)}
|
||||
key={`pad-positive-${renderStamp}--${index}`}
|
||||
>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
39
packages/ui/src/react/motion.tsx
Normal file
39
packages/ui/src/react/motion.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
import { motion, type MotionProps } from 'framer-motion';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface MotionComponentProps extends MotionProps {
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
children?: ReactNode;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
delay: 0.4,
|
||||
}
|
||||
|
||||
export const ReactMotionComponent = ({
|
||||
as = 'div',
|
||||
children,
|
||||
class: className,
|
||||
...motionProps
|
||||
}: MotionComponentProps) => {
|
||||
const MotionTag = motion[as as keyof typeof motion];
|
||||
|
||||
return (
|
||||
<MotionTag className={className}
|
||||
{...motionProps}
|
||||
// animate={isInView ? whileInView : undefined}
|
||||
>
|
||||
{children}
|
||||
</MotionTag>
|
||||
);
|
||||
};
|
||||
|
||||
export const MotionComponent = qwikify$(ReactMotionComponent);
|
||||
117
packages/ui/src/react/my-cursor.tsx
Normal file
117
packages/ui/src/react/my-cursor.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/** @jsxImportSource react */
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { cn } from '@/design'
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
|
||||
export const CursorSVG = ({ flip }: { flip?: boolean }) => (
|
||||
<svg fill="none" height="18"
|
||||
className={cn(flip ? 'scale-x-[-1]' : '', 'mb-9')}
|
||||
viewBox="0 0 17 18" width="17">
|
||||
<path
|
||||
d="M15.5036 3.11002L12.5357 15.4055C12.2666 16.5204 10.7637 16.7146 10.22 15.7049L7.4763 10.6094L2.00376 8.65488C0.915938 8.26638 0.891983 6.73663 1.96711 6.31426L13.8314 1.65328C14.7729 1.28341 15.741 2.12672 15.5036 3.11002ZM7.56678 10.6417L7.56645 10.6416C7.56656 10.6416 7.56667 10.6416 7.56678 10.6417L7.65087 10.4062L7.56678 10.6417Z"
|
||||
fill="currentColor"
|
||||
style={{
|
||||
stroke: `var(--cursor-color)`,
|
||||
fill: `var(--cursor-color-light)`,
|
||||
}}
|
||||
stroke="currentColor"
|
||||
// className={cn(color ? `stroke-${color}-400 text-${color}-500` : 'stroke-primary-400 text-primary-500')}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
type CursorProps = { class?: string; text: string, color?: string, flip?: boolean }
|
||||
|
||||
const hexToRGBA = (hex: string, alpha: number = 1) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
export const ReactMyCursor: React.FC<CursorProps> = ({
|
||||
class: className,
|
||||
text,
|
||||
color = "#3B82F6",
|
||||
flip = false,
|
||||
}) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [position, setPosition] = React.useState({ x: 0, y: 0 });
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
setPosition({
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
});
|
||||
|
||||
console.log(event.clientX - rect.left, event.clientY - rect.top)
|
||||
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
const cursorElement = <CursorSVG flip={flip} />;
|
||||
const textElement = (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: `var(--cursor-color)`,
|
||||
borderColor: `var(--cursor-color-dark)`,
|
||||
}}
|
||||
className={cn(
|
||||
'w-fit rounded-full py-1 px-2 text-white',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const colorStyles = {
|
||||
'--cursor-color': color,
|
||||
'--cursor-color-light': hexToRGBA(color, 0.7),
|
||||
'--cursor-color-dark': hexToRGBA(color, 0.8),
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
...colorStyles,
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
}}
|
||||
className="flex items-center"
|
||||
>
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{flip ? (
|
||||
<>
|
||||
{cursorElement}
|
||||
{textElement}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{textElement}
|
||||
{cursorElement}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const MyCursor = qwikify$(ReactMyCursor)
|
||||
28
packages/ui/src/react/react-example.tsx
Normal file
28
packages/ui/src/react/react-example.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/** @jsxImportSource react */
|
||||
//This is used for testing whether the Qwik React components are working
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
export const ReactExample = () => {
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
style={{
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
backgroundColor: "red",
|
||||
}}
|
||||
animate={{y:0}}
|
||||
initial={{y:100}}
|
||||
// whileHover={{ scale: 1.2, rotate: 90 }}
|
||||
// whileTap={{
|
||||
// scale: 0.8,
|
||||
// rotate: -90,
|
||||
// borderRadius: "100%"
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Example = qwikify$(ReactExample)
|
||||
119
packages/ui/src/react/save.tsx
Normal file
119
packages/ui/src/react/save.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/** @jsxImportSource react */
|
||||
import { cn } from "@/design";
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
motion,
|
||||
useAnimationFrame,
|
||||
useMotionValue,
|
||||
useScroll,
|
||||
useSpring,
|
||||
useTransform,
|
||||
useVelocity,
|
||||
} from "framer-motion";
|
||||
|
||||
interface VelocityScrollProps {
|
||||
text: string;
|
||||
default_velocity?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
interface ParallaxProps {
|
||||
children: string;
|
||||
baseVelocity: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const wrap = (min: number, max: number, v: number) => {
|
||||
const rangeSize = max - min;
|
||||
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
|
||||
};
|
||||
|
||||
export function ReactMarquee({
|
||||
class: className,
|
||||
text,
|
||||
default_velocity = 5,
|
||||
}: VelocityScrollProps) {
|
||||
function ParallaxText({
|
||||
children,
|
||||
baseVelocity = 100,
|
||||
class:className,
|
||||
}: ParallaxProps) {
|
||||
const baseX = useMotionValue(0);
|
||||
const { scrollY } = useScroll();
|
||||
const scrollVelocity = useVelocity(scrollY);
|
||||
const smoothVelocity = useSpring(scrollVelocity, {
|
||||
damping: 50,
|
||||
stiffness: 400,
|
||||
});
|
||||
|
||||
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
|
||||
clamp: false,
|
||||
});
|
||||
|
||||
const [repetitions, setRepetitions] = useState(1);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateRepetitions = () => {
|
||||
if (containerRef.current && textRef.current) {
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const textWidth = textRef.current.offsetWidth;
|
||||
const newRepetitions = Math.ceil(containerWidth / textWidth) + 2;
|
||||
setRepetitions(newRepetitions);
|
||||
}
|
||||
};
|
||||
|
||||
calculateRepetitions();
|
||||
|
||||
window.addEventListener("resize", calculateRepetitions);
|
||||
return () => window.removeEventListener("resize", calculateRepetitions);
|
||||
}, [children]);
|
||||
|
||||
const x = useTransform(baseX, (v) => `${wrap(-100 / repetitions, 0, v)}%`);
|
||||
|
||||
const directionFactor = React.useRef<number>(1);
|
||||
useAnimationFrame((t, delta) => {
|
||||
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
|
||||
|
||||
if (velocityFactor.get() < 0) {
|
||||
directionFactor.current = -1;
|
||||
} else if (velocityFactor.get() > 0) {
|
||||
directionFactor.current = 1;
|
||||
}
|
||||
|
||||
moveBy += directionFactor.current * moveBy * velocityFactor.get();
|
||||
|
||||
baseX.set(baseX.get() + moveBy);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden whitespace-nowrap"
|
||||
ref={containerRef}
|
||||
>
|
||||
<motion.div className={cn("inline-block", className)} style={{ x }}>
|
||||
{Array.from({ length: repetitions }).map((_, i) => (
|
||||
<span key={i} ref={i === 0 ? textRef : null}>
|
||||
{children}{" "}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative w-full">
|
||||
<ParallaxText baseVelocity={default_velocity} class={className}>
|
||||
{text}
|
||||
</ParallaxText>
|
||||
<ParallaxText baseVelocity={-default_velocity} class={className}>
|
||||
{text}
|
||||
</ParallaxText>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const Marquee = qwikify$(ReactMarquee);
|
||||
70
packages/ui/src/react/text.tsx
Normal file
70
packages/ui/src/react/text.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
text,
|
||||
type TextProps as TextVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp,
|
||||
cn
|
||||
} from "@/design"
|
||||
// import * as ReactBalancer from "react-wrap-balancer"
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
|
||||
type TextSize = TextVariants["size"]
|
||||
type TitleSizeProp = TextSize | {
|
||||
initial?: TextSize,
|
||||
sm?: TextSize,
|
||||
md?: TextSize,
|
||||
lg?: TextSize,
|
||||
xl?: TextSize,
|
||||
xxl?: TextSize,
|
||||
}
|
||||
|
||||
export interface TextProps extends React.HTMLAttributes<HTMLParagraphElement | HTMLSpanElement | HTMLDivElement> {
|
||||
as?: "p" | "div" | "span" | "em" | "strong",
|
||||
className?: string,
|
||||
size?: TitleSizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp;
|
||||
neutral?: boolean;
|
||||
}
|
||||
|
||||
export const ReactText: React.FC<TextProps> = ({
|
||||
size,
|
||||
as = "p",
|
||||
weight,
|
||||
align,
|
||||
neutral,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
|
||||
const TextElement = as
|
||||
|
||||
if (as === "strong") {
|
||||
weight = weight || "medium"
|
||||
neutral = neutral || true
|
||||
} else if (as === "em") {
|
||||
neutral = neutral || true
|
||||
}
|
||||
|
||||
return (
|
||||
<TextElement className={text({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
neutral,
|
||||
className: cn("dark:text-primary-50/70 text-primary-950/70", className)
|
||||
})} {...props}>
|
||||
{/* <ReactBalancer.Balancer> */}
|
||||
{children}
|
||||
{/* </ReactBalancer.Balancer> */}
|
||||
</TextElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactText.displayName = "Text"
|
||||
|
||||
export const Text = qwikify$(ReactText)
|
||||
88
packages/ui/src/react/title-section.tsx
Normal file
88
packages/ui/src/react/title-section.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import { motion } from "framer-motion"
|
||||
import { ReactDisplay } from "@/react/display"
|
||||
// type Props = {
|
||||
// children?: React.ReactElement[]
|
||||
// }
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
description: string | string[]
|
||||
}
|
||||
|
||||
export function ReactTitleSection({ title, description }: Props) {
|
||||
return (
|
||||
<>
|
||||
<section className="px-4" >
|
||||
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-4">
|
||||
<motion.img
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 120
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
|
||||
<div className="my-4 sm:my-8">
|
||||
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.1,
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
{title}
|
||||
</motion.span>
|
||||
</ReactDisplay>
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 50
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="dark:text-primary-50/70 text-primary-950/70 text-lg font-normal tracking-tight sm:text-xl"
|
||||
>
|
||||
{Array.isArray(description) ? description.map((item, index) => {
|
||||
return <span key={`id-${index}`}>{item} <br /> </span>
|
||||
}) : description}
|
||||
</motion.p>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const TitleSection = qwikify$(ReactTitleSection)
|
||||
56
packages/ui/src/react/title.tsx
Normal file
56
packages/ui/src/react/title.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
title,
|
||||
type TitleProps as TitleVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp,
|
||||
cn
|
||||
} from "@/design"
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
|
||||
|
||||
type TitleSize = TitleVariants["size"]
|
||||
type TitleSizeProp = TitleSize | {
|
||||
initial?: TitleSize,
|
||||
sm?: TitleSize,
|
||||
md?: TitleSize,
|
||||
lg?: TitleSize,
|
||||
xl?: TitleSize,
|
||||
xxl?: TitleSize,
|
||||
}
|
||||
|
||||
export interface TitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "span",
|
||||
className?: string,
|
||||
size?: TitleSizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp
|
||||
}
|
||||
|
||||
export const ReactTitle: React.FC<TitleProps> = ({
|
||||
size,
|
||||
as = "h1",
|
||||
weight,
|
||||
align,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const TitleElement = as
|
||||
return (
|
||||
<TitleElement className={title({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
className: cn("font-title dark:text-primary-50 text-primary-950", className)
|
||||
})} {...props}>
|
||||
{children}
|
||||
</TitleElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactTitle.displayName = "Title"
|
||||
|
||||
export const Title = qwikify$(ReactTitle)
|
||||
Reference in New Issue
Block a user