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:
Wanjohi
2024-08-30 16:19:58 +03:00
committed by GitHub
parent d13d3dc5d8
commit 73cec51728
102 changed files with 5096 additions and 105 deletions

View 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)

View 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)

View 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)

View 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"

View 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>
);
})

View 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);

View 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)

View 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)

View 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);

View 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)

View 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)

View 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)