almost done
This commit is contained in:
@@ -7,8 +7,7 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
Show,
|
Show,
|
||||||
For,
|
For,
|
||||||
Suspense,
|
Suspense
|
||||||
createMemo
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
import { TerminalSplash } from "./TerminalSplash";
|
import { TerminalSplash } from "./TerminalSplash";
|
||||||
@@ -57,8 +56,8 @@ export function RightBarContent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="text-text flex h-full w-min flex-col gap-6 overflow-y-auto pb-6">
|
<div class="text-text flex h-full flex-col gap-6 overflow-y-auto pb-6 md:w-min">
|
||||||
<Typewriter keepAlive={false} class="z-50 px-4 pt-4">
|
<Typewriter keepAlive={false} class="z-50 px-4 md:pt-4">
|
||||||
<ul class="flex flex-col gap-4">
|
<ul class="flex flex-col gap-4">
|
||||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||||
<a href="/contact">Contact Me</a>
|
<a href="/contact">Contact Me</a>
|
||||||
@@ -113,7 +112,8 @@ export function RightBarContent() {
|
|||||||
|
|
||||||
{/* Git Activity Section */}
|
{/* Git Activity Section */}
|
||||||
<Suspense fallback={<TerminalSplash />}>
|
<Suspense fallback={<TerminalSplash />}>
|
||||||
<div class="border-overlay0 flex min-w-0 flex-col gap-6 border-t px-4 pt-6">
|
<hr class="border-overlay0" />
|
||||||
|
<div class="flex min-w-0 flex-col gap-6 px-4 pt-6">
|
||||||
<RecentCommits
|
<RecentCommits
|
||||||
commits={githubCommits()}
|
commits={githubCommits()}
|
||||||
title="Recent GitHub Commits"
|
title="Recent GitHub Commits"
|
||||||
@@ -405,7 +405,7 @@ export function LeftBar() {
|
|||||||
{/* Navigation Links */}
|
{/* Navigation Links */}
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<Typewriter keepAlive={false}>
|
<Typewriter keepAlive={false}>
|
||||||
<ul class="flex flex-row gap-4 py-6 md:flex-col">
|
<ul class="flex flex-col gap-4 py-6">
|
||||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -442,13 +442,12 @@ export function LeftBar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
|
|
||||||
{/* Dark Mode Toggle */}
|
<hr class="border-overlay0 -mx-4 my-auto" />
|
||||||
<div class="border-overlay0 border-t pt-6">
|
<div class="my-auto">
|
||||||
<DarkModeToggle />
|
<DarkModeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RightBar content on mobile */}
|
<div class="border-overlay0 -mx-4 border-t pt-8 md:hidden">
|
||||||
<div class="border-overlay0 border-t pt-8 md:hidden">
|
|
||||||
<RightBarContent />
|
<RightBarContent />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -469,10 +469,10 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
instance().chain().focus().setHorizontalRule().run()
|
instance().chain().focus().setHorizontalRule().run()
|
||||||
}
|
}
|
||||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
class="bg-surface0 hover:bg-surface1 rounded px-3 py-1 text-xs"
|
||||||
title="Horizontal Rule"
|
title="Horizontal Rule"
|
||||||
>
|
>
|
||||||
─ HR
|
━━ HR
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -481,7 +481,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
class="prose prose-sm prose-invert sm:prose-base md:prose-xl lg:prose-xl xl:prose-2xl mx-auto min-h-[400px] min-w-full focus:outline-none"
|
class="prose prose-sm prose-invert sm:prose-base md:prose-xl lg:prose-xl xl:prose-2xl [&_hr]:border-surface2 mx-auto min-h-[400px] min-w-full focus:outline-none [&_hr]:my-8 [&_hr]:border-t-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { useNavigate, cache, redirect } from "@solidjs/router";
|
|||||||
import { getEvent } from "vinxi/http";
|
import { getEvent } from "vinxi/http";
|
||||||
import Eye from "~/components/icons/Eye";
|
import Eye from "~/components/icons/Eye";
|
||||||
import EyeSlash from "~/components/icons/EyeSlash";
|
import EyeSlash from "~/components/icons/EyeSlash";
|
||||||
|
import XCircle from "~/components/icons/XCircle";
|
||||||
|
import Dropzone from "~/components/blog/Dropzone";
|
||||||
|
import AddImageToS3 from "~/lib/s3upload";
|
||||||
import { validatePassword, isValidEmail } from "~/lib/validation";
|
import { validatePassword, isValidEmail } from "~/lib/validation";
|
||||||
import { checkAuthStatus } from "~/server/utils";
|
import { checkAuthStatus } from "~/server/utils";
|
||||||
|
|
||||||
@@ -71,6 +74,17 @@ export default function AccountPage() {
|
|||||||
createSignal(false);
|
createSignal(false);
|
||||||
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
|
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
|
||||||
|
|
||||||
|
// Profile image state
|
||||||
|
const [profileImage, setProfileImage] = createSignal<Blob | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [profileImageHolder, setProfileImageHolder] = createSignal<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [profileImageStateChange, setProfileImageStateChange] =
|
||||||
|
createSignal(false);
|
||||||
|
const [preSetHolder, setPreSetHolder] = createSignal<string | null>(null);
|
||||||
|
|
||||||
// Form refs
|
// Form refs
|
||||||
let oldPasswordRef: HTMLInputElement | undefined;
|
let oldPasswordRef: HTMLInputElement | undefined;
|
||||||
let newPasswordRef: HTMLInputElement | undefined;
|
let newPasswordRef: HTMLInputElement | undefined;
|
||||||
@@ -90,6 +104,10 @@ export default function AccountPage() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.result?.data) {
|
if (result.result?.data) {
|
||||||
setUser(result.result.data);
|
setUser(result.result.data);
|
||||||
|
// Set preset holder if user has existing image
|
||||||
|
if (result.result.data.image) {
|
||||||
|
setPreSetHolder(result.result.data.image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -99,6 +117,82 @@ export default function AccountPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Profile image handlers
|
||||||
|
const handleImageDrop = (acceptedFiles: File[]) => {
|
||||||
|
acceptedFiles.forEach((file: File) => {
|
||||||
|
setProfileImage(file);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const str = reader.result as string;
|
||||||
|
setProfileImageHolder(str);
|
||||||
|
setProfileImageStateChange(true);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeImage = () => {
|
||||||
|
setProfileImage(undefined);
|
||||||
|
setProfileImageHolder(null);
|
||||||
|
if (preSetHolder()) {
|
||||||
|
setProfileImageStateChange(true);
|
||||||
|
setPreSetHolder(null);
|
||||||
|
} else {
|
||||||
|
setProfileImageStateChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUserImage = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setProfileImageSetLoading(true);
|
||||||
|
setShowImageSuccess(false);
|
||||||
|
|
||||||
|
const currentUser = user();
|
||||||
|
if (!currentUser) {
|
||||||
|
setProfileImageSetLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let imageUrl = "";
|
||||||
|
|
||||||
|
// Upload new image if one was selected
|
||||||
|
if (profileImage()) {
|
||||||
|
const imageKey = await AddImageToS3(
|
||||||
|
profileImage()!,
|
||||||
|
currentUser.id,
|
||||||
|
"user"
|
||||||
|
);
|
||||||
|
imageUrl = imageKey || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user profile image
|
||||||
|
const response = await fetch("/api/trpc/user.updateProfileImage", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ imageUrl })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok && result.result?.data) {
|
||||||
|
setUser(result.result.data);
|
||||||
|
setShowImageSuccess(true);
|
||||||
|
setProfileImageStateChange(false);
|
||||||
|
setTimeout(() => setShowImageSuccess(false), 3000);
|
||||||
|
|
||||||
|
// Update preSetHolder with new image
|
||||||
|
setPreSetHolder(imageUrl || null);
|
||||||
|
} else {
|
||||||
|
alert("Error updating profile image!");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Profile image update error:", err);
|
||||||
|
alert("Error updating profile image! Check console.");
|
||||||
|
} finally {
|
||||||
|
setProfileImageSetLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Email update handler
|
// Email update handler
|
||||||
const setEmailTrigger = async (e: Event) => {
|
const setEmailTrigger = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -375,6 +469,57 @@ export default function AccountPage() {
|
|||||||
Account Settings
|
Account Settings
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Image Section */}
|
||||||
|
<div class="mx-auto mb-8 flex max-w-md justify-center">
|
||||||
|
<div class="flex flex-col py-4">
|
||||||
|
<div class="mb-2 text-center text-lg font-semibold">
|
||||||
|
Profile Image
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<Dropzone
|
||||||
|
onDrop={handleImageDrop}
|
||||||
|
acceptedFiles="image/jpg, image/jpeg, image/png"
|
||||||
|
fileHolder={profileImageHolder()}
|
||||||
|
preSet={preSetHolder() || currentUser().image || null}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={removeImage}
|
||||||
|
class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125"
|
||||||
|
>
|
||||||
|
<XCircle
|
||||||
|
height={36}
|
||||||
|
width={36}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={setUserImage}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
profileImageSetLoading() || !profileImageStateChange()
|
||||||
|
}
|
||||||
|
class={`${
|
||||||
|
profileImageSetLoading() || !profileImageStateChange()
|
||||||
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
|
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
{profileImageSetLoading() ? "Uploading..." : "Set Image"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<Show when={showImageSuccess()}>
|
||||||
|
<div class="text-green mt-2 text-center text-sm">
|
||||||
|
Profile image updated!
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="mx-auto mb-8 max-w-4xl" />
|
||||||
|
|
||||||
{/* Email Section */}
|
{/* Email Section */}
|
||||||
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
|
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
|
||||||
<div class="flex items-center justify-center text-lg md:justify-normal">
|
<div class="flex items-center justify-center text-lg md:justify-normal">
|
||||||
@@ -431,7 +576,7 @@ export default function AccountPage() {
|
|||||||
emailButtonLoading() ||
|
emailButtonLoading() ||
|
||||||
(currentUser().email !== null &&
|
(currentUser().email !== null &&
|
||||||
!currentUser().emailVerified)
|
!currentUser().emailVerified)
|
||||||
? "bg-blue cursor-not-allowed brightness-50"
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
>
|
>
|
||||||
@@ -482,7 +627,7 @@ export default function AccountPage() {
|
|||||||
disabled={displayNameButtonLoading()}
|
disabled={displayNameButtonLoading()}
|
||||||
class={`${
|
class={`${
|
||||||
displayNameButtonLoading()
|
displayNameButtonLoading()
|
||||||
? "bg-blue cursor-not-allowed brightness-50"
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
>
|
>
|
||||||
@@ -610,7 +755,7 @@ export default function AccountPage() {
|
|||||||
disabled={passwordChangeLoading() || !passwordsMatch()}
|
disabled={passwordChangeLoading() || !passwordsMatch()}
|
||||||
class={`${
|
class={`${
|
||||||
passwordChangeLoading() || !passwordsMatch()
|
passwordChangeLoading() || !passwordsMatch()
|
||||||
? "bg-blue cursor-not-allowed brightness-50"
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
} my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
} my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
>
|
>
|
||||||
@@ -670,7 +815,7 @@ export default function AccountPage() {
|
|||||||
disabled={deleteAccountButtonLoading()}
|
disabled={deleteAccountButtonLoading()}
|
||||||
class={`${
|
class={`${
|
||||||
deleteAccountButtonLoading()
|
deleteAccountButtonLoading()
|
||||||
? "bg-red cursor-not-allowed brightness-50"
|
? "bg-red cursor-not-allowed brightness-75"
|
||||||
: "bg-red hover:brightness-125 active:scale-90"
|
: "bg-red hover:brightness-125 active:scale-90"
|
||||||
} mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
} mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -105,15 +105,14 @@ export default function ContactPage() {
|
|||||||
<div class="w-full py-12">
|
<div class="w-full py-12">
|
||||||
<RevealDropDown title={"Questions about Life and Lineage?"}>
|
<RevealDropDown title={"Questions about Life and Lineage?"}>
|
||||||
<div>
|
<div>
|
||||||
Feel free to use the form{" "}
|
Feel free to use the form below, I will respond as quickly as
|
||||||
{viewer() === "lineage" ? "below" : "above"}, I will respond as
|
possible, however, you may find an answer to your question in the
|
||||||
quickly as possible, however, you may find an answer to your
|
following.
|
||||||
question in the following.
|
|
||||||
</div>
|
</div>
|
||||||
<ol>
|
<ol>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">1.</span> Personal Information
|
<span class="-ml-2 pr-2">1.</span> Personal Information
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
<div class="pb-2">
|
<div class="pb-2">
|
||||||
@@ -130,7 +129,7 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">2.</span> Remote Backups
|
<span class="-ml-2 pr-2">2.</span> Remote Backups
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
<em>Life and Lineage</em> uses a per-user database approach for
|
<em>Life and Lineage</em> uses a per-user database approach for
|
||||||
@@ -146,7 +145,7 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">3.</span> Cross Device Play
|
<span class="-ml-2 pr-2">3.</span> Cross Device Play
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
You can use the above mentioned remote-backups to save progress
|
You can use the above mentioned remote-backups to save progress
|
||||||
@@ -155,7 +154,7 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">4.</span> Online Requirements
|
<span class="-ml-2 pr-2">4.</span> Online Requirements
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
Currently, the only time you need to be online is for remote
|
Currently, the only time you need to be online is for remote
|
||||||
@@ -166,7 +165,7 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">5.</span> Microtransactions
|
<span class="-ml-2 pr-2">5.</span> Microtransactions
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
Microtransactions are not required to play or complete the game,
|
Microtransactions are not required to play or complete the game,
|
||||||
@@ -205,9 +204,7 @@ export default function ContactPage() {
|
|||||||
(for this website or any of my apps...)
|
(for this website or any of my apps...)
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={viewer() === "lineage"}>
|
<LineageQuestionsDropDown />
|
||||||
<LineageQuestionsDropDown />
|
|
||||||
</Show>
|
|
||||||
<form onSubmit={sendEmailTrigger} class="w-full">
|
<form onSubmit={sendEmailTrigger} class="w-full">
|
||||||
<div
|
<div
|
||||||
class={`flex w-full flex-col justify-evenly pt-6 ${
|
class={`flex w-full flex-col justify-evenly pt-6 ${
|
||||||
@@ -286,9 +283,6 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<Show when={viewer() !== "lineage"}>
|
|
||||||
<LineageQuestionsDropDown />
|
|
||||||
</Show>
|
|
||||||
<div
|
<div
|
||||||
class={`${
|
class={`${
|
||||||
emailSent()
|
emailSent()
|
||||||
|
|||||||
@@ -2,38 +2,51 @@ import { Typewriter } from "~/components/Typewriter";
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main class="p-4 text-xl">
|
<main class="flex h-full flex-col p-4 text-xl">
|
||||||
<Typewriter speed={30} keepAlive={2000}>
|
<div class="flex-1">
|
||||||
<div class="text-4xl">Hey!</div>
|
<Typewriter speed={30} keepAlive={2000}>
|
||||||
</Typewriter>
|
<div class="text-4xl">Hey!</div>
|
||||||
<Typewriter speed={80} keepAlive={2000}>
|
</Typewriter>
|
||||||
<div>
|
<Typewriter speed={80} keepAlive={2000}>
|
||||||
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
|
<div>
|
||||||
<span class="text-blue">Software Engineer</span> based in{" "}
|
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
|
||||||
<span class="text-yellow">Brooklyn, NY</span>
|
<span class="text-blue">Software Engineer</span> based in{" "}
|
||||||
</div>
|
<span class="text-yellow">Brooklyn, NY</span>
|
||||||
</Typewriter>
|
</div>
|
||||||
<Typewriter speed={100}>
|
</Typewriter>
|
||||||
I'm a passionate dev tooling, game, and open source software developer.
|
<Typewriter speed={100} keepAlive={2000}>
|
||||||
Recently been working in the world of{" "}
|
I'm a passionate dev tooling, game, and open source software
|
||||||
<a
|
developer. Recently been working in the world of{" "}
|
||||||
href="https://www.love2d.org"
|
<a
|
||||||
class="text-blue hover-underline-animation"
|
href="https://www.love2d.org"
|
||||||
>
|
class="text-blue hover-underline-animation"
|
||||||
LÖVE
|
>
|
||||||
</a>{" "}
|
LÖVE
|
||||||
</Typewriter>
|
</a>{" "}
|
||||||
You can see some of my work <a>here</a>(github)
|
</Typewriter>
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
|
||||||
<div>My Collection of By-the-ways:</div>
|
<Typewriter speed={100} keepAlive={2000}>
|
||||||
</Typewriter>
|
You can see some of my work{" "}
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
<a
|
||||||
<ul class="list-disc pl-8">
|
href="https://github.com/mikefreno"
|
||||||
<li>I use Neovim</li>
|
class="text-blue hover-underline-animation"
|
||||||
<li>I use Arch Linux</li>
|
>
|
||||||
<li>I use Rust</li>
|
here (github)
|
||||||
</ul>
|
</a>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end gap-4">
|
||||||
|
<Typewriter speed={50} keepAlive={false}>
|
||||||
|
<div>My Collection of By-the-ways:</div>
|
||||||
|
</Typewriter>
|
||||||
|
<Typewriter speed={50} keepAlive={false}>
|
||||||
|
<ul class="list-disc pl-8">
|
||||||
|
<li>I use Neovim</li>
|
||||||
|
<li>I use Arch Linux</li>
|
||||||
|
<li>I use Rust</li>
|
||||||
|
</ul>
|
||||||
|
</Typewriter>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { createTRPCRouter, publicProcedure } from "../utils";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { env } from "~/env/server";
|
import { env } from "~/env/server";
|
||||||
import { ConnectionFactory, getUserID, hashPassword, checkPassword } from "~/server/utils";
|
import {
|
||||||
|
ConnectionFactory,
|
||||||
|
getUserID,
|
||||||
|
hashPassword,
|
||||||
|
checkPassword
|
||||||
|
} from "~/server/utils";
|
||||||
import { setCookie } from "vinxi/http";
|
import { setCookie } from "vinxi/http";
|
||||||
import type { User } from "~/types/user";
|
import type { User } from "~/types/user";
|
||||||
import { toUserProfile } from "~/types/user";
|
import { toUserProfile } from "~/types/user";
|
||||||
@@ -15,20 +20,20 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Not authenticated",
|
message: "Not authenticated"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId],
|
args: [userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +50,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Not authenticated",
|
message: "Not authenticated"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,20 +59,20 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET email = ?, email_verified = ? WHERE id = ?",
|
sql: "UPDATE User SET email = ?, email_verified = ? WHERE id = ?",
|
||||||
args: [email, 0, userId],
|
args: [email, 0, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch updated user
|
// Fetch updated user
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId],
|
args: [userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = res.rows[0] as unknown as User;
|
const user = res.rows[0] as unknown as User;
|
||||||
|
|
||||||
// Set email cookie for verification flow
|
// Set email cookie for verification flow
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", email, {
|
setCookie(ctx.event.nativeEvent, "emailToken", email, {
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
|
|
||||||
return toUserProfile(user);
|
return toUserProfile(user);
|
||||||
@@ -82,7 +87,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Not authenticated",
|
message: "Not authenticated"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +96,13 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET display_name = ? WHERE id = ?",
|
sql: "UPDATE User SET display_name = ? WHERE id = ?",
|
||||||
args: [displayName, userId],
|
args: [displayName, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch updated user
|
// Fetch updated user
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId],
|
args: [userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = res.rows[0] as unknown as User;
|
const user = res.rows[0] as unknown as User;
|
||||||
@@ -106,14 +111,14 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Update profile image
|
// Update profile image
|
||||||
updateProfileImage: publicProcedure
|
updateProfileImage: publicProcedure
|
||||||
.input(z.object({ imageUrl: z.string().url() }))
|
.input(z.object({ imageUrl: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = await getUserID(ctx.event.nativeEvent);
|
const userId = await getUserID(ctx.event.nativeEvent);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Not authenticated",
|
message: "Not authenticated"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,13 +127,13 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET image = ? WHERE id = ?",
|
sql: "UPDATE User SET image = ? WHERE id = ?",
|
||||||
args: [imageUrl, userId],
|
args: [imageUrl, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch updated user
|
// Fetch updated user
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId],
|
args: [userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = res.rows[0] as unknown as User;
|
const user = res.rows[0] as unknown as User;
|
||||||
@@ -141,8 +146,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
oldPassword: z.string(),
|
oldPassword: z.string(),
|
||||||
newPassword: z.string().min(8),
|
newPassword: z.string().min(8),
|
||||||
newPasswordConfirmation: z.string().min(8),
|
newPasswordConfirmation: z.string().min(8)
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = await getUserID(ctx.event.nativeEvent);
|
const userId = await getUserID(ctx.event.nativeEvent);
|
||||||
@@ -150,7 +155,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Not authenticated",
|
message: "Not authenticated"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,20 +164,20 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (newPassword !== newPasswordConfirmation) {
|
if (newPassword !== newPasswordConfirmation) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Password Mismatch",
|
message: "Password Mismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId],
|
args: [userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,16 +186,19 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!user.password_hash) {
|
if (!user.password_hash) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "No password set",
|
message: "No password set"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatch = await checkPassword(oldPassword, user.password_hash);
|
const passwordMatch = await checkPassword(
|
||||||
|
oldPassword,
|
||||||
|
user.password_hash
|
||||||
|
);
|
||||||
|
|
||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Password did not match record",
|
message: "Password did not match record"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,17 +206,17 @@ export const userRouter = createTRPCRouter({
|
|||||||
const newPasswordHash = await hashPassword(newPassword);
|
const newPasswordHash = await hashPassword(newPassword);
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
||||||
args: [newPasswordHash, userId],
|
args: [newPasswordHash, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear session cookies (force re-login)
|
// Clear session cookies (force re-login)
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, message: "success" };
|
return { success: true, message: "success" };
|
||||||
@@ -219,8 +227,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
newPassword: z.string().min(8),
|
newPassword: z.string().min(8),
|
||||||
newPasswordConfirmation: z.string().min(8),
|
newPasswordConfirmation: z.string().min(8)
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = await getUserID(ctx.event.nativeEvent);
|
const userId = await getUserID(ctx.event.nativeEvent);
|
||||||
@@ -228,7 +236,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Not authenticated",
|
message: "Not authenticated"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,20 +245,20 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (newPassword !== newPasswordConfirmation) {
|
if (newPassword !== newPasswordConfirmation) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Password Mismatch",
|
message: "Password Mismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId],
|
args: [userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +267,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (user.password_hash) {
|
if (user.password_hash) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Password exists",
|
message: "Password exists"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,17 +275,17 @@ export const userRouter = createTRPCRouter({
|
|||||||
const passwordHash = await hashPassword(newPassword);
|
const passwordHash = await hashPassword(newPassword);
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
||||||
args: [passwordHash, userId],
|
args: [passwordHash, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear session cookies (force re-login)
|
// Clear session cookies (force re-login)
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, message: "success" };
|
return { success: true, message: "success" };
|
||||||
@@ -292,7 +300,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Not authenticated",
|
message: "Not authenticated"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,13 +309,13 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId],
|
args: [userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +324,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!user.password_hash) {
|
if (!user.password_hash) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Password required",
|
message: "Password required"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +333,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Password Did Not Match",
|
message: "Password Did Not Match"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,19 +347,19 @@ export const userRouter = createTRPCRouter({
|
|||||||
provider = ?,
|
provider = ?,
|
||||||
image = ?
|
image = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
args: [null, 0, null, "user deleted", null, null, userId],
|
args: [null, 0, null, "user deleted", null, null, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear session cookies
|
// Clear session cookies
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, message: "deleted" };
|
return { success: true, message: "deleted" };
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user