import { createSignal, onMount, onCleanup, Show } from "solid-js";
import {
useSearchParams,
useNavigate,
useLocation,
query,
createAsync
} from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import { A } from "@solidjs/router";
import { action, redirect } from "@solidjs/router";
import { api } from "~/lib/api";
import { getClientCookie, setClientCookie } from "~/lib/cookies.client";
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import LoadingSpinner from "~/components/LoadingSpinner";
import RevealDropDown from "~/components/RevealDropDown";
import type { UserProfile } from "~/types/user";
import { getCookie, setCookie } from "vinxi/http";
import { z } from "zod";
import { env } from "~/env/server";
import {
fetchWithTimeout,
checkResponse,
fetchWithRetry,
NetworkError,
TimeoutError,
APIError
} from "~/server/fetch-utils";
const getContactData = query(async () => {
"use server";
const contactExp = getCookie("contactRequestSent");
let remainingTime = 0;
if (contactExp) {
const expires = new Date(contactExp);
remainingTime = Math.max(0, (expires.getTime() - Date.now()) / 1000);
}
return { remainingTime };
}, "contact-data");
const sendContactEmail = action(async (formData: FormData) => {
"use server";
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
// Validate inputs
const schema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Valid email is required"),
message: z
.string()
.min(1, "Message is required")
.max(500, "Message too long")
});
try {
schema.parse({ name, email, message });
} catch (err: any) {
return redirect(
`/contact?error=${encodeURIComponent(err.errors[0]?.message || "Invalid input")}`
);
}
const contactExp = getCookie("contactRequestSent");
if (contactExp) {
const expires = new Date(contactExp);
const remaining = expires.getTime() - Date.now();
if (remaining > 0) {
return redirect(
"/contact?error=Please wait before sending another message"
);
}
}
// Send email
const apiKey = env.SENDINBLUE_KEY;
const apiUrl = "https://api.sendinblue.com/v3/smtp/email";
const sendinblueData = {
sender: {
name: "freno.me",
email: "michael@freno.me"
},
to: [{ email: "michael@freno.me" }],
htmlContent: `
Request Name: ${name}
Request Email: ${email}
Request Message: ${message}
`,
subject: "freno.me Contact Request"
};
try {
await fetchWithRetry(
async () => {
const response = await fetchWithTimeout(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
},
body: JSON.stringify(sendinblueData),
timeout: 15000
});
await checkResponse(response);
return response;
},
{
maxRetries: 2,
retryDelay: 1000
}
);
// Set cooldown cookie
const exp = new Date(Date.now() + 1 * 60 * 1000);
setCookie("contactRequestSent", exp.toUTCString(), {
expires: exp,
path: "/"
});
return redirect("/contact?success=true");
} catch (error) {
let errorMessage =
"Failed to send message. You can reach me at michael@freno.me";
if (error instanceof TimeoutError) {
errorMessage =
"Email service timed out. Please try again or contact michael@freno.me";
} else if (error instanceof NetworkError) {
errorMessage =
"Network error. Please try again or contact michael@freno.me";
} else if (error instanceof APIError) {
errorMessage =
"Email service error. You can reach me at michael@freno.me";
}
return redirect(`/contact?error=${encodeURIComponent(errorMessage)}`);
}
});
export default function ContactPage() {
const [searchParams] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const viewer = () => searchParams.viewer ?? "default";
// Load server data using createAsync
const contactData = createAsync(() => getContactData(), {
deferStream: true
});
const [countDown, setCountDown] = createSignal(0);
const [emailSent, setEmailSent] = createSignal(
searchParams.success === "true"
);
const [error, setError] = createSignal(
searchParams.error ? decodeURIComponent(searchParams.error) : ""
);
const [loading, setLoading] = createSignal(false);
const [user, setUser] = createSignal(null);
const [jsEnabled, setJsEnabled] = createSignal(false);
let timerIdRef: ReturnType | null = null;
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 (timerIdRef !== null) {
clearInterval(timerIdRef);
}
} else {
setCountDown(remainingInSeconds);
}
};
onMount(() => {
setJsEnabled(true);
// Initialize countdown from server data
const serverData = contactData();
if (serverData?.remainingTime) {
setCountDown(serverData.remainingTime);
}
const timer = getClientCookie("contactRequestSent");
if (timer) {
timerIdRef = setInterval(() => calcRemainder(timer), 1000);
}
// Fetch user data if authenticated
api.user.getProfile
.query()
.then((userData) => {
if (userData) {
setUser(userData);
}
})
.catch(() => {
// User not authenticated, no problem
});
// Clear URL params after reading them (for better UX on refresh)
if (searchParams.success || searchParams.error) {
const timer = setTimeout(() => {
const newUrl =
location.pathname +
(viewer() !== "default" ? `?viewer=${viewer()}` : "");
navigate(newUrl, { replace: true });
setEmailSent(false);
setError("");
}, 5000);
onCleanup(() => clearTimeout(timer));
}
onCleanup(() => {
if (timerIdRef !== null) {
clearInterval(timerIdRef);
}
});
});
// Progressive enhancement: JS-enhanced form submission
const sendEmailTrigger = async (e: Event) => {
// Only intercept if JS is enabled
if (!jsEnabled()) return;
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
if (name && email && message) {
setLoading(true);
setError("");
setEmailSent(false);
try {
const res = await api.misc.sendContactRequest.mutate({
name,
email,
message
});
if (res.message === "email sent") {
setEmailSent(true);
setError("");
(e.target as HTMLFormElement).reset();
const timer = getClientCookie("contactRequestSent");
if (timer) {
if (timerIdRef !== null) {
clearInterval(timerIdRef);
}
timerIdRef = setInterval(() => calcRemainder(timer), 1000);
}
}
} catch (err: any) {
setError(err.message || "An error occurred");
setEmailSent(false);
}
setLoading(false);
}
};
const LineageQuestionsDropDown = () => {
return (
Feel free to use the form below, I will respond as quickly as
possible, however, you may find an answer to your question in the
following.
1. Personal Information
You can find the entire privacy policy{" "}
here
.
2. Remote Backups
Life and Lineage uses a per-user database approach for
its remote storage, this provides better separation of users and
therefore privacy, and it makes requesting the removal of your
data simpler, you can even request the database dump if you so
choose. This isn't particularly expensive, but not free for
n users, so use of this feature requires a purchase of an
IAP(in-app purchase) - this can be the specific IAP for the
remote save feature, and any other IAP will also unlock this
feature.
3. Cross Device Play
You can use the above mentioned remote-backups to save progress
between devices/platforms.
4. Online Requirements
Currently, the only time you need to be online is for remote
save access. There are plans for pvp, which will require an
internet connection, but this is not implemented at time of
writing.
5. Microtransactions
Microtransactions are not required to play or complete the game,
the game can be fully completed without spending any money,
however 2 of the classes(necromancer and ranger) are pay-walled.
Microtransactions are supported cross-platform, so no need to
pay for each device, you simply need to login to your
gmail/apple/email account. This would require first creating a
character, signing in under options{">"}remote backups first.
);
};
const renderTime = ({ remainingTime }: { remainingTime: number }) => {
return (
{remainingTime.toFixed(0)}
);
};
return (
<>
Contact | Michael Freno
Contact
(for this website or any of my apps...)
{emailSent() ? "Email Sent!" : error()}
>
);
}