diff --git a/.gitignore b/.gitignore index 751513c..aa24275 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,11 @@ app.config.timestamp_*.js .classpath *.launch .settings/ +tasks # Temp gitignore +#*_migration_source # System Files .DS_Store diff --git a/public/BlackLogo.png b/public/BlackLogo.png new file mode 100644 index 0000000..f3d495d Binary files /dev/null and b/public/BlackLogo.png differ diff --git a/public/Cave/0.png b/public/Cave/0.png new file mode 100755 index 0000000..f3258bd Binary files /dev/null and b/public/Cave/0.png differ diff --git a/public/Cave/1.png b/public/Cave/1.png new file mode 100755 index 0000000..dccd408 Binary files /dev/null and b/public/Cave/1.png differ diff --git a/public/Cave/2.png b/public/Cave/2.png new file mode 100755 index 0000000..b0083b4 Binary files /dev/null and b/public/Cave/2.png differ diff --git a/public/Cave/3.png b/public/Cave/3.png new file mode 100755 index 0000000..ba83786 Binary files /dev/null and b/public/Cave/3.png differ diff --git a/public/Cave/4.png b/public/Cave/4.png new file mode 100755 index 0000000..afd4fbd Binary files /dev/null and b/public/Cave/4.png differ diff --git a/public/Cave/5.png b/public/Cave/5.png new file mode 100755 index 0000000..2b262c7 Binary files /dev/null and b/public/Cave/5.png differ diff --git a/public/Cave/6.png b/public/Cave/6.png new file mode 100755 index 0000000..215888b Binary files /dev/null and b/public/Cave/6.png differ diff --git a/public/Cave/7.png b/public/Cave/7.png new file mode 100755 index 0000000..5c65621 Binary files /dev/null and b/public/Cave/7.png differ diff --git a/public/LineageIcon.png b/public/LineageIcon.png new file mode 100644 index 0000000..27d7862 Binary files /dev/null and b/public/LineageIcon.png differ diff --git a/public/Mirror.png b/public/Mirror.png new file mode 100644 index 0000000..4b32edc Binary files /dev/null and b/public/Mirror.png differ diff --git a/public/StormyMountain/0.png b/public/StormyMountain/0.png new file mode 100755 index 0000000..28150f6 Binary files /dev/null and b/public/StormyMountain/0.png differ diff --git a/public/StormyMountain/1.png b/public/StormyMountain/1.png new file mode 100755 index 0000000..de7783f Binary files /dev/null and b/public/StormyMountain/1.png differ diff --git a/public/StormyMountain/10.png b/public/StormyMountain/10.png new file mode 100755 index 0000000..b272d92 Binary files /dev/null and b/public/StormyMountain/10.png differ diff --git a/public/StormyMountain/11.png b/public/StormyMountain/11.png new file mode 100755 index 0000000..5f0976c Binary files /dev/null and b/public/StormyMountain/11.png differ diff --git a/public/StormyMountain/2.png b/public/StormyMountain/2.png new file mode 100755 index 0000000..b2a58bc Binary files /dev/null and b/public/StormyMountain/2.png differ diff --git a/public/StormyMountain/3.png b/public/StormyMountain/3.png new file mode 100755 index 0000000..9f91684 Binary files /dev/null and b/public/StormyMountain/3.png differ diff --git a/public/StormyMountain/4.png b/public/StormyMountain/4.png new file mode 100755 index 0000000..e58d707 Binary files /dev/null and b/public/StormyMountain/4.png differ diff --git a/public/StormyMountain/5.png b/public/StormyMountain/5.png new file mode 100755 index 0000000..d96cb04 Binary files /dev/null and b/public/StormyMountain/5.png differ diff --git a/public/StormyMountain/6.png b/public/StormyMountain/6.png new file mode 100755 index 0000000..21a3830 Binary files /dev/null and b/public/StormyMountain/6.png differ diff --git a/public/StormyMountain/7.png b/public/StormyMountain/7.png new file mode 100755 index 0000000..55c75b1 Binary files /dev/null and b/public/StormyMountain/7.png differ diff --git a/public/StormyMountain/8.png b/public/StormyMountain/8.png new file mode 100755 index 0000000..d2430b9 Binary files /dev/null and b/public/StormyMountain/8.png differ diff --git a/public/StormyMountain/9.png b/public/StormyMountain/9.png new file mode 100755 index 0000000..790d89a Binary files /dev/null and b/public/StormyMountain/9.png differ diff --git a/public/StormyMountain/Stormy_Mountains_Rain.gif b/public/StormyMountain/Stormy_Mountains_Rain.gif new file mode 100755 index 0000000..934eb79 Binary files /dev/null and b/public/StormyMountain/Stormy_Mountains_Rain.gif differ diff --git a/public/WhiteLogo.png b/public/WhiteLogo.png new file mode 100644 index 0000000..4309156 Binary files /dev/null and b/public/WhiteLogo.png differ diff --git a/public/bitcoin.jpg b/public/bitcoin.jpg new file mode 100644 index 0000000..48b1a87 Binary files /dev/null and b/public/bitcoin.jpg differ diff --git a/public/blueprint.jpg b/public/blueprint.jpg new file mode 100644 index 0000000..87eae4c Binary files /dev/null and b/public/blueprint.jpg differ diff --git a/public/blur_SH_water.jpg b/public/blur_SH_water.jpg new file mode 100644 index 0000000..3b41cce Binary files /dev/null and b/public/blur_SH_water.jpg differ diff --git a/public/blur_SH_water_uncompressed.jpg b/public/blur_SH_water_uncompressed.jpg new file mode 100644 index 0000000..8d94447 Binary files /dev/null and b/public/blur_SH_water_uncompressed.jpg differ diff --git a/public/favicon.ico b/public/favicon.ico index fb282da..f384885 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/google-play-badge.png b/public/google-play-badge.png new file mode 100644 index 0000000..131f3ac Binary files /dev/null and b/public/google-play-badge.png differ diff --git a/public/just box.png b/public/just box.png new file mode 100644 index 0000000..2f5a6aa Binary files /dev/null and b/public/just box.png differ diff --git a/public/manhattan-night-skyline.jpg b/public/manhattan-night-skyline.jpg new file mode 100644 index 0000000..a74b4a3 Binary files /dev/null and b/public/manhattan-night-skyline.jpg differ diff --git a/public/me_in_flannel.jpg b/public/me_in_flannel.jpg new file mode 100644 index 0000000..05e9bc3 Binary files /dev/null and b/public/me_in_flannel.jpg differ diff --git a/public/pic01.jpg b/public/pic01.jpg new file mode 100644 index 0000000..2c4200c Binary files /dev/null and b/public/pic01.jpg differ diff --git a/public/pic02.jpg b/public/pic02.jpg new file mode 100644 index 0000000..34ac5e8 Binary files /dev/null and b/public/pic02.jpg differ diff --git a/public/pic03.jpg b/public/pic03.jpg new file mode 100644 index 0000000..db24317 Binary files /dev/null and b/public/pic03.jpg differ diff --git a/public/shapes-app-store-qr.jpg b/public/shapes-app-store-qr.jpg new file mode 100644 index 0000000..656a5c9 Binary files /dev/null and b/public/shapes-app-store-qr.jpg differ diff --git a/public/textures/grass/Grass_001_COLOR.JPG b/public/textures/grass/Grass_001_COLOR.JPG new file mode 100755 index 0000000..1cf1fba Binary files /dev/null and b/public/textures/grass/Grass_001_COLOR.JPG differ diff --git a/public/textures/grass/Grass_001_DISP.JPG b/public/textures/grass/Grass_001_DISP.JPG new file mode 100755 index 0000000..afdc1e6 Binary files /dev/null and b/public/textures/grass/Grass_001_DISP.JPG differ diff --git a/public/textures/grass/Grass_001_NRM.JPG b/public/textures/grass/Grass_001_NRM.JPG new file mode 100755 index 0000000..2e201aa Binary files /dev/null and b/public/textures/grass/Grass_001_NRM.JPG differ diff --git a/public/textures/grass/Grass_001_OCC.JPG b/public/textures/grass/Grass_001_OCC.JPG new file mode 100755 index 0000000..3d97e5c Binary files /dev/null and b/public/textures/grass/Grass_001_OCC.JPG differ diff --git a/public/textures/rocky_terrain/rocky_terrain_02_ao_1k.jpg b/public/textures/rocky_terrain/rocky_terrain_02_ao_1k.jpg new file mode 100755 index 0000000..929bf38 Binary files /dev/null and b/public/textures/rocky_terrain/rocky_terrain_02_ao_1k.jpg differ diff --git a/public/textures/rocky_terrain/rocky_terrain_02_arm_1k.jpg b/public/textures/rocky_terrain/rocky_terrain_02_arm_1k.jpg new file mode 100755 index 0000000..6c869b6 Binary files /dev/null and b/public/textures/rocky_terrain/rocky_terrain_02_arm_1k.jpg differ diff --git a/public/textures/rocky_terrain/rocky_terrain_02_diff_1k.jpg b/public/textures/rocky_terrain/rocky_terrain_02_diff_1k.jpg new file mode 100755 index 0000000..e39dab1 Binary files /dev/null and b/public/textures/rocky_terrain/rocky_terrain_02_diff_1k.jpg differ diff --git a/public/textures/rocky_terrain/rocky_terrain_02_disp_1k.png b/public/textures/rocky_terrain/rocky_terrain_02_disp_1k.png new file mode 100755 index 0000000..b1de248 Binary files /dev/null and b/public/textures/rocky_terrain/rocky_terrain_02_disp_1k.png differ diff --git a/public/textures/rocky_terrain/rocky_terrain_02_nor_gl_1k.jpg b/public/textures/rocky_terrain/rocky_terrain_02_nor_gl_1k.jpg new file mode 100755 index 0000000..3aadbf5 Binary files /dev/null and b/public/textures/rocky_terrain/rocky_terrain_02_nor_gl_1k.jpg differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_ao_2k.jpg b/public/textures/rocky_terrain_2/rocky_terrain_ao_2k.jpg new file mode 100755 index 0000000..3ac7516 Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_ao_2k.jpg differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_arm_2k.jpg b/public/textures/rocky_terrain_2/rocky_terrain_arm_2k.jpg new file mode 100755 index 0000000..fc1a2c2 Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_arm_2k.jpg differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_diff_2k.jpg b/public/textures/rocky_terrain_2/rocky_terrain_diff_2k.jpg new file mode 100755 index 0000000..bfaec16 Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_diff_2k.jpg differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_diff_2k.png b/public/textures/rocky_terrain_2/rocky_terrain_diff_2k.png new file mode 100755 index 0000000..d13a931 Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_diff_2k.png differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_disp_2k.jpg b/public/textures/rocky_terrain_2/rocky_terrain_disp_2k.jpg new file mode 100755 index 0000000..cbe65d4 Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_disp_2k.jpg differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_disp_2k.png b/public/textures/rocky_terrain_2/rocky_terrain_disp_2k.png new file mode 100755 index 0000000..f832b8c Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_disp_2k.png differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_nor_dx_2k.exr b/public/textures/rocky_terrain_2/rocky_terrain_nor_dx_2k.exr new file mode 100755 index 0000000..d4a8569 Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_nor_dx_2k.exr differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_nor_gl_2k.exr b/public/textures/rocky_terrain_2/rocky_terrain_nor_gl_2k.exr new file mode 100755 index 0000000..dc46cfd Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_nor_gl_2k.exr differ diff --git a/public/textures/rocky_terrain_2/rocky_terrain_rough_2k.exr b/public/textures/rocky_terrain_2/rocky_terrain_rough_2k.exr new file mode 100755 index 0000000..10ab171 Binary files /dev/null and b/public/textures/rocky_terrain_2/rocky_terrain_rough_2k.exr differ diff --git a/src/components/CountdownCircleTimer.tsx b/src/components/CountdownCircleTimer.tsx new file mode 100644 index 0000000..6f0188a --- /dev/null +++ b/src/components/CountdownCircleTimer.tsx @@ -0,0 +1,73 @@ +import { Component, createEffect, onCleanup } from "solid-js"; + +interface CountdownCircleTimerProps { + duration: number; + initialRemainingTime: number; + size: number; + strokeWidth: number; + colors: string; + children: () => any; +} + +const CountdownCircleTimer: Component = (props) => { + const radius = (props.size - props.strokeWidth) / 2; + const circumference = radius * 2 * Math.PI; + + // Calculate progress (0 to 1) + const progress = () => props.initialRemainingTime / props.duration; + const strokeDashoffset = () => circumference * (1 - progress()); + + return ( +
+ + {/* Background circle */} + + {/* Progress circle */} + + + {/* Timer text in center */} +
+ {props.children()} +
+
+ ); +}; + +export default CountdownCircleTimer; diff --git a/src/components/icons/Eye.tsx b/src/components/icons/Eye.tsx new file mode 100644 index 0000000..545748f --- /dev/null +++ b/src/components/icons/Eye.tsx @@ -0,0 +1,36 @@ +import { Component } from "solid-js"; + +interface EyeProps { + strokeWidth: number; + height: number; + width: number; + class?: string; +} + +const Eye: Component = (props) => { + return ( + + + + + ); +}; + +export default Eye; diff --git a/src/components/icons/EyeSlash.tsx b/src/components/icons/EyeSlash.tsx new file mode 100644 index 0000000..df2c2aa --- /dev/null +++ b/src/components/icons/EyeSlash.tsx @@ -0,0 +1,31 @@ +import { Component } from "solid-js"; + +interface EyeSlashProps { + strokeWidth: number; + height: number; + width: number; + class?: string; +} + +const EyeSlash: Component = (props) => { + return ( + + + + ); +}; + +export default EyeSlash; diff --git a/src/components/icons/GitHub.tsx b/src/components/icons/GitHub.tsx new file mode 100644 index 0000000..1bac3af --- /dev/null +++ b/src/components/icons/GitHub.tsx @@ -0,0 +1,24 @@ +import { Component } from "solid-js"; + +interface GitHubProps { + height?: string | number; + width?: string | number; + fill?: string; +} + +const GitHub: Component = (props) => { + return ( + + + + ); +}; + +export default GitHub; diff --git a/src/components/icons/GoogleLogo.tsx b/src/components/icons/GoogleLogo.tsx new file mode 100644 index 0000000..3e5703c --- /dev/null +++ b/src/components/icons/GoogleLogo.tsx @@ -0,0 +1,37 @@ +import { Component } from "solid-js"; + +interface GoogleLogoProps { + height: number; + width: number; +} + +const GoogleLogo: Component = (props) => { + return ( + + + + + + + + ); +}; + +export default GoogleLogo; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..27e7deb --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,90 @@ +import { JSX, splitProps, Show } from "solid-js"; + +export interface ButtonProps extends JSX.ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; + fullWidth?: boolean; +} + +/** + * Reusable button component with variants and loading state + */ +export default function Button(props: ButtonProps) { + const [local, others] = splitProps(props, [ + "variant", + "size", + "loading", + "fullWidth", + "class", + "children", + "disabled", + ]); + + const variant = () => local.variant || "primary"; + const size = () => local.size || "md"; + + const baseClasses = "flex justify-center items-center rounded font-semibold transition-all duration-300 ease-out disabled:opacity-50 disabled:cursor-not-allowed"; + + const variantClasses = () => { + switch (variant()) { + case "primary": + return "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700 text-white shadow-lg shadow-blue-300 dark:shadow-blue-700"; + case "secondary": + return "bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white"; + case "danger": + return "bg-red-500 hover:bg-red-600 active:scale-90 text-white shadow-lg shadow-red-300 dark:shadow-red-700"; + case "ghost": + return "bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300"; + default: + return ""; + } + }; + + const sizeClasses = () => { + switch (size()) { + case "sm": + return "px-3 py-1.5 text-sm"; + case "md": + return "px-4 py-2 text-base"; + case "lg": + return "px-6 py-3 text-lg"; + default: + return ""; + } + }; + + const widthClass = () => (local.fullWidth ? "w-full" : ""); + + return ( + + ); +} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..24f5152 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,45 @@ +import { JSX, splitProps } from "solid-js"; + +export interface InputProps extends JSX.InputHTMLAttributes { + label?: string; + error?: string; + helperText?: string; +} + +/** + * Reusable input component with label and error handling + * Styled to match Next.js migration source (underlined input style) + */ +export default function Input(props: InputProps) { + const [local, others] = splitProps(props, ["label", "error", "helperText", "class"]); + + return ( +
+ + + {local.label && ( + + )} + {local.error && ( + + {local.error} + + )} + {local.helperText && !local.error && ( + + {local.helperText} + + )} +
+ ); +} diff --git a/src/lib/SOLID-PATTERNS.md b/src/lib/SOLID-PATTERNS.md new file mode 100644 index 0000000..177a0fe --- /dev/null +++ b/src/lib/SOLID-PATTERNS.md @@ -0,0 +1,441 @@ +# React to SolidJS Conversion Patterns + +This guide documents common patterns for converting React code to SolidJS for this migration. + +## Table of Contents +- [State Management](#state-management) +- [Effects](#effects) +- [Refs](#refs) +- [Routing](#routing) +- [Conditional Rendering](#conditional-rendering) +- [Lists](#lists) +- [Forms](#forms) +- [Event Handlers](#event-handlers) + +## State Management + +### React (useState) +```tsx +import { useState } from "react"; + +const [count, setCount] = useState(0); +const [user, setUser] = useState(null); + +// Update +setCount(count + 1); +setCount(prev => prev + 1); +setUser({ ...user, name: "John" }); +``` + +### Solid (createSignal) +```tsx +import { createSignal } from "solid-js"; + +const [count, setCount] = createSignal(0); +const [user, setUser] = createSignal(null); + +// Update - note the function call to read value +setCount(count() + 1); +setCount(prev => prev + 1); +setUser({ ...user(), name: "John" }); + +// ⚠️ Important: Always call the signal to read its value +console.log(count()); // ✅ Correct +console.log(count); // ❌ Wrong - this is the function itself +``` + +## Effects + +### React (useEffect) +```tsx +import { useEffect } from "react"; + +// Run once on mount +useEffect(() => { + console.log("Mounted"); + + return () => { + console.log("Cleanup"); + }; +}, []); + +// Run when dependency changes +useEffect(() => { + console.log(count); +}, [count]); +``` + +### Solid (createEffect / onMount / onCleanup) +```tsx +import { createEffect, onMount, onCleanup } from "solid-js"; + +// Run once on mount +onMount(() => { + console.log("Mounted"); +}); + +// Cleanup +onCleanup(() => { + console.log("Cleanup"); +}); + +// Run when dependency changes (automatic tracking) +createEffect(() => { + console.log(count()); // Automatically tracks count signal +}); + +// ⚠️ Important: Effects automatically track any signal reads +// No dependency array needed! +``` + +## Refs + +### React (useRef) +```tsx +import { useRef } from "react"; + +const inputRef = useRef(null); + +// Access +inputRef.current?.focus(); + +// In JSX + +``` + +### Solid (let binding or signal) +```tsx +// Method 1: Direct binding (preferred for simple cases) +let inputRef: HTMLInputElement | undefined; + +// Access +inputRef?.focus(); + +// In JSX + + +// Method 2: Using a signal (for reactive refs) +import { createSignal } from "solid-js"; + +const [inputRef, setInputRef] = createSignal(); + +// Access +inputRef()?.focus(); + +// In JSX + +``` + +## Routing + +### React (Next.js) +```tsx +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +// Link component +About + +// Programmatic navigation +const router = useRouter(); +router.push("/dashboard"); +router.back(); +router.refresh(); +``` + +### Solid (SolidStart) +```tsx +import { A, useNavigate } from "@solidjs/router"; + +// Link component +About + +// Programmatic navigation +const navigate = useNavigate(); +navigate("/dashboard"); +navigate(-1); // Go back +// Note: No refresh() - Solid is reactive by default +``` + +## Conditional Rendering + +### React +```tsx +// Using && operator +{isLoggedIn && } + +// Using ternary +{isLoggedIn ? : } + +// Using if statement +if (loading) return ; +return ; +``` + +### Solid +```tsx +import { Show } from "solid-js"; + +// Using Show component (recommended) + + + + +// With fallback +}> + + + +// ⚠️ Important: Can still use && and ternary, but Show is more efficient +// because it doesn't recreate the DOM on every change + +// Early return still works +if (loading()) return ; +return ; +``` + +## Lists + +### React +```tsx +// Using map +{users.map(user => ( +
{user.name}
+))} + +// With index +{users.map((user, index) => ( +
{user.name}
+))} +``` + +### Solid +```tsx +import { For, Index } from "solid-js"; + +// Using For (when items have stable keys) + + {(user) =>
{user.name}
} +
+ +// With index + + {(user, index) =>
{index()} - {user.name}
} +
+ +// Using Index (when items have no stable identity, keyed by index) + + {(user, index) =>
{index} - {user().name}
} +
+ +// ⚠️ Key differences: +// - For: Better when items have stable identity (keyed by reference) +// - Index: Better when items change frequently (keyed by index) +// - Note the () on user in Index component +``` + +## Forms + +### React +```tsx +import { useState } from "react"; + +const [email, setEmail] = useState(""); + +const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log(email); +}; + +
+ setEmail(e.target.value)} + /> +
+``` + +### Solid +```tsx +import { createSignal } from "solid-js"; + +const [email, setEmail] = createSignal(""); + +const handleSubmit = (e: Event) => { + e.preventDefault(); + console.log(email()); +}; + +
+ setEmail(e.currentTarget.value)} + /> +
+ +// ⚠️ Important differences: +// - Use onInput instead of onChange for real-time updates +// - onChange fires on blur in Solid +// - Use e.currentTarget instead of e.target for type safety +``` + +## Event Handlers + +### React +```tsx +// Inline + + + } + > +
+ Don't have an account yet? + +
+ + + {/* Form */} +
+ {/* Email input */} +
+
+ + + +
+
+ + {/* Password input - shown for login with password or registration */} + +
+
+ + + +
+ +
+
+ Password too short! Min Length: 8 +
+
+ + {/* Password confirmation - shown only for registration */} + +
+
+ + + +
+ +
+
= 6 + ? "" + : "select-none opacity-0" + } text-center text-red-500 transition-opacity duration-200 ease-in-out`} + > + Passwords do not match! +
+
+ + {/* Remember Me checkbox */} +
+ +
Remember Me
+
+ + {/* Error/Success messages */} +
+ Credentials did not match any record + Login Success! Redirecting... +
+ + {/* Submit button or countdown timer */} +
+ 0} + fallback={ + + } + > + + {renderTime} + + + + {/* Toggle password/email link */} + + + + + + +
+
+ + {/* Password reset link */} + +
+ Trouble Logging In?{" "} + + Reset Password + +
+
+ + {/* Email sent confirmation */} +
+ Email Sent! +
+ + {/* Or divider */} +
Or
+ + {/* OAuth buttons */} + + + + ); +} diff --git a/src/routes/test-utils.tsx b/src/routes/test-utils.tsx new file mode 100644 index 0000000..bf18e1b --- /dev/null +++ b/src/routes/test-utils.tsx @@ -0,0 +1,169 @@ +import { createSignal } from "solid-js"; +import Input from "~/components/ui/Input"; +import Button from "~/components/ui/Button"; +import { isValidEmail, validatePassword, passwordsMatch } from "~/lib/validation"; + +/** + * Test page to validate Task 01 components and utilities + * Navigate to /test-utils to view + */ +export default function TestUtilsPage() { + const [email, setEmail] = createSignal(""); + const [password, setPassword] = createSignal(""); + const [passwordConf, setPasswordConf] = createSignal(""); + const [loading, setLoading] = createSignal(false); + + const emailError = () => { + if (!email()) return undefined; + return isValidEmail(email()) ? undefined : "Invalid email format"; + }; + + const passwordError = () => { + if (!password()) return undefined; + const validation = validatePassword(password()); + return validation.isValid ? undefined : validation.errors.join(", "); + }; + + const passwordMatchError = () => { + if (!passwordConf()) return undefined; + return passwordsMatch(password(), passwordConf()) + ? undefined + : "Passwords do not match"; + }; + + const handleSubmit = async (e: Event) => { + e.preventDefault(); + setLoading(true); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 2000)); + + alert(`Form submitted!\nEmail: ${email()}\nPassword: ${password()}`); + setLoading(false); + }; + + return ( +
+
+
+

Task 01 - Utility Testing

+

+ Testing shared utilities, types, and UI components +

+
+ +
+

Form Components & Validation

+ +
+ setEmail(e.currentTarget.value)} + error={emailError()} + helperText="Enter a valid email address" + required + /> + + setPassword(e.currentTarget.value)} + error={passwordError()} + helperText="Minimum 8 characters" + required + /> + + setPasswordConf(e.currentTarget.value)} + error={passwordMatchError()} + required + /> + +
+ + + + + + + +
+
+
+ +
+

Validation Status

+ +
+
+ + Email Valid: {isValidEmail(email()) ? "✓" : "✗"} +
+ +
+ + Password Valid: {validatePassword(password()).isValid ? "✓" : "✗"} +
+ +
+ + Passwords Match: {passwordsMatch(password(), passwordConf()) ? "✓" : "✗"} +
+
+
+ +
+

✅ Task 01 Complete

+
    +
  • ✓ User types created
  • +
  • ✓ Cookie utilities created
  • +
  • ✓ Validation helpers created
  • +
  • ✓ Input component created
  • +
  • ✓ Button component created
  • +
  • ✓ Conversion patterns documented
  • +
  • ✓ Build successful
  • +
+
+
+
+ ); +} diff --git a/src/routes/test.tsx b/src/routes/test.tsx index 6d2079a..4665e82 100644 --- a/src/routes/test.tsx +++ b/src/routes/test.tsx @@ -5,78 +5,854 @@ type EndpointTest = { router: string; procedure: string; method: "query" | "mutation"; - input?: object; + sampleInput?: object; description: string; + requiresAuth?: boolean; + requiresAdmin?: boolean; }; -const endpoints: EndpointTest[] = [ - // JSON Service (no input needed) +type RouterSection = { + name: string; + description: string; + endpoints: EndpointTest[]; +}; + +const routerSections: RouterSection[] = [ + // ============================================================ + // Example Router + // ============================================================ { - name: "Get Attacks", - router: "lineage.jsonService", - procedure: "attacks", - method: "query", - description: "Get all attack data", - }, - { - name: "Get Conditions", - router: "lineage.jsonService", - procedure: "conditions", - method: "query", - description: "Get all condition data", - }, - { - name: "Get Dungeons", - router: "lineage.jsonService", - procedure: "dungeons", - method: "query", - description: "Get all dungeon data", - }, - { - name: "Get Enemies", - router: "lineage.jsonService", - procedure: "enemies", - method: "query", - description: "Get all enemy data", - }, - { - name: "Get Items", - router: "lineage.jsonService", - procedure: "items", - method: "query", - description: "Get all item data", - }, - { - name: "Get Misc", - router: "lineage.jsonService", - procedure: "misc", - method: "query", - description: "Get all misc data", + name: "Example Router", + description: "Example endpoints demonstrating public, protected, and admin procedures", + endpoints: [ + { + name: "Hello", + router: "example", + procedure: "hello", + method: "query", + description: "Simple hello world endpoint", + sampleInput: "World", + }, + { + name: "Get Profile", + router: "example", + procedure: "getProfile", + method: "query", + description: "Get authenticated user profile", + requiresAuth: true, + }, + { + name: "Admin Dashboard", + router: "example", + procedure: "adminDashboard", + method: "query", + description: "Access admin dashboard", + requiresAdmin: true, + }, + ], }, - // Misc + // ============================================================ + // Auth Router + // ============================================================ { - name: "Offline Secret", - router: "lineage.misc", - procedure: "offlineSecret", - method: "query", - description: "Get offline serialization secret", + name: "Auth Router", + description: "OAuth callbacks and email-based authentication", + endpoints: [ + { + name: "Email Registration", + router: "auth", + procedure: "emailRegistration", + method: "mutation", + description: "Register new user with email/password", + sampleInput: { + email: "newuser@example.com", + password: "SecurePass123!", + passwordConfirmation: "SecurePass123!", + }, + }, + { + name: "Email Password Login", + router: "auth", + procedure: "emailPasswordLogin", + method: "mutation", + description: "Login with email and password", + sampleInput: { + email: "test@example.com", + password: "SecurePass123!", + rememberMe: true, + }, + }, + { + name: "Request Email Link Login", + router: "auth", + procedure: "requestEmailLinkLogin", + method: "mutation", + description: "Request magic link login email", + sampleInput: { + email: "test@example.com", + rememberMe: false, + }, + }, + { + name: "Email Login (with token)", + router: "auth", + procedure: "emailLogin", + method: "mutation", + description: "Complete magic link login", + sampleInput: { + email: "test@example.com", + token: "eyJhbGciOiJIUzI1NiJ9...", + rememberMe: true, + }, + }, + { + name: "Request Password Reset", + router: "auth", + procedure: "requestPasswordReset", + method: "mutation", + description: "Send password reset email", + sampleInput: { + email: "test@example.com", + }, + }, + { + name: "Reset Password", + router: "auth", + procedure: "resetPassword", + method: "mutation", + description: "Reset password with token from email", + sampleInput: { + token: "eyJhbGciOiJIUzI1NiJ9...", + newPassword: "NewSecurePass123!", + newPasswordConfirmation: "NewSecurePass123!", + }, + }, + { + name: "Resend Email Verification", + router: "auth", + procedure: "resendEmailVerification", + method: "mutation", + description: "Resend verification email", + sampleInput: { + email: "test@example.com", + }, + }, + { + name: "Email Verification", + router: "auth", + procedure: "emailVerification", + method: "mutation", + description: "Verify email with token", + sampleInput: { + email: "test@example.com", + token: "eyJhbGciOiJIUzI1NiJ9...", + }, + }, + { + name: "Sign Out", + router: "auth", + procedure: "signOut", + method: "mutation", + description: "Clear session cookies and sign out", + }, + { + name: "GitHub Callback", + router: "auth", + procedure: "githubCallback", + method: "mutation", + description: "Complete GitHub OAuth flow", + sampleInput: { code: "github_oauth_code_here" }, + }, + { + name: "Google Callback", + router: "auth", + procedure: "googleCallback", + method: "mutation", + description: "Complete Google OAuth flow", + sampleInput: { code: "google_oauth_code_here" }, + }, + ], }, - // PvP + // ============================================================ + // Database Router + // ============================================================ { - name: "Get Opponents", - router: "lineage.pvp", - procedure: "getOpponents", - method: "query", - description: "Get 3 random PvP opponents", + name: "Database - Comment Reactions", + description: "Add/remove reactions to comments", + endpoints: [ + { + name: "Get Comment Reactions", + router: "database", + procedure: "getCommentReactions", + method: "query", + description: "Get all reactions for a comment", + sampleInput: { commentID: "comment_123" }, + }, + { + name: "Add Comment Reaction", + router: "database", + procedure: "addCommentReaction", + method: "mutation", + description: "Add a reaction to a comment", + sampleInput: { + type: "👍", + comment_id: "comment_123", + user_id: "user_123", + }, + }, + { + name: "Remove Comment Reaction", + router: "database", + procedure: "removeCommentReaction", + method: "mutation", + description: "Remove a reaction from a comment", + sampleInput: { + type: "👍", + comment_id: "comment_123", + user_id: "user_123", + }, + }, + ], + }, + { + name: "Database - Comments", + description: "Fetch comments for posts", + endpoints: [ + { + name: "Get All Comments", + router: "database", + procedure: "getAllComments", + method: "query", + description: "Fetch all comments in the system", + }, + { + name: "Get Comments by Post ID", + router: "database", + procedure: "getCommentsByPostId", + method: "query", + description: "Fetch comments for a specific post", + sampleInput: { post_id: "post_123" }, + }, + ], + }, + { + name: "Database - Posts", + description: "CRUD operations for blog/project posts", + endpoints: [ + { + name: "Get Post by ID", + router: "database", + procedure: "getPostById", + method: "query", + description: "Fetch a post by ID with tags", + sampleInput: { category: "blog", id: 1 }, + }, + { + name: "Get Post by Title", + router: "database", + procedure: "getPostByTitle", + method: "query", + description: "Fetch post with comments, likes, and tags", + sampleInput: { category: "project", title: "My Project" }, + }, + { + name: "Create Post", + router: "database", + procedure: "createPost", + method: "mutation", + description: "Create a new blog/project post", + sampleInput: { + category: "blog", + title: "Test Post", + subtitle: "A test subtitle", + body: "Post content here", + banner_photo: null, + published: false, + tags: ["tech", "coding"], + author_id: "user_123", + }, + }, + { + name: "Update Post", + router: "database", + procedure: "updatePost", + method: "mutation", + description: "Update an existing post", + sampleInput: { + id: 1, + title: "Updated Title", + published: true, + author_id: "user_123", + }, + }, + ], + }, + { + name: "Database - Post Likes", + description: "Like/unlike posts", + endpoints: [ + { + name: "Add Post Like", + router: "database", + procedure: "addPostLike", + method: "mutation", + description: "Add a like to a post", + sampleInput: { user_id: "user_123", post_id: "post_123" }, + }, + { + name: "Remove Post Like", + router: "database", + procedure: "removePostLike", + method: "mutation", + description: "Remove a like from a post", + sampleInput: { user_id: "user_123", post_id: "post_123" }, + }, + ], + }, + { + name: "Database - Users", + description: "User profile and data management", + endpoints: [ + { + name: "Get User by ID", + router: "database", + procedure: "getUserById", + method: "query", + description: "Fetch complete user data by ID", + sampleInput: { id: "user_123" }, + }, + { + name: "Get User Public Data", + router: "database", + procedure: "getUserPublicData", + method: "query", + description: "Fetch public user data (email, name, image)", + sampleInput: { id: "user_123" }, + }, + { + name: "Get User Image", + router: "database", + procedure: "getUserImage", + method: "query", + description: "Fetch user image URL", + sampleInput: { id: "user_123" }, + }, + { + name: "Update User Image", + router: "database", + procedure: "updateUserImage", + method: "mutation", + description: "Update user profile image", + sampleInput: { id: "user_123", imageURL: "path/to/image.jpg" }, + }, + { + name: "Update User Email", + router: "database", + procedure: "updateUserEmail", + method: "mutation", + description: "Update user email address", + sampleInput: { + id: "user_123", + newEmail: "new@example.com", + oldEmail: "old@example.com", + }, + }, + ], + }, + + // ============================================================ + // User Router + // ============================================================ + { + name: "User Router", + description: "User profile management and account operations", + endpoints: [ + { + name: "Get Profile", + router: "user", + procedure: "getProfile", + method: "query", + description: "Get current user's profile", + requiresAuth: true, + }, + { + name: "Update Email", + router: "user", + procedure: "updateEmail", + method: "mutation", + description: "Update user's email address", + sampleInput: { email: "newemail@example.com" }, + requiresAuth: true, + }, + { + name: "Update Display Name", + router: "user", + procedure: "updateDisplayName", + method: "mutation", + description: "Update user's display name", + sampleInput: { displayName: "New Display Name" }, + requiresAuth: true, + }, + { + name: "Update Profile Image", + router: "user", + procedure: "updateProfileImage", + method: "mutation", + description: "Update user's profile image URL", + sampleInput: { imageUrl: "https://example.com/image.jpg" }, + requiresAuth: true, + }, + { + name: "Change Password", + router: "user", + procedure: "changePassword", + method: "mutation", + description: "Change password (requires old password)", + sampleInput: { + oldPassword: "oldpass123", + newPassword: "newpass123", + newPasswordConfirmation: "newpass123", + }, + requiresAuth: true, + }, + { + name: "Set Password", + router: "user", + procedure: "setPassword", + method: "mutation", + description: "Set password for OAuth users", + sampleInput: { + newPassword: "newpass123", + newPasswordConfirmation: "newpass123", + }, + requiresAuth: true, + }, + { + name: "Delete Account", + router: "user", + procedure: "deleteAccount", + method: "mutation", + description: "Delete account (anonymize user data)", + sampleInput: { password: "mypassword123" }, + requiresAuth: true, + }, + ], + }, + + // ============================================================ + // Misc Router + // ============================================================ + { + name: "Misc - Downloads", + description: "Generate signed URLs for downloadable assets", + endpoints: [ + { + name: "Get Download URL", + router: "misc", + procedure: "getDownloadUrl", + method: "query", + description: "Get signed S3 URL for asset download", + sampleInput: { asset_name: "shapes-with-abigail" }, + }, + ], + }, + { + name: "Misc - S3 Operations", + description: "S3 image upload/delete operations", + endpoints: [ + { + name: "Get Pre-Signed URL", + router: "misc", + procedure: "getPreSignedURL", + method: "mutation", + description: "Get signed URL for S3 upload", + sampleInput: { + type: "blog", + title: "my-post", + filename: "image.jpg", + }, + }, + { + name: "Delete Image", + router: "misc", + procedure: "deleteImage", + method: "mutation", + description: "Delete image from S3 and update DB", + sampleInput: { + key: "blog/my-post/image.jpg", + newAttachmentString: "[]", + type: "Post", + id: 1, + }, + }, + { + name: "Simple Delete Image", + router: "misc", + procedure: "simpleDeleteImage", + method: "mutation", + description: "Delete image from S3 only", + sampleInput: { key: "blog/my-post/image.jpg" }, + }, + ], + }, + { + name: "Misc - Password Utilities", + description: "Password hashing and verification", + endpoints: [ + { + name: "Hash Password", + router: "misc", + procedure: "hashPassword", + method: "mutation", + description: "Hash a password with bcrypt", + sampleInput: { password: "mypassword123" }, + }, + { + name: "Check Password", + router: "misc", + procedure: "checkPassword", + method: "mutation", + description: "Verify password against hash", + sampleInput: { + password: "mypassword123", + hash: "$2b$10$...", + }, + }, + ], + }, + + // ============================================================ + // Lineage Router + // ============================================================ + { + name: "Lineage - JSON Service", + description: "Static game data - no authentication required", + endpoints: [ + { + name: "Get Items", + router: "lineage.jsonService", + procedure: "items", + method: "query", + description: "Get all item data (weapons, armor, potions, etc.)", + }, + { + name: "Get Attacks", + router: "lineage.jsonService", + procedure: "attacks", + method: "query", + description: "Get all attack and spell data", + }, + { + name: "Get Conditions", + router: "lineage.jsonService", + procedure: "conditions", + method: "query", + description: "Get all condition and debilitation data", + }, + { + name: "Get Dungeons", + router: "lineage.jsonService", + procedure: "dungeons", + method: "query", + description: "Get all dungeon and encounter data", + }, + { + name: "Get Enemies", + router: "lineage.jsonService", + procedure: "enemies", + method: "query", + description: "Get all enemy and boss data", + }, + { + name: "Get Misc", + router: "lineage.jsonService", + procedure: "misc", + method: "query", + description: "Get misc game data (jobs, activities, etc.)", + }, + ], + }, + { + name: "Lineage - Authentication", + description: "User registration and login endpoints", + endpoints: [ + { + name: "Email Registration", + router: "lineage.auth", + procedure: "emailRegistration", + method: "mutation", + description: "Register new user with email/password", + sampleInput: { + email: "test@example.com", + password: "password123", + password_conf: "password123", + }, + }, + { + name: "Email Login", + router: "lineage.auth", + procedure: "emailLogin", + method: "mutation", + description: "Login with email/password (requires verified email)", + sampleInput: { email: "test@example.com", password: "password123" }, + requiresAuth: true, + }, + { + name: "Email Verification", + router: "lineage.auth", + procedure: "emailVerification", + method: "mutation", + description: "Verify email with token from email", + sampleInput: { token: "eyJhbGciOiJIUzI1NiJ9..." }, + }, + { + name: "Refresh Verification Email", + router: "lineage.auth", + procedure: "refreshVerification", + method: "mutation", + description: "Resend verification email", + sampleInput: { email: "test@example.com" }, + }, + { + name: "Refresh Auth Token", + router: "lineage.auth", + procedure: "refreshToken", + method: "mutation", + description: "Refresh expired JWT token", + sampleInput: { + email: "test@example.com", + authToken: "eyJhbGciOiJIUzI1NiJ9...", + }, + requiresAuth: true, + }, + { + name: "Google Registration", + router: "lineage.auth", + procedure: "googleRegistration", + method: "mutation", + description: "Register/login with Google OAuth", + sampleInput: { idToken: "google_id_token_here" }, + }, + { + name: "Apple Registration", + router: "lineage.auth", + procedure: "appleRegistration", + method: "mutation", + description: "Register/login with Apple Sign In", + sampleInput: { + userString: "apple_user_string_here", + email: "user@privaterelay.appleid.com", + }, + }, + { + name: "Apple Get Email", + router: "lineage.auth", + procedure: "appleGetEmail", + method: "mutation", + description: "Get email from Apple user string", + sampleInput: { userString: "apple_user_string_here" }, + }, + ], + }, + { + name: "Lineage - Database Management", + description: "User database credentials and deletion workflow", + endpoints: [ + { + name: "Get Credentials", + router: "lineage.database", + procedure: "credentials", + method: "mutation", + description: "Get per-user database credentials", + sampleInput: { + email: "test@example.com", + provider: "email", + authToken: "jwt_token_here", + }, + requiresAuth: true, + }, + { + name: "Init Deletion", + router: "lineage.database", + procedure: "deletionInit", + method: "mutation", + description: "Start 24hr database deletion countdown", + sampleInput: { + email: "test@example.com", + db_name: "db_name", + db_token: "db_token", + authToken: "jwt_token", + }, + requiresAuth: true, + }, + { + name: "Check Deletion Status", + router: "lineage.database", + procedure: "deletionCheck", + method: "mutation", + description: "Check if deletion is scheduled", + sampleInput: { + email: "test@example.com", + db_name: "db_name", + db_token: "db_token", + authToken: "jwt_token", + }, + requiresAuth: true, + }, + { + name: "Cancel Deletion", + router: "lineage.database", + procedure: "deletionCancel", + method: "mutation", + description: "Cancel scheduled database deletion", + sampleInput: { + email: "test@example.com", + db_name: "db_name", + db_token: "db_token", + authToken: "jwt_token", + }, + requiresAuth: true, + }, + { + name: "Deletion Cron", + router: "lineage.database", + procedure: "deletionCron", + method: "query", + description: "Cleanup expired databases (runs via cron)", + }, + ], + }, + { + name: "Lineage - PvP", + description: "Player vs Player matchmaking and battle system", + endpoints: [ + { + name: "Register Character", + router: "lineage.pvp", + procedure: "registerCharacter", + method: "mutation", + description: "Register/update character for PvP", + sampleInput: { + character: { + playerClass: "Mage", + name: "TestMage", + maxHealth: 100, + maxSanity: 100, + maxMana: 150, + baseManaRegen: 10, + strength: 5, + intelligence: 15, + dexterity: 10, + resistanceTable: "{}", + damageTable: "{}", + attackStrings: "[]", + knownSpells: "[]", + }, + linkID: "unique_player_id_here", + }, + }, + { + name: "Get Opponents", + router: "lineage.pvp", + procedure: "getOpponents", + method: "query", + description: "Get 3 random PvP opponents", + }, + { + name: "Record Battle Result", + router: "lineage.pvp", + procedure: "battleResult", + method: "mutation", + description: "Record PvP battle outcome", + sampleInput: { + winnerID: "player_id_1", + loserID: "player_id_2", + }, + }, + ], + }, + { + name: "Lineage - Misc", + description: "Analytics, device tokens, and utility endpoints", + endpoints: [ + { + name: "Track Analytics", + router: "lineage.misc", + procedure: "analytics", + method: "mutation", + description: "Store player analytics data", + sampleInput: { + playerID: "player_123", + dungeonProgression: { dungeon_1: 5 }, + playerClass: "Mage", + spellCount: 10, + proficiencies: { fire: 3 }, + jobs: { blacksmith: 2 }, + resistanceTable: { fire: 10 }, + damageTable: { physical: 5 }, + }, + }, + { + name: "Update Device Token", + router: "lineage.misc", + procedure: "tokens", + method: "mutation", + description: "Register/update push notification token", + sampleInput: { token: "device_push_token_here" }, + }, + { + name: "Offline Secret", + router: "lineage.misc", + procedure: "offlineSecret", + method: "query", + description: "Get offline serialization secret", + }, + ], + }, + { + name: "Lineage - Maintenance (Admin Only)", + description: "Database cleanup and administrative endpoints", + endpoints: [ + { + name: "Find Loose Databases", + router: "lineage.maintenance", + procedure: "findLooseDatabases", + method: "query", + description: "Find orphaned databases not linked to users", + requiresAdmin: true, + }, + { + name: "Cleanup Expired Databases", + router: "lineage.maintenance", + procedure: "cleanupExpiredDatabases", + method: "query", + description: "Delete databases past 24hr deletion window", + requiresAdmin: true, + }, + ], }, ]; export default function TestPage() { + const [expandedSections, setExpandedSections] = createSignal>( + new Set(), + ); const [results, setResults] = createSignal>({}); const [loading, setLoading] = createSignal>({}); const [errors, setErrors] = createSignal>({}); + const [inputEdits, setInputEdits] = createSignal>({}); + + const toggleSection = (sectionName: string) => { + const expanded = new Set(expandedSections()); + if (expanded.has(sectionName)) { + expanded.delete(sectionName); + } else { + expanded.add(sectionName); + } + setExpandedSections(expanded); + }; const testEndpoint = async (endpoint: EndpointTest) => { const key = `${endpoint.router}.${endpoint.procedure}`; @@ -84,12 +860,37 @@ export default function TestPage() { setErrors({ ...errors(), [key]: "" }); try { - const url = `/api/trpc/${endpoint.router}.${endpoint.procedure}`; - const response = await fetch(url, { + // Get input - either from edited JSON or sample + let input = endpoint.sampleInput; + const editedInput = inputEdits()[key]; + if (editedInput) { + try { + // Try to parse as JSON (handles objects, arrays, strings in quotes, numbers, booleans) + input = JSON.parse(editedInput); + } catch (e) { + throw new Error("Invalid JSON in input field"); + } + } + + let url = `/api/trpc/${endpoint.router}.${endpoint.procedure}`; + const options: RequestInit = { method: endpoint.method === "query" ? "GET" : "POST", - headers: endpoint.input ? { "Content-Type": "application/json" } : {}, - body: endpoint.input ? JSON.stringify(endpoint.input) : undefined, - }); + headers: {}, + }; + + // For queries, input goes in URL parameter + if (endpoint.method === "query" && input !== undefined) { + const encodedInput = encodeURIComponent(JSON.stringify(input)); + url += `?input=${encodedInput}`; + } + + // For mutations, input goes in body + if (endpoint.method === "mutation" && input !== undefined) { + options.headers = { "Content-Type": "application/json" }; + options.body = JSON.stringify(input); + } + + const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); @@ -104,69 +905,164 @@ export default function TestPage() { } }; - const testAll = async () => { - for (const endpoint of endpoints) { - await testEndpoint(endpoint); - // Small delay to avoid overwhelming the server - await new Promise((resolve) => setTimeout(resolve, 100)); - } + const updateInput = (key: string, value: string) => { + setInputEdits({ ...inputEdits(), [key]: value }); }; return (
-

Lineage API Testing Dashboard

-

Test all migrated tRPC endpoints

+

tRPC API Testing Dashboard

+

+ Complete API coverage: Example, Auth, Database, User, Misc, and Lineage routers +

- +
+

+ Quick Start: Expand any section below to test + endpoints. Public endpoints work immediately. Auth-required + endpoints need valid tokens. +

+
- - {(endpoint) => { - const key = `${endpoint.router}.${endpoint.procedure}`; + + {(section) => { + const isExpanded = () => expandedSections().has(section.name); + return ( -
-
-
-

{endpoint.name}

-

- {endpoint.description} +

+ {/* Section Header */} + -
- - -
-

Error:

-

{errors()[key]}

+
+ {isExpanded() ? "−" : "+"}
- + - -
-

- Response: -

-
-                        {JSON.stringify(results()[key], null, 2)}
-                      
+ {/* Section Content */} + +
+ + {(endpoint) => { + const key = `${endpoint.router}.${endpoint.procedure}`; + const hasInput = endpoint.sampleInput !== undefined; + const displayInput = () => { + if (inputEdits()[key]) { + return inputEdits()[key]; + } + // Handle primitive values (string, number, boolean) + if (typeof endpoint.sampleInput === 'string') { + return `"${endpoint.sampleInput}"`; + } + if (typeof endpoint.sampleInput === 'number' || typeof endpoint.sampleInput === 'boolean') { + return String(endpoint.sampleInput); + } + // Handle objects and arrays + return JSON.stringify(endpoint.sampleInput, null, 2); + }; + + return ( +
+ {/* Endpoint Header */} +
+
+
+

+ {endpoint.name} +

+ + + 🔒 Auth Required + + + + + 👑 Admin Only + + +
+

+ {endpoint.description} +

+
+ + {key} + + + {endpoint.method === "query" + ? "GET" + : "POST"} + +
+
+ +
+ + {/* Input Editor */} + +
+ +