package updates, minor bug fixes, stack items, background animation fixes
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
const PALETTES = {
|
||||
dark: {
|
||||
@@ -37,6 +38,12 @@ interface Particle {
|
||||
wobblePhase: number;
|
||||
}
|
||||
|
||||
interface CanvasSize {
|
||||
dpr: number;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface AnimatedBackgroundContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
@@ -48,6 +55,9 @@ interface AnimatedBackgroundContainerProps {
|
||||
|
||||
const DEFAULT_PARTICLE_COLORS: readonly string[] = PALETTES.dark.particles;
|
||||
const PARTICLE_COLOR_COUNT = DEFAULT_PARTICLE_COLORS.length;
|
||||
const EDGE_FADE_DISTANCE = 80;
|
||||
const MAX_DEVICE_PIXEL_RATIO = 2;
|
||||
const MOBILE_TARGET_DISTANCE = 30;
|
||||
|
||||
const rand = (min: number, max: number) => Math.random() * (max - min) + min;
|
||||
|
||||
@@ -87,15 +97,18 @@ export default function AnimatedBackgroundContainer({
|
||||
const frameRef = useRef(0);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const isMobileRef = useRef(false);
|
||||
const isVisibleRef = useRef(true);
|
||||
const prefersReducedMotionRef = useRef(false);
|
||||
const particlesRef = useRef<Particle[]>([]);
|
||||
const mousePosRef = useRef({ x: 0, y: 0 });
|
||||
const smoothMouseRef = useRef({ x: 0, y: 0 });
|
||||
const mobileAnchorRef = useRef({ x: 0, y: 0 });
|
||||
const mobileTargetRef = useRef({ x: 0, y: 0 });
|
||||
const canvasSizeRef = useRef<CanvasSize>({ dpr: 1, height: 0, width: 0 });
|
||||
const containerRectRef = useRef<DOMRect | null>(null);
|
||||
const followSpeedRef = useRef(followSpeed);
|
||||
const mobileSpeedRef = useRef(mobileSpeed);
|
||||
const particleColorsRef = useRef<readonly string[]>(DEFAULT_PARTICLE_COLORS);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDark = resolvedTheme === undefined || resolvedTheme === "dark";
|
||||
@@ -120,13 +133,13 @@ export default function AnimatedBackgroundContainer({
|
||||
}, [particleCount, orbitRadius]);
|
||||
|
||||
const seedPositions = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
const { height, width } = canvasSizeRef.current;
|
||||
if (width === 0 || height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const centerX = container.clientWidth / 2;
|
||||
const centerY = container.clientHeight / 2;
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
mousePosRef.current = { x: centerX, y: centerY };
|
||||
smoothMouseRef.current = { x: centerX, y: centerY };
|
||||
@@ -137,6 +150,13 @@ export default function AnimatedBackgroundContainer({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateContainerRect = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
containerRectRef.current = container.getBoundingClientRect();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resizeCanvas = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
@@ -146,10 +166,19 @@ export default function AnimatedBackgroundContainer({
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, MAX_DEVICE_PIXEL_RATIO);
|
||||
const nextWidth = Math.round(width * dpr);
|
||||
const nextHeight = Math.round(height * dpr);
|
||||
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
canvasSizeRef.current = { dpr, height, width };
|
||||
updateContainerRect();
|
||||
|
||||
if (canvas.width !== nextWidth) {
|
||||
canvas.width = nextWidth;
|
||||
}
|
||||
if (canvas.height !== nextHeight) {
|
||||
canvas.height = nextHeight;
|
||||
}
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
@@ -160,14 +189,10 @@ export default function AnimatedBackgroundContainer({
|
||||
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
if (!mounted) {
|
||||
seedPositions();
|
||||
}
|
||||
}, [mounted, seedPositions]);
|
||||
seedPositions();
|
||||
}, [seedPositions, updateContainerRect]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
isMobileRef.current = isMobileDevice();
|
||||
resizeCanvas();
|
||||
|
||||
@@ -176,25 +201,35 @@ export default function AnimatedBackgroundContainer({
|
||||
resizeCanvas();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [resizeCanvas]);
|
||||
const resizeObserver =
|
||||
"ResizeObserver" in window
|
||||
? new ResizeObserver(() => {
|
||||
handleResize();
|
||||
})
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) {
|
||||
return;
|
||||
if (containerRef.current && resizeObserver) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
seedPositions();
|
||||
}, [mounted, seedPositions]);
|
||||
window.addEventListener("resize", handleResize);
|
||||
window.addEventListener("scroll", updateContainerRect, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
return () => {
|
||||
resizeObserver?.disconnect();
|
||||
window.removeEventListener("resize", handleResize);
|
||||
window.removeEventListener("scroll", updateContainerRect, { capture: true });
|
||||
};
|
||||
}, [resizeCanvas, updateContainerRect]);
|
||||
|
||||
const handleMouseMove = useCallback((event: MouseEvent) => {
|
||||
const container = containerRef.current;
|
||||
if (!container || isMobileRef.current) {
|
||||
const rect = containerRectRef.current;
|
||||
if (!rect || isMobileRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
mousePosRef.current = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
@@ -212,13 +247,8 @@ export default function AnimatedBackgroundContainer({
|
||||
}, [handleMouseMove]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!canvas || !container) {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,9 +257,25 @@ export default function AnimatedBackgroundContainer({
|
||||
return;
|
||||
}
|
||||
|
||||
const stopAnimation = () => {
|
||||
if (animationFrameRef.current !== null) {
|
||||
window.cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const draw = () => {
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
if (!isVisibleRef.current || prefersReducedMotionRef.current) {
|
||||
animationFrameRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const { height, width } = canvasSizeRef.current;
|
||||
if (width === 0 || height === 0) {
|
||||
animationFrameRef.current = window.requestAnimationFrame(draw);
|
||||
return;
|
||||
}
|
||||
|
||||
frameRef.current += 1;
|
||||
|
||||
if (isMobileRef.current) {
|
||||
@@ -241,14 +287,15 @@ export default function AnimatedBackgroundContainer({
|
||||
|
||||
const dx = mobileTargetRef.current.x - mobileAnchorRef.current.x;
|
||||
const dy = mobileTargetRef.current.y - mobileAnchorRef.current.y;
|
||||
if (Math.hypot(dx, dy) < 30) {
|
||||
if (Math.hypot(dx, dy) < MOBILE_TARGET_DISTANCE) {
|
||||
mobileTargetRef.current = {
|
||||
x: rand(width * 0.15, width * 0.85),
|
||||
y: rand(height * 0.15, height * 0.85),
|
||||
};
|
||||
}
|
||||
|
||||
smoothMouseRef.current = { ...mobileAnchorRef.current };
|
||||
smoothMouseRef.current.x = mobileAnchorRef.current.x;
|
||||
smoothMouseRef.current.y = mobileAnchorRef.current.y;
|
||||
} else {
|
||||
const desktopLerp = followSpeedRef.current;
|
||||
smoothMouseRef.current.x +=
|
||||
@@ -275,7 +322,13 @@ export default function AnimatedBackgroundContainer({
|
||||
|
||||
const edgeFade = Math.max(
|
||||
0,
|
||||
Math.min(x / 80, (width - x) / 80, y / 80, (height - y) / 80, 1),
|
||||
Math.min(
|
||||
x / EDGE_FADE_DISTANCE,
|
||||
(width - x) / EDGE_FADE_DISTANCE,
|
||||
y / EDGE_FADE_DISTANCE,
|
||||
(height - y) / EDGE_FADE_DISTANCE,
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
||||
if (edgeFade <= 0) {
|
||||
@@ -293,15 +346,51 @@ export default function AnimatedBackgroundContainer({
|
||||
animationFrameRef.current = window.requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
animationFrameRef.current = window.requestAnimationFrame(draw);
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current !== null) {
|
||||
window.cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = null;
|
||||
const startAnimation = () => {
|
||||
if (
|
||||
animationFrameRef.current === null &&
|
||||
isVisibleRef.current &&
|
||||
!prefersReducedMotionRef.current
|
||||
) {
|
||||
animationFrameRef.current = window.requestAnimationFrame(draw);
|
||||
}
|
||||
};
|
||||
}, [mounted]);
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
isVisibleRef.current = document.visibilityState === "visible";
|
||||
|
||||
if (isVisibleRef.current) {
|
||||
startAnimation();
|
||||
} else {
|
||||
stopAnimation();
|
||||
}
|
||||
};
|
||||
|
||||
const motionMedia = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const handleMotionChange = () => {
|
||||
prefersReducedMotionRef.current = motionMedia.matches;
|
||||
|
||||
if (prefersReducedMotionRef.current) {
|
||||
stopAnimation();
|
||||
ctx.clearRect(0, 0, canvasSizeRef.current.width, canvasSizeRef.current.height);
|
||||
} else {
|
||||
startAnimation();
|
||||
}
|
||||
};
|
||||
|
||||
isVisibleRef.current = document.visibilityState === "visible";
|
||||
prefersReducedMotionRef.current = motionMedia.matches;
|
||||
startAnimation();
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
motionMedia.addEventListener("change", handleMotionChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
motionMedia.removeEventListener("change", handleMotionChange);
|
||||
stopAnimation();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
53
src/app/_components/Form/Fields/IntInputFormField.tsx
Normal file
53
src/app/_components/Form/Fields/IntInputFormField.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import type { Control, FieldValues, Path } from "react-hook-form";
|
||||
import { FormControl, FormField, FormItem, FormLabel } from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
|
||||
type IntInputFormFieldProps<T extends FieldValues> = Omit<
|
||||
ComponentProps<typeof Input>,
|
||||
"defaultValue" | "name" | "onChange" | "type" | "value"
|
||||
> & {
|
||||
control: Control<T>;
|
||||
emptyValue?: null | undefined;
|
||||
label: string;
|
||||
name: Path<T>;
|
||||
};
|
||||
|
||||
export default function IntInputFormField<T extends FieldValues>({
|
||||
control,
|
||||
emptyValue,
|
||||
label,
|
||||
name,
|
||||
...inputProps
|
||||
}: IntInputFormFieldProps<T>) {
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...inputProps}
|
||||
inputMode="numeric"
|
||||
onBlur={field.onBlur}
|
||||
onChange={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
field.onChange(
|
||||
value === "" ? emptyValue : Number.parseInt(value, 10),
|
||||
);
|
||||
}}
|
||||
placeholder={inputProps.placeholder ?? name}
|
||||
ref={field.ref}
|
||||
step={inputProps.step ?? 1}
|
||||
type="number"
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CheckedState } from "@radix-ui/react-checkbox";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { createContext,useContext, useState } from "react";
|
||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||
import { createContext,useContext, useState, type KeyboardEventHandler } from "react";
|
||||
import { useFormContext, type Control, type ControllerRenderProps, type FieldValues, type Path } from "react-hook-form";
|
||||
import type { Entries } from "type-fest";
|
||||
import { Button } from "~/components/ui/button";
|
||||
@@ -16,6 +17,7 @@ const MultiBooleanFieldContext = createContext<MultiBooleanFieldContextProps|und
|
||||
|
||||
function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0: string[]) => void }) {
|
||||
const context = useContext(MultiBooleanFieldContext)
|
||||
const [searchBuffer, setSearchBuffer] = useState<string>("")
|
||||
if (context === undefined) {
|
||||
return (<></>)
|
||||
}
|
||||
@@ -43,19 +45,40 @@ function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0
|
||||
}
|
||||
return context.checkedValues[key]
|
||||
}
|
||||
|
||||
const handleKeyDown:KeyboardEventHandler = (e) => {
|
||||
if (e.ctrlKey && e.key == "c") {
|
||||
setSearchBuffer("")
|
||||
} else if (e.code == "Backspace") {
|
||||
setSearchBuffer((prev) => {
|
||||
const newVal = prev.substring(0,prev.length - 2)
|
||||
console.log(newVal)
|
||||
return newVal
|
||||
})
|
||||
}
|
||||
|
||||
else if (e.key.length === 1) {
|
||||
setSearchBuffer((prev) => {
|
||||
const newVal = prev + e.key;
|
||||
console.log(newVal)
|
||||
return newVal;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollArea onKeyDown={handleKeyDown} className="flex h-60">
|
||||
{
|
||||
params.options.map((opt) => (
|
||||
<FormItem key={opt}>
|
||||
<div className="flex flex-row justify-between py-2 border-b-1">
|
||||
opt.startsWith(searchBuffer) && <FormItem key={opt}>
|
||||
<div className="flex flex-row justify-between py-2 border-b">
|
||||
<FormLabel>{opt}</FormLabel>
|
||||
<Checkbox data-testid="multiboolean-checkbox" checked={checked(opt)} onCheckedChange={onCheckedItemChange(opt)} />
|
||||
</div>
|
||||
</FormItem>
|
||||
))
|
||||
}
|
||||
</>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
export default function MultiBooleanFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, options: string[], defaultValues?: string[] }) {
|
||||
@@ -76,9 +99,9 @@ export default function MultiBooleanFormField<T extends FieldValues>(params: { c
|
||||
</PopoverTrigger>
|
||||
<FormControl>
|
||||
<PopoverContent data-testid="multiboolean-content">
|
||||
<MultiBooleanFieldContext.Provider value={{checkedValues: checkedValues, setCheckedValue: setCheckedValues}}>
|
||||
<InnerMultiBooleanFormField onChange={field.onChange} options={params.options} />
|
||||
</MultiBooleanFieldContext.Provider>
|
||||
<MultiBooleanFieldContext.Provider value={{checkedValues: checkedValues, setCheckedValue: setCheckedValues}}>
|
||||
<InnerMultiBooleanFormField onChange={field.onChange} options={params.options} />
|
||||
</MultiBooleanFieldContext.Provider>
|
||||
</PopoverContent>
|
||||
</FormControl>
|
||||
</Popover>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as BooleanFormField } from './BooleanFormField'
|
||||
export { default as TextInputFormField } from './TextInputFormField'
|
||||
export { default as IntInputFormField } from './IntInputFormField'
|
||||
export { default as MultiBooleanFormField } from './MultiBooleanFormField'
|
||||
export { default as SelectFormField } from './SelectFormField'
|
||||
export { default as MdeFormField } from './MdeFormField'
|
||||
|
||||
Reference in New Issue
Block a user