From 7479378dd15fbeff3d030b651bd900f569d5ed97 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 17 Dec 2025 01:11:14 -0500 Subject: [PATCH] most done, outside the blog --- .gitignore | 2 +- bun.lockb | Bin 318328 -> 325156 bytes package.json | 2 + src/app.tsx | 37 +- src/components/SimpleParallax.tsx | 243 +++++++ src/components/icons/DownloadOnAppStore.tsx | 71 ++ .../icons/DownloadOnAppStoreDark.tsx | 129 ++++ src/components/icons/LinkedIn.tsx | 26 + src/lib/s3upload.ts | 52 ++ src/routes/account.tsx | 624 ++++++++++++++++++ src/routes/downloads.tsx | 167 +++++ src/routes/{login.tsx => login/index.tsx} | 0 src/routes/login/password-reset.tsx | 321 +++++++++ src/routes/login/request-password-reset.tsx | 204 ++++++ src/routes/marketing/life-and-lineage.tsx | 46 ++ .../privacy-policy/life-and-lineage.tsx | 112 ++++ .../privacy-policy/shapes-with-abigail.tsx | 98 +++ 17 files changed, 2116 insertions(+), 18 deletions(-) create mode 100644 src/components/SimpleParallax.tsx create mode 100644 src/components/icons/DownloadOnAppStore.tsx create mode 100644 src/components/icons/DownloadOnAppStoreDark.tsx create mode 100644 src/components/icons/LinkedIn.tsx create mode 100644 src/lib/s3upload.ts create mode 100644 src/routes/account.tsx create mode 100644 src/routes/downloads.tsx rename src/routes/{login.tsx => login/index.tsx} (100%) create mode 100644 src/routes/login/password-reset.tsx create mode 100644 src/routes/login/request-password-reset.tsx create mode 100644 src/routes/marketing/life-and-lineage.tsx create mode 100644 src/routes/privacy-policy/life-and-lineage.tsx create mode 100644 src/routes/privacy-policy/shapes-with-abigail.tsx diff --git a/.gitignore b/.gitignore index aa24275..c29c3bc 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ tasks # Temp gitignore -#*_migration_source +*_migration_source # System Files .DS_Store diff --git a/bun.lockb b/bun.lockb index 22c05e5f4ed671c2138dfbe4b38ac488117fa14d..47337a0cafacc19d156c8ecd77f21cb0dd1d8bd8 100755 GIT binary patch delta 57193 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 F{~J=qa#R2S delta 53246 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 j|L>B#{VdN|)A-evE=jK6Tk3vKOR_CnCwZh -
- - ( -
- -
- {props.children} + + +
+ + ( +
+ +
+ {props.children} +
+
- -
- )} - > - - -
- + )} + > + + +
+ + ); } diff --git a/src/components/SimpleParallax.tsx b/src/components/SimpleParallax.tsx new file mode 100644 index 0000000..531b75c --- /dev/null +++ b/src/components/SimpleParallax.tsx @@ -0,0 +1,243 @@ +import { createSignal, createEffect, onMount, onCleanup, children as resolveChildren, type ParentComponent, createMemo, For } from "solid-js"; +import { animate } from "motion"; + +type ParallaxBackground = { + imageSet: { [key: number]: string }; + size: { width: number; height: number }; + verticalOffset: number; +}; + +type ParallaxLayerProps = { + layer: number; + caveParallax: ParallaxBackground; + dimensions: { width: number; height: number }; + scale: number; + scaledWidth: number; + scaledHeight: number; + verticalOffsetPixels: number; + imagesNeeded: number; + direction: number; +}; + +function ParallaxLayer(props: ParallaxLayerProps) { + let containerRef: HTMLDivElement | undefined; + + const layerDepthFactor = createMemo(() => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1)); + const layerVerticalOffset = createMemo(() => props.verticalOffsetPixels * layerDepthFactor()); + const speed = createMemo(() => (120 - props.layer * 10) * 1000); + const targetX = createMemo(() => props.direction * -props.caveParallax.size.width * props.imagesNeeded); + + const containerStyle = createMemo(() => ({ + width: `${props.caveParallax.size.width * props.imagesNeeded * 3}px`, + height: `${props.caveParallax.size.height}px`, + left: `${(props.dimensions.width - props.scaledWidth) / 2}px`, + top: `${(props.dimensions.height - props.scaledHeight) / 2 + layerVerticalOffset()}px`, + "transform-origin": "center center", + "will-change": "transform", + })); + + // Set up animation when component mounts or when direction/speed changes + createEffect(() => { + if (!containerRef) return; + + const target = targetX(); + const duration = speed() / 1000; + + const controls = animate( + containerRef, + { + transform: [ + `translateX(0px) scale(${props.scale})`, + `translateX(${target}px) scale(${props.scale})` + ] + }, + { + duration, + easing: "linear", + repeat: Infinity, + } + ); + + onCleanup(() => controls.stop()); + }); + + const imageGroups = createMemo(() => { + return [-1, 0, 1].map((groupOffset) => ( +
+ {Array.from({ length: props.imagesNeeded }).map((_, index) => ( +
+ {`Parallax Object.keys(props.caveParallax.imageSet).length - 3 ? "eager" : "lazy"} + /> +
+ ))} +
+ )); + }); + + return ( +
+ {imageGroups()} +
+ ); +} + +const SimpleParallax: ParentComponent = (props) => { + let containerRef: HTMLDivElement | undefined; + const [dimensions, setDimensions] = createSignal({ width: 0, height: 0 }); + const [direction, setDirection] = createSignal(1); + + const caveParallax = createMemo(() => ({ + imageSet: { + 0: "/Cave/0.png", + 1: "/Cave/1.png", + 2: "/Cave/2.png", + 3: "/Cave/3.png", + 4: "/Cave/4.png", + 5: "/Cave/5.png", + 6: "/Cave/6.png", + 7: "/Cave/7.png", + }, + size: { width: 384, height: 216 }, + verticalOffset: 0.4, + })); + + const layerCount = createMemo(() => Object.keys(caveParallax().imageSet).length - 1); + const imagesNeeded = 3; + + const updateDimensions = () => { + if (containerRef) { + setDimensions({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + }; + + onMount(() => { + let timeoutId: ReturnType; + + const handleResize = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(updateDimensions, 100); + }; + + updateDimensions(); + window.addEventListener("resize", handleResize); + + const intervalId = setInterval(() => { + setDirection((prev) => prev * -1); + }, 30000); + + onCleanup(() => { + clearTimeout(timeoutId); + clearInterval(intervalId); + window.removeEventListener("resize", handleResize); + }); + }); + + const calculations = createMemo(() => { + const dims = dimensions(); + if (dims.width === 0) { + return { + scale: 0, + scaledWidth: 0, + scaledHeight: 0, + verticalOffsetPixels: 0, + }; + } + + const cave = caveParallax(); + const scaleHeight = dims.height / cave.size.height; + const scaleWidth = dims.width / cave.size.width; + const scale = Math.max(scaleHeight, scaleWidth) * 1.21; + + return { + scale, + scaledWidth: cave.size.width * scale, + scaledHeight: cave.size.height * scale, + verticalOffsetPixels: cave.verticalOffset * dims.height, + }; + }); + + const parallaxLayers = createMemo(() => { + const dims = dimensions(); + if (dims.width === 0) return null; + + const calc = calculations(); + const cave = caveParallax(); + const dir = direction(); + + return Array.from({ length: layerCount() }).map((_, i) => { + const layerIndex = layerCount() - i; + return ( + + ); + }); + }); + + const resolved = resolveChildren(() => props.children); + + return ( +
+
+
+ {parallaxLayers()} +
+
{resolved()}
+ +
+ ); +}; + +export default SimpleParallax; diff --git a/src/components/icons/DownloadOnAppStore.tsx b/src/components/icons/DownloadOnAppStore.tsx new file mode 100644 index 0000000..f1c2f68 --- /dev/null +++ b/src/components/icons/DownloadOnAppStore.tsx @@ -0,0 +1,71 @@ +import { Component } from "solid-js"; + +const DownloadOnAppStore: Component<{ size: number }> = (props) => { + return ( + + Download_on_the_App_Store_Badge + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default DownloadOnAppStore; diff --git a/src/components/icons/DownloadOnAppStoreDark.tsx b/src/components/icons/DownloadOnAppStoreDark.tsx new file mode 100644 index 0000000..651ef40 --- /dev/null +++ b/src/components/icons/DownloadOnAppStoreDark.tsx @@ -0,0 +1,129 @@ +export default function DownloadOnAppStoreDark(props: { size: number }) { + return ( + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/icons/LinkedIn.tsx b/src/components/icons/LinkedIn.tsx new file mode 100644 index 0000000..4f76ea1 --- /dev/null +++ b/src/components/icons/LinkedIn.tsx @@ -0,0 +1,26 @@ +import { Component } from "solid-js"; + +const LinkedIn: Component<{ + height: string | number; + width: string | number; + fill?: string; + stroke?: string; +}> = (props) => { + return ( + + + + ); +}; + +export default LinkedIn; diff --git a/src/lib/s3upload.ts b/src/lib/s3upload.ts new file mode 100644 index 0000000..99afb54 --- /dev/null +++ b/src/lib/s3upload.ts @@ -0,0 +1,52 @@ +/** + * S3 Upload Utility for SolidStart + * Uploads files to S3 using pre-signed URLs from tRPC + */ + +export default async function AddImageToS3( + file: Blob | File, + title: string, + type: string, +): Promise { + try { + // Get pre-signed URL from tRPC endpoint + const getPreSignedResponse = await fetch("/api/trpc/misc.getPreSignedURL", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: type, + title: title, + filename: (file as File).name, + }), + }); + + if (!getPreSignedResponse.ok) { + throw new Error("Failed to get pre-signed URL"); + } + + const responseData = await getPreSignedResponse.json(); + const { uploadURL, key } = responseData.result.data as { + uploadURL: string; + key: string; + }; + + console.log("url: " + uploadURL, "key: " + key); + + // Upload file to S3 using pre-signed URL + const uploadResponse = await fetch(uploadURL, { + method: "PUT", + body: file as File, + }); + + if (!uploadResponse.ok) { + throw new Error("Failed to upload file to S3"); + } + + return key; + } catch (e) { + console.error("S3 upload error:", e); + throw e; + } +} diff --git a/src/routes/account.tsx b/src/routes/account.tsx new file mode 100644 index 0000000..b4d97b9 --- /dev/null +++ b/src/routes/account.tsx @@ -0,0 +1,624 @@ +import { createSignal, createEffect, Show, onMount } from "solid-js"; +import { useNavigate } from "@solidjs/router"; +import Eye from "~/components/icons/Eye"; +import EyeSlash from "~/components/icons/EyeSlash"; +import { validatePassword, isValidEmail } from "~/lib/validation"; + +type UserProfile = { + id: string; + email: string | null; + emailVerified: boolean; + displayName: string | null; + image: string | null; + provider: string; + hasPassword: boolean; +}; + +export default function AccountPage() { + const navigate = useNavigate(); + + // User data + const [user, setUser] = createSignal(null); + const [loading, setLoading] = createSignal(true); + + // Form loading states + const [emailButtonLoading, setEmailButtonLoading] = createSignal(false); + const [displayNameButtonLoading, setDisplayNameButtonLoading] = createSignal(false); + const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false); + const [deleteAccountButtonLoading, setDeleteAccountButtonLoading] = createSignal(false); + const [profileImageSetLoading, setProfileImageSetLoading] = createSignal(false); + + // Password state + const [passwordsMatch, setPasswordsMatch] = createSignal(false); + const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false); + const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false); + const [passwordBlurred, setPasswordBlurred] = createSignal(false); + const [passwordError, setPasswordError] = createSignal(false); + const [passwordDeletionError, setPasswordDeletionError] = createSignal(false); + + // Show/hide password toggles + const [showOldPasswordInput, setShowOldPasswordInput] = createSignal(false); + const [showPasswordInput, setShowPasswordInput] = createSignal(false); + const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false); + + // Success messages + const [showImageSuccess, setShowImageSuccess] = createSignal(false); + const [showEmailSuccess, setShowEmailSuccess] = createSignal(false); + const [showDisplayNameSuccess, setShowDisplayNameSuccess] = createSignal(false); + const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false); + + // Form refs + let oldPasswordRef: HTMLInputElement | undefined; + let newPasswordRef: HTMLInputElement | undefined; + let newPasswordConfRef: HTMLInputElement | undefined; + let emailRef: HTMLInputElement | undefined; + let displayNameRef: HTMLInputElement | undefined; + let deleteAccountPasswordRef: HTMLInputElement | undefined; + + // Fetch user profile on mount + onMount(async () => { + try { + const response = await fetch("/api/trpc/user.getProfile", { + method: "GET", + }); + + if (response.ok) { + const result = await response.json(); + if (result.result?.data) { + setUser(result.result.data); + } else { + // Not logged in, redirect to login + navigate("/login"); + } + } else { + navigate("/login"); + } + } catch (err) { + console.error("Failed to fetch user profile:", err); + navigate("/login"); + } finally { + setLoading(false); + } + }); + + // Email update handler + const setEmailTrigger = async (e: Event) => { + e.preventDefault(); + if (!emailRef) return; + + const email = emailRef.value; + if (!isValidEmail(email)) { + alert("Invalid email address"); + return; + } + + setEmailButtonLoading(true); + setShowEmailSuccess(false); + + try { + const response = await fetch("/api/trpc/user.updateEmail", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + + const result = await response.json(); + if (response.ok && result.result?.data) { + setUser(result.result.data); + setShowEmailSuccess(true); + setTimeout(() => setShowEmailSuccess(false), 3000); + } + } catch (err) { + console.error("Email update error:", err); + } finally { + setEmailButtonLoading(false); + } + }; + + // Display name update handler + const setDisplayNameTrigger = async (e: Event) => { + e.preventDefault(); + if (!displayNameRef) return; + + const displayName = displayNameRef.value; + setDisplayNameButtonLoading(true); + setShowDisplayNameSuccess(false); + + try { + const response = await fetch("/api/trpc/user.updateDisplayName", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ displayName }), + }); + + const result = await response.json(); + if (response.ok && result.result?.data) { + setUser(result.result.data); + setShowDisplayNameSuccess(true); + setTimeout(() => setShowDisplayNameSuccess(false), 3000); + } + } catch (err) { + console.error("Display name update error:", err); + } finally { + setDisplayNameButtonLoading(false); + } + }; + + // Password change/set handler + const handlePasswordSubmit = async (e: Event) => { + e.preventDefault(); + const currentUser = user(); + if (!currentUser) return; + + if (currentUser.hasPassword) { + // Change password (requires old password) + if (!oldPasswordRef || !newPasswordRef || !newPasswordConfRef) return; + + const oldPassword = oldPasswordRef.value; + const newPassword = newPasswordRef.value; + const newPasswordConf = newPasswordConfRef.value; + + const passwordValidation = validatePassword(newPassword); + if (!passwordValidation.isValid) { + setPasswordError(true); + return; + } + + if (newPassword !== newPasswordConf) { + setPasswordError(true); + return; + } + + setPasswordChangeLoading(true); + setPasswordError(false); + + try { + const response = await fetch("/api/trpc/user.changePassword", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ oldPassword, newPassword }), + }); + + const result = await response.json(); + if (response.ok && result.result?.data?.success) { + setShowPasswordSuccess(true); + setTimeout(() => setShowPasswordSuccess(false), 3000); + // Clear form + if (oldPasswordRef) oldPasswordRef.value = ""; + if (newPasswordRef) newPasswordRef.value = ""; + if (newPasswordConfRef) newPasswordConfRef.value = ""; + } else { + setPasswordError(true); + } + } catch (err) { + console.error("Password change error:", err); + setPasswordError(true); + } finally { + setPasswordChangeLoading(false); + } + } else { + // Set password (first time for OAuth users) + if (!newPasswordRef || !newPasswordConfRef) return; + + const newPassword = newPasswordRef.value; + const newPasswordConf = newPasswordConfRef.value; + + const passwordValidation = validatePassword(newPassword); + if (!passwordValidation.isValid) { + setPasswordError(true); + return; + } + + if (newPassword !== newPasswordConf) { + setPasswordError(true); + return; + } + + setPasswordChangeLoading(true); + setPasswordError(false); + + try { + const response = await fetch("/api/trpc/user.setPassword", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: newPassword }), + }); + + const result = await response.json(); + if (response.ok && result.result?.data?.success) { + // Refresh user data to show hasPassword = true + const profileResponse = await fetch("/api/trpc/user.getProfile"); + const profileResult = await profileResponse.json(); + if (profileResult.result?.data) { + setUser(profileResult.result.data); + } + setShowPasswordSuccess(true); + setTimeout(() => setShowPasswordSuccess(false), 3000); + // Clear form + if (newPasswordRef) newPasswordRef.value = ""; + if (newPasswordConfRef) newPasswordConfRef.value = ""; + } else { + setPasswordError(true); + } + } catch (err) { + console.error("Password set error:", err); + setPasswordError(true); + } finally { + setPasswordChangeLoading(false); + } + } + }; + + // Delete account handler + const deleteAccountTrigger = async (e: Event) => { + e.preventDefault(); + if (!deleteAccountPasswordRef) return; + + const password = deleteAccountPasswordRef.value; + setDeleteAccountButtonLoading(true); + setPasswordDeletionError(false); + + try { + const response = await fetch("/api/trpc/user.deleteAccount", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + + const result = await response.json(); + if (response.ok && result.result?.data?.success) { + // Redirect to login + navigate("/login"); + } else { + setPasswordDeletionError(true); + } + } catch (err) { + console.error("Delete account error:", err); + setPasswordDeletionError(true); + } finally { + setDeleteAccountButtonLoading(false); + } + }; + + // Resend email verification + const sendEmailVerification = async () => { + const currentUser = user(); + if (!currentUser?.email) return; + + try { + await fetch("/api/trpc/auth.resendEmailVerification", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: currentUser.email }), + }); + alert("Verification email sent!"); + } catch (err) { + console.error("Email verification error:", err); + } + }; + + // Password validation helpers + const checkPasswordLength = (password: string) => { + if (password.length >= 8) { + setPasswordLengthSufficient(true); + setShowPasswordLengthWarning(false); + } else { + setPasswordLengthSufficient(false); + if (passwordBlurred()) { + setShowPasswordLengthWarning(true); + } + } + }; + + const checkForMatch = (newPassword: string, newPasswordConf: string) => { + setPasswordsMatch(newPassword === newPasswordConf); + }; + + const handleNewPasswordChange = (e: Event) => { + const target = e.target as HTMLInputElement; + checkPasswordLength(target.value); + if (newPasswordConfRef) { + checkForMatch(target.value, newPasswordConfRef.value); + } + }; + + const handlePasswordConfChange = (e: Event) => { + const target = e.target as HTMLInputElement; + if (newPasswordRef) { + checkForMatch(newPasswordRef.value, target.value); + } + }; + + const handlePasswordBlur = () => { + if (!passwordLengthSufficient() && newPasswordRef && newPasswordRef.value !== "") { + setShowPasswordLengthWarning(true); + } + setPasswordBlurred(true); + }; + + return ( +
+
+ +
Loading...
+
+ } + > + {(currentUser) => ( + <> +
+ Account Settings +
+ + {/* Email Section */} +
+
+
+
Current email:
+ {currentUser().email ? ( + {currentUser().email} + ) : ( + None Set + )} +
+ + + +
+ +
+
+ + + +
+
+ +
+ +
Email updated!
+
+
+ + {/* Display Name Section */} +
+
+
Display Name:
+ {currentUser().displayName ? ( + {currentUser().displayName} + ) : ( + None Set + )} +
+
+ +
+
+ + + +
+
+ +
+ +
Display name updated!
+
+
+
+ + {/* Password Change/Set Section */} +
+
+
+ {currentUser().hasPassword ? "Change Password" : "Set Password"} +
+ + +
+ + + + +
+
+ +
+ + + + +
+ + +
+ Password too short! Min Length: 8 +
+
+ +
+ + + + +
+ + = 6}> +
+ Passwords do not match! +
+
+ + + + +
+ {currentUser().hasPassword + ? "Password did not match record" + : "Error setting password"} +
+
+ + +
+ Password {currentUser().hasPassword ? "changed" : "set"} successfully! +
+
+
+
+ +
+ + {/* Delete Account Section */} +
+
+
Delete Account
+
+ Warning: This will delete all account information and is irreversible +
+ +
+
+
+ + + +
+
+ + + + +
+ Password did not match record +
+
+
+
+
+ + )} + +
+
+ ); +} diff --git a/src/routes/downloads.tsx b/src/routes/downloads.tsx new file mode 100644 index 0000000..a22f535 --- /dev/null +++ b/src/routes/downloads.tsx @@ -0,0 +1,167 @@ +import { A } from "@solidjs/router"; +import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; +import GitHub from "~/components/icons/GitHub"; +import LinkedIn from "~/components/icons/LinkedIn"; + +export default function DownloadsPage() { + const download = (assetName: string) => { + fetch(`/api/downloads/public/${assetName}`) + .then((response) => response.json()) + .then((data) => { + const url = data.downloadURL; + window.location.href = url; + }) + .catch((error) => console.error(error)); + }; + + const joinBetaPrompt = () => { + window.alert( + "This isn't released yet, if you would like to help test, please go the contact page and include the game and platform you would like to help test in the message. Otherwise the apk is available for direct install. Thanks!" + ); + }; + + return ( +
+
+ Downloads +
+ +
+
+ Life and Lineage +
+
+ +
+
+
Android (apk only)
+ +
+ Note the android version is not well tested, and has performance + issues. +
+
Or
+ +
(Coming soon)
+ +
+ +
+
iOS
+ + + +
+
+
+ +
+
+ Shapes with Abigail! +
+ (apk and iOS) +
+ +
+
+
Android
+ +
Or
+
(Coming soon)
+ +
+ +
+
iOS
+ + + +
+
+ +
+
+ Cork +
+ (macOS 13 Ventura or later) +
+ +
+ +
+
+ Just unzip and drag into 'Applications' folder +
+
+ + +
+
+ ); +} diff --git a/src/routes/login.tsx b/src/routes/login/index.tsx similarity index 100% rename from src/routes/login.tsx rename to src/routes/login/index.tsx diff --git a/src/routes/login/password-reset.tsx b/src/routes/login/password-reset.tsx new file mode 100644 index 0000000..91b373a --- /dev/null +++ b/src/routes/login/password-reset.tsx @@ -0,0 +1,321 @@ +import { createSignal, createEffect, Show } from "solid-js"; +import { A, useNavigate, useSearchParams } from "@solidjs/router"; +import CountdownCircleTimer from "~/components/CountdownCircleTimer"; +import Eye from "~/components/icons/Eye"; +import EyeSlash from "~/components/icons/EyeSlash"; +import { validatePassword } from "~/lib/validation"; + +export default function PasswordResetPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + // State management + const [passwordBlurred, setPasswordBlurred] = createSignal(false); + const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false); + const [passwordsMatch, setPasswordsMatch] = createSignal(false); + const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false); + const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false); + const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false); + const [countDown, setCountDown] = createSignal(false); + const [error, setError] = createSignal(""); + const [showPasswordInput, setShowPasswordInput] = createSignal(false); + const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false); + + // Form refs + let newPasswordRef: HTMLInputElement | undefined; + let newPasswordConfRef: HTMLInputElement | undefined; + + // Get token from URL + const token = searchParams.token; + + // Redirect to request page if no token + createEffect(() => { + if (!token) { + navigate("/login/request-password-reset"); + } + }); + + // Form submission handler + const setNewPasswordTrigger = async (e: Event) => { + e.preventDefault(); + setShowRequestNewEmail(false); + setError(""); + + if (!newPasswordRef || !newPasswordConfRef) { + setError("Please fill in all fields"); + return; + } + + const newPassword = newPasswordRef.value; + const newPasswordConf = newPasswordConfRef.value; + + // Validate password + const passwordValidation = validatePassword(newPassword); + if (!passwordValidation.isValid) { + setError(passwordValidation.errors[0] || "Invalid password"); + return; + } + + if (newPassword !== newPasswordConf) { + setError("Passwords do not match"); + return; + } + + setPasswordChangeLoading(true); + + try { + const response = await fetch("/api/trpc/auth.resetPassword", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + token: token, + newPassword, + newPasswordConfirmation: newPasswordConf, + }), + }); + + const result = await response.json(); + + if (response.ok && result.result?.data) { + setCountDown(true); + } else { + const errorMsg = result.error?.message || "Failed to reset password"; + if (errorMsg.includes("expired") || errorMsg.includes("token")) { + setShowRequestNewEmail(true); + setError("Token has expired"); + } else { + setError(errorMsg); + } + } + } catch (err) { + console.error("Password reset error:", err); + setError("An error occurred. Please try again."); + } finally { + setPasswordChangeLoading(false); + } + }; + + // Check if passwords match + const checkForMatch = (newPassword: string, newPasswordConf: string) => { + if (newPassword === newPasswordConf) { + setPasswordsMatch(true); + } else { + setPasswordsMatch(false); + } + }; + + // Check password length + const checkPasswordLength = (password: string) => { + if (password.length >= 8) { + setPasswordLengthSufficient(true); + setShowPasswordLengthWarning(false); + } else { + setPasswordLengthSufficient(false); + if (passwordBlurred()) { + setShowPasswordLengthWarning(true); + } + } + }; + + // Handle password blur + const passwordLengthBlurCheck = () => { + if ( + !passwordLengthSufficient() && + newPasswordRef && + newPasswordRef.value !== "" + ) { + setShowPasswordLengthWarning(true); + } + setPasswordBlurred(true); + }; + + // Handle new password change + const handleNewPasswordChange = (e: Event) => { + const target = e.target as HTMLInputElement; + checkPasswordLength(target.value); + if (newPasswordConfRef) { + checkForMatch(target.value, newPasswordConfRef.value); + } + }; + + // Handle password confirmation change + const handlePasswordConfChange = (e: Event) => { + const target = e.target as HTMLInputElement; + if (newPasswordRef) { + checkForMatch(newPasswordRef.value, target.value); + } + }; + + // Handle password blur + const handlePasswordBlur = () => { + passwordLengthBlurCheck(); + }; + + // Render countdown timer + const renderTime = (timeRemaining: number) => { + if (timeRemaining === 0) { + navigate("/login"); + } + return ( +
+
Change Successful!
+
{timeRemaining}
+
Redirecting...
+
+ ); + }; + + return ( +
+
+ Set New Password +
+ +
setNewPasswordTrigger(e)} + class="mt-4 flex w-full justify-center" + > +
+ {/* New Password Input */} +
+ + + + +
+ + {/* Password Length Warning */} +
+ Password too short! Min Length: 8 +
+ + {/* Password Confirmation Input */} +
+ + + + +
+ + {/* Password Mismatch Warning */} +
= 6 + ? "" + : "select-none opacity-0" + } transition-opacity text-center text-red-500 text-sm duration-200 ease-in-out mt-2`} + > + Passwords do not match! +
+ + {/* Countdown Timer or Submit Button */} + + {passwordChangeLoading() ? "Setting..." : "Set New Password"} + + } + > +
+ false} + > + {({ remainingTime }) => renderTime(remainingTime)} + +
+
+
+
+ + {/* Error Message */} + +
+
{error()}
+
+
+ + {/* Token Expired Message */} +
+ Token has expired, request a new one{" "} + + here + +
+ + {/* Back to Login Link */} + + + +
+ ); +} diff --git a/src/routes/login/request-password-reset.tsx b/src/routes/login/request-password-reset.tsx new file mode 100644 index 0000000..b83daec --- /dev/null +++ b/src/routes/login/request-password-reset.tsx @@ -0,0 +1,204 @@ +import { createSignal, createEffect, onCleanup, Show } from "solid-js"; +import { A, useNavigate } from "@solidjs/router"; +import CountdownCircleTimer from "~/components/CountdownCircleTimer"; +import { isValidEmail } from "~/lib/validation"; +import { getClientCookie } from "~/lib/cookies.client"; + +export default function RequestPasswordResetPage() { + const navigate = useNavigate(); + + // State management + const [loading, setLoading] = createSignal(false); + const [countDown, setCountDown] = createSignal(0); + const [showSuccessMessage, setShowSuccessMessage] = createSignal(false); + const [error, setError] = createSignal(""); + + // Form refs + let emailRef: HTMLInputElement | undefined; + let timerInterval: number | undefined; + + // Calculate remaining time from cookie + const calcRemainder = (timer: string) => { + const expires = new Date(timer); + const remaining = expires.getTime() - Date.now(); + const remainingInSeconds = remaining / 1000; + + if (remainingInSeconds <= 0) { + setCountDown(0); + if (timerInterval) { + clearInterval(timerInterval); + } + } else { + setCountDown(remainingInSeconds); + } + }; + + // Check for existing timer on mount + createEffect(() => { + const timer = getClientCookie("passwordResetRequested"); + if (timer) { + timerInterval = setInterval(() => calcRemainder(timer), 1000) as unknown as number; + onCleanup(() => { + if (timerInterval) { + clearInterval(timerInterval); + } + }); + } + }); + + // Form submission handler + const requestPasswordResetTrigger = async (e: Event) => { + e.preventDefault(); + setError(""); + setShowSuccessMessage(false); + + if (!emailRef) { + setError("Please enter an email address"); + return; + } + + const email = emailRef.value; + + // Validate email + if (!isValidEmail(email)) { + setError("Invalid email address"); + return; + } + + setLoading(true); + + try { + const response = await fetch("/api/trpc/auth.requestPasswordReset", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + + const result = await response.json(); + + if (response.ok && result.result?.data) { + setShowSuccessMessage(true); + setError(""); + + // Start countdown timer + const timer = getClientCookie("passwordResetRequested"); + if (timer) { + if (timerInterval) { + clearInterval(timerInterval); + } + timerInterval = setInterval(() => { + calcRemainder(timer); + }, 1000) as unknown as number; + } + } else { + const errorMsg = result.error?.message || "Failed to send reset email"; + if (errorMsg.includes("countdown not expired")) { + setError("Please wait before requesting another reset email"); + } else { + setError(errorMsg); + } + } + } catch (err) { + console.error("Password reset request error:", err); + setError("An error occurred. Please try again."); + } finally { + setLoading(false); + } + }; + + const renderTime = () => { + return ( +
+
{countDown().toFixed(0)}
+
+ ); + }; + + return ( +
+
+ Password Reset Request +
+ +
requestPasswordResetTrigger(e)} + class="mt-4 flex w-full justify-center" + > +
+ {/* Email Input */} +
+ + + +
+ + {/* Countdown Timer or Submit Button */} + 0} + fallback={ + + } + > +
+ false} + > + {renderTime} + +
+
+
+
+ + {/* Success Message */} +
+ If email exists, you will receive an email shortly! +
+ + {/* Error Message */} + +
+
{error()}
+
+
+ + {/* Back to Login Link */} + +
+ ); +} diff --git a/src/routes/marketing/life-and-lineage.tsx b/src/routes/marketing/life-and-lineage.tsx new file mode 100644 index 0000000..f818319 --- /dev/null +++ b/src/routes/marketing/life-and-lineage.tsx @@ -0,0 +1,46 @@ +import { A } from "@solidjs/router"; +import SimpleParallax from "~/components/SimpleParallax"; +import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; + +export default function LifeAndLineageMarketing() { + return ( + +
+
+ Lineage App Icon +
+

+ Life and Lineage +

+

A dark fantasy adventure

+ +
+
+ ); +} diff --git a/src/routes/privacy-policy/life-and-lineage.tsx b/src/routes/privacy-policy/life-and-lineage.tsx new file mode 100644 index 0000000..085f786 --- /dev/null +++ b/src/routes/privacy-policy/life-and-lineage.tsx @@ -0,0 +1,112 @@ +import { A } from "@solidjs/router"; + +export default function PrivacyPolicy() { + return ( +
+
+
+ Life and Lineage's Privacy Policy +
+
Last Updated: October 22, 2024
+
+ Welcome to Life and Lineage ('We', 'Us', + 'Our'). Your privacy is important to us. This privacy + policy will help you understand our policies and procedures related + to the collection, use, and storage of personal information from our + users. +
+
    +
    +
    + 1. Personal Information +
    +
    +
    +
    (a) Collection of Personal Data:
    {" "} + Life and Lineage collects and stores personal data only if + users opt to use the remote saving feature. The information + collected includes email address, and if using an OAuth + provider - first name, and last name. This information is used + solely for the purpose of providing and managing the remote + saving feature. It is and never will be shared with a third + party. +
    +
    +
    (b) Data Removal:
    Users can + request the removal of all information related to them by + visiting{" "} + + this page + {" "} + and filling out the provided form. +
    +
    +
    + +
    +
    + 2. Third-Party Access +
    +
    +
    (a) Limited Third-Party Access:
    We + do not share or sell user information to third parties. However, + we do utilize third-party services for crash reporting and + performance profiling. These services do not have access to + personal user information and only receive anonymized data + related to app performance and stability. +
    +
    + +
    +
    + 3. Security +
    +
    +
    (a) Data Protection:
    Life and + Lineage takes appropriate measures to protect the personal + information of users who opt for the remote saving feature. We + implement industry-standard security protocols to prevent + unauthorized access, disclosure, alteration, or destruction of + user data. +
    +
    + +
    +
    + 4. Changes to the Privacy + Policy +
    +
    +
    (a) Updates:
    We may update this + privacy policy periodically. Any changes to this privacy policy + will be posted on this page. We encourage users to review this + policy regularly to stay informed about how we protect their + information. +
    +
    + +
    +
    + 5. Contact Us +
    +
    +
    (a) Reaching Out:
    If there are any + questions or comments regarding this privacy policy, you can + contact us{" "} + + here + + . +
    +
    +
+
+
+ ); +} diff --git a/src/routes/privacy-policy/shapes-with-abigail.tsx b/src/routes/privacy-policy/shapes-with-abigail.tsx new file mode 100644 index 0000000..c60a549 --- /dev/null +++ b/src/routes/privacy-policy/shapes-with-abigail.tsx @@ -0,0 +1,98 @@ +import { A } from "@solidjs/router"; + +export default function PrivacyPolicy() { + return ( +
+
+
+ Shapes with Abigail!'s Privacy Policy +
+
Last Updated: December 21, 2023
+
+ Welcome to Shapes with Abigail! ('We' , 'Us', + 'Our'). Your privacy is important to us. For that reason, + our app, "Shapes with Abigail!" has been designed to + provide our users with a secure environment. This privacy policy + will help you understand our policies and procedures related to the + non-collection, non-use, and non-storage of personal information + from our users. +
+
    +
    +
    + 1. Personal Information +
    +
    +
    +
    + (a) Non-Collection of Personal Data: +
    {" "} + Shapes with Abigail! does not collect nor store personal data. + We respect the privacy of our users, especially considering + the age of our users. We believe that no information, whether + private or personal, should be required for children to enjoy + our fun and educational app. +
    +
    +
    + +
    +
    + 2. Third-Party Access +
    +
    +
    (a) No Third-Party Access:
    Since we + do not collect or store any user data, there is no possibility + of sharing or selling our users' information to third + parties. Our priority is the safety and privacy of our users. +
    +
    + +
    +
    + 3. Security +
    +
    +
    (a) Secure Environment:
    Shapes with + Abigail! offers a secure and safe platform for children to play + and learn. Not requiring any personal data naturally enhances + security by eliminating potential risks related to data breaches + and misuse of information. +
    +
    + +
    +
    + 4. Changes to the Privacy + Policy +
    +
    +
    (a) Updates:
    We may update this + privacy policy periodically. Any changes to this privacy policy + will be posted on this page. However, since we do not collect + any personal data, these updates are likely to be insignificant. +
    +
    + +
    +
    + 5. Contact Us +
    +
    +
    (a) Reaching Out:
    If there are any + questions or comments regarding this privacy policy, you can + contact us{" "} + + here + + . +
    +
    +
+
+
+ ); +}