From 8f4fac422b6af239028a811fa630df3afbc7964c Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 19 Dec 2025 14:04:32 -0500 Subject: [PATCH] almost done --- src/components/Bars.tsx | 19 ++-- src/components/blog/TextEditor.tsx | 6 +- src/routes/account.tsx | 153 ++++++++++++++++++++++++++++- src/routes/contact.tsx | 24 ++--- src/routes/index.tsx | 77 +++++++++------ src/server/api/routers/user.ts | 100 ++++++++++--------- 6 files changed, 269 insertions(+), 110 deletions(-) diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index 457dc27..cd7192c 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -7,8 +7,7 @@ import { createResource, Show, For, - Suspense, - createMemo + Suspense } from "solid-js"; import { api } from "~/lib/api"; import { TerminalSplash } from "./TerminalSplash"; @@ -57,8 +56,8 @@ export function RightBarContent() { }); return ( -
- +
+
  • Contact Me @@ -113,7 +112,8 @@ export function RightBarContent() { {/* Git Activity Section */} }> -
    +
    +
    -
      +
      • Home
      • @@ -442,13 +442,12 @@ export function LeftBar() {
      - {/* Dark Mode Toggle */} -
      +
      +
      - {/* RightBar content on mobile */} -
      +
      diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index 77df348..abea53a 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -469,10 +469,10 @@ export default function TextEditor(props: TextEditorProps) { onClick={() => 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" > - ─ HR + ━━ HR
      @@ -481,7 +481,7 @@ export default function TextEditor(props: TextEditorProps) {
      ); diff --git a/src/routes/account.tsx b/src/routes/account.tsx index 593218d..3bc5f9d 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -3,6 +3,9 @@ import { useNavigate, cache, redirect } from "@solidjs/router"; import { getEvent } from "vinxi/http"; import Eye from "~/components/icons/Eye"; 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 { checkAuthStatus } from "~/server/utils"; @@ -71,6 +74,17 @@ export default function AccountPage() { createSignal(false); const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false); + // Profile image state + const [profileImage, setProfileImage] = createSignal( + undefined + ); + const [profileImageHolder, setProfileImageHolder] = createSignal< + string | null + >(null); + const [profileImageStateChange, setProfileImageStateChange] = + createSignal(false); + const [preSetHolder, setPreSetHolder] = createSignal(null); + // Form refs let oldPasswordRef: HTMLInputElement | undefined; let newPasswordRef: HTMLInputElement | undefined; @@ -90,6 +104,10 @@ export default function AccountPage() { const result = await response.json(); if (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) { @@ -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 const setEmailTrigger = async (e: Event) => { e.preventDefault(); @@ -375,6 +469,57 @@ export default function AccountPage() { Account Settings
    + {/* Profile Image Section */} +
    +
    +
    + Profile Image +
    +
    + + +
    +
    + +
    + +
    + Profile image updated! +
    +
    +
    +
    + +
    + {/* Email Section */}
    @@ -431,7 +576,7 @@ export default function AccountPage() { emailButtonLoading() || (currentUser().email !== null && !currentUser().emailVerified) - ? "bg-blue cursor-not-allowed brightness-50" + ? "bg-blue cursor-not-allowed brightness-75" : "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`} > @@ -482,7 +627,7 @@ export default function AccountPage() { disabled={displayNameButtonLoading()} class={`${ displayNameButtonLoading() - ? "bg-blue cursor-not-allowed brightness-50" + ? "bg-blue cursor-not-allowed brightness-75" : "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`} > @@ -610,7 +755,7 @@ export default function AccountPage() { disabled={passwordChangeLoading() || !passwordsMatch()} class={`${ passwordChangeLoading() || !passwordsMatch() - ? "bg-blue cursor-not-allowed brightness-50" + ? "bg-blue cursor-not-allowed brightness-75" : "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`} > @@ -670,7 +815,7 @@ export default function AccountPage() { disabled={deleteAccountButtonLoading()} class={`${ deleteAccountButtonLoading() - ? "bg-red cursor-not-allowed brightness-50" + ? "bg-red cursor-not-allowed brightness-75" : "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`} > diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx index cbc4579..1269ebe 100644 --- a/src/routes/contact.tsx +++ b/src/routes/contact.tsx @@ -105,15 +105,14 @@ export default function ContactPage() {
    - Feel free to use the form{" "} - {viewer() === "lineage" ? "below" : "above"}, I will respond as - quickly as possible, however, you may find an answer to your - question in the following. + 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 + 1. Personal Information
      @@ -130,7 +129,7 @@ export default function ContactPage() {
      - 2. Remote Backups + 2. Remote Backups
      Life and Lineage uses a per-user database approach for @@ -146,7 +145,7 @@ export default function ContactPage() {
      - 3. Cross Device Play + 3. Cross Device Play
      You can use the above mentioned remote-backups to save progress @@ -155,7 +154,7 @@ export default function ContactPage() {
      - 4. Online Requirements + 4. Online Requirements
      Currently, the only time you need to be online is for remote @@ -166,7 +165,7 @@ export default function ContactPage() {
      - 5. Microtransactions + 5. Microtransactions
      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...)
      - - - +
      - - -
      - -
      Hey!
      -
      - -
      - My name is Mike Freno, I'm a{" "} - Software Engineer based in{" "} - Brooklyn, NY -
      -
      - - I'm a passionate dev tooling, game, and open source software developer. - Recently been working in the world of{" "} - - LÖVE - {" "} - - You can see some of my work here(github) - -
      My Collection of By-the-ways:
      -
      - -
        -
      • I use Neovim
      • -
      • I use Arch Linux
      • -
      • I use Rust
      • -
      -
      +
      +
      + +
      Hey!
      +
      + +
      + My name is Mike Freno, I'm a{" "} + Software Engineer based in{" "} + Brooklyn, NY +
      +
      + + I'm a passionate dev tooling, game, and open source software + developer. Recently been working in the world of{" "} + + LÖVE + {" "} + + + + You can see some of my work{" "} + + here (github) + + +
      +
      + +
      My Collection of By-the-ways:
      +
      + +
        +
      • I use Neovim
      • +
      • I use Arch Linux
      • +
      • I use Rust
      • +
      +
      +
      ); } diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index 0f314e0..1625e11 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -2,7 +2,12 @@ import { createTRPCRouter, publicProcedure } from "../utils"; import { z } from "zod"; import { TRPCError } from "@trpc/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 type { User } from "~/types/user"; import { toUserProfile } from "~/types/user"; @@ -15,20 +20,20 @@ export const userRouter = createTRPCRouter({ if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Not authenticated", + message: "Not authenticated" }); } const conn = ConnectionFactory(); const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", - args: [userId], + args: [userId] }); if (res.rows.length === 0) { throw new TRPCError({ code: "NOT_FOUND", - message: "User not found", + message: "User not found" }); } @@ -45,7 +50,7 @@ export const userRouter = createTRPCRouter({ if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Not authenticated", + message: "Not authenticated" }); } @@ -54,20 +59,20 @@ export const userRouter = createTRPCRouter({ await conn.execute({ sql: "UPDATE User SET email = ?, email_verified = ? WHERE id = ?", - args: [email, 0, userId], + args: [email, 0, userId] }); // Fetch updated user const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", - args: [userId], + args: [userId] }); const user = res.rows[0] as unknown as User; // Set email cookie for verification flow setCookie(ctx.event.nativeEvent, "emailToken", email, { - path: "/", + path: "/" }); return toUserProfile(user); @@ -82,7 +87,7 @@ export const userRouter = createTRPCRouter({ if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Not authenticated", + message: "Not authenticated" }); } @@ -91,13 +96,13 @@ export const userRouter = createTRPCRouter({ await conn.execute({ sql: "UPDATE User SET display_name = ? WHERE id = ?", - args: [displayName, userId], + args: [displayName, userId] }); // Fetch updated user const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", - args: [userId], + args: [userId] }); const user = res.rows[0] as unknown as User; @@ -106,14 +111,14 @@ export const userRouter = createTRPCRouter({ // Update profile image updateProfileImage: publicProcedure - .input(z.object({ imageUrl: z.string().url() })) + .input(z.object({ imageUrl: z.string() })) .mutation(async ({ input, ctx }) => { const userId = await getUserID(ctx.event.nativeEvent); if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Not authenticated", + message: "Not authenticated" }); } @@ -122,13 +127,13 @@ export const userRouter = createTRPCRouter({ await conn.execute({ sql: "UPDATE User SET image = ? WHERE id = ?", - args: [imageUrl, userId], + args: [imageUrl, userId] }); // Fetch updated user const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", - args: [userId], + args: [userId] }); const user = res.rows[0] as unknown as User; @@ -141,8 +146,8 @@ export const userRouter = createTRPCRouter({ z.object({ oldPassword: z.string(), newPassword: z.string().min(8), - newPasswordConfirmation: z.string().min(8), - }), + newPasswordConfirmation: z.string().min(8) + }) ) .mutation(async ({ input, ctx }) => { const userId = await getUserID(ctx.event.nativeEvent); @@ -150,7 +155,7 @@ export const userRouter = createTRPCRouter({ if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Not authenticated", + message: "Not authenticated" }); } @@ -159,20 +164,20 @@ export const userRouter = createTRPCRouter({ if (newPassword !== newPasswordConfirmation) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Password Mismatch", + message: "Password Mismatch" }); } const conn = ConnectionFactory(); const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", - args: [userId], + args: [userId] }); if (res.rows.length === 0) { throw new TRPCError({ code: "NOT_FOUND", - message: "User not found", + message: "User not found" }); } @@ -181,16 +186,19 @@ export const userRouter = createTRPCRouter({ if (!user.password_hash) { throw new TRPCError({ 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) { throw new TRPCError({ 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); await conn.execute({ sql: "UPDATE User SET password_hash = ? WHERE id = ?", - args: [newPasswordHash, userId], + args: [newPasswordHash, userId] }); // Clear session cookies (force re-login) setCookie(ctx.event.nativeEvent, "emailToken", "", { maxAge: 0, - path: "/", + path: "/" }); setCookie(ctx.event.nativeEvent, "userIDToken", "", { maxAge: 0, - path: "/", + path: "/" }); return { success: true, message: "success" }; @@ -219,8 +227,8 @@ export const userRouter = createTRPCRouter({ .input( z.object({ newPassword: z.string().min(8), - newPasswordConfirmation: z.string().min(8), - }), + newPasswordConfirmation: z.string().min(8) + }) ) .mutation(async ({ input, ctx }) => { const userId = await getUserID(ctx.event.nativeEvent); @@ -228,7 +236,7 @@ export const userRouter = createTRPCRouter({ if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Not authenticated", + message: "Not authenticated" }); } @@ -237,20 +245,20 @@ export const userRouter = createTRPCRouter({ if (newPassword !== newPasswordConfirmation) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Password Mismatch", + message: "Password Mismatch" }); } const conn = ConnectionFactory(); const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", - args: [userId], + args: [userId] }); if (res.rows.length === 0) { throw new TRPCError({ code: "NOT_FOUND", - message: "User not found", + message: "User not found" }); } @@ -259,7 +267,7 @@ export const userRouter = createTRPCRouter({ if (user.password_hash) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Password exists", + message: "Password exists" }); } @@ -267,17 +275,17 @@ export const userRouter = createTRPCRouter({ const passwordHash = await hashPassword(newPassword); await conn.execute({ sql: "UPDATE User SET password_hash = ? WHERE id = ?", - args: [passwordHash, userId], + args: [passwordHash, userId] }); // Clear session cookies (force re-login) setCookie(ctx.event.nativeEvent, "emailToken", "", { maxAge: 0, - path: "/", + path: "/" }); setCookie(ctx.event.nativeEvent, "userIDToken", "", { maxAge: 0, - path: "/", + path: "/" }); return { success: true, message: "success" }; @@ -292,7 +300,7 @@ export const userRouter = createTRPCRouter({ if (!userId) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Not authenticated", + message: "Not authenticated" }); } @@ -301,13 +309,13 @@ export const userRouter = createTRPCRouter({ const res = await conn.execute({ sql: "SELECT * FROM User WHERE id = ?", - args: [userId], + args: [userId] }); if (res.rows.length === 0) { throw new TRPCError({ code: "NOT_FOUND", - message: "User not found", + message: "User not found" }); } @@ -316,7 +324,7 @@ export const userRouter = createTRPCRouter({ if (!user.password_hash) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Password required", + message: "Password required" }); } @@ -325,7 +333,7 @@ export const userRouter = createTRPCRouter({ if (!passwordMatch) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Password Did Not Match", + message: "Password Did Not Match" }); } @@ -339,19 +347,19 @@ export const userRouter = createTRPCRouter({ provider = ?, image = ? WHERE id = ?`, - args: [null, 0, null, "user deleted", null, null, userId], + args: [null, 0, null, "user deleted", null, null, userId] }); // Clear session cookies setCookie(ctx.event.nativeEvent, "emailToken", "", { maxAge: 0, - path: "/", + path: "/" }); setCookie(ctx.event.nativeEvent, "userIDToken", "", { maxAge: 0, - path: "/", + path: "/" }); return { success: true, message: "deleted" }; - }), + }) });