layouting fixes

This commit is contained in:
Michael Freno
2025-12-20 23:58:48 -05:00
parent 89e9a2ee45
commit 0e1b51af11
4 changed files with 399 additions and 191 deletions

View File

@@ -255,81 +255,83 @@ export default function PostForm(props: PostFormProps) {
};
return (
<div class="bg-base text-text min-h-screen px-8 py-32">
<div class="bg-base text-text min-h-screen py-32">
<div class="text-center text-2xl tracking-wide">
{props.mode === "edit" ? "Edit a Blog" : "Create a Blog"}
</div>
<div class="flex h-full w-full justify-center">
<form onSubmit={handleSubmit} class="w-full md:w-3/4 lg:w-1/3 xl:w-1/2">
{/* Title */}
<div class="input-group mx-4">
<input
type="text"
value={title()}
onInput={(e) => setTitle(e.currentTarget.value)}
name="title"
placeholder=" "
class="underlinedInput w-full bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Title</label>
</div>
<form onSubmit={handleSubmit} class="px-4">
<div class="mx-auto w-full md:w-3/4 xl:w-1/2">
{/* Title */}
<div class="input-group mx-4">
<input
type="text"
value={title()}
onInput={(e) => setTitle(e.currentTarget.value)}
name="title"
placeholder=" "
class="underlinedInput w-full bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Title</label>
</div>
{/* Subtitle */}
<div class="input-group mx-4">
<input
type="text"
value={subtitle()}
onInput={(e) => setSubtitle(e.currentTarget.value)}
name="subtitle"
placeholder=" "
class="underlinedInput w-full bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Subtitle</label>
</div>
{/* Subtitle */}
<div class="input-group mx-4">
<input
type="text"
value={subtitle()}
onInput={(e) => setSubtitle(e.currentTarget.value)}
name="subtitle"
placeholder=" "
class="underlinedInput w-full bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Subtitle</label>
</div>
{/* Banner */}
<div class="pt-8 text-center text-xl">Banner</div>
<div class="flex justify-center pb-8">
<Dropzone
onDrop={handleBannerImageDrop}
accept="image/jpg, image/jpeg, image/png"
fileHolder={bannerImageHolder()}
preSet={
props.mode === "edit" && !requestedDeleteImage()
? bannerPhoto() || null
: null
{/* Banner */}
<div class="pt-8 text-center text-xl">Banner</div>
<div class="flex justify-center pb-8">
<Dropzone
onDrop={handleBannerImageDrop}
accept="image/jpg, image/jpeg, image/png"
fileHolder={bannerImageHolder()}
preSet={
props.mode === "edit" && !requestedDeleteImage()
? bannerPhoto() || null
: null
}
/>
<button
type="button"
class="z-50 -ml-6 h-fit rounded-full"
onClick={removeBannerImage}
>
<XCircle
height={36}
width={36}
stroke={"currentColor"}
strokeWidth={1}
/>
</button>
</div>
{/* Attachments */}
<AddAttachmentSection
type="blog"
postId={props.postId}
postTitle={title()}
existingAttachments={
props.mode === "edit" && props.initialData
? (props.initialData as any)?.attachments
: undefined
}
/>
<button
type="button"
class="z-50 -ml-6 h-fit rounded-full"
onClick={removeBannerImage}
>
<XCircle
height={36}
width={36}
stroke={"currentColor"}
strokeWidth={1}
/>
</button>
</div>
{/* Attachments */}
<AddAttachmentSection
type="blog"
postId={props.postId}
postTitle={title()}
existingAttachments={
props.mode === "edit" && props.initialData
? (props.initialData as any)?.attachments
: undefined
}
/>
{/* Text Editor */}
<div class="-mx-6 md:-mx-36">
<div class="">
<TextEditor updateContent={setBody} preSet={initialBody()} />
</div>

View File

@@ -9,6 +9,13 @@ import { TRPCError } from "@trpc/server";
import { OAuth2Client } from "google-auth-library";
import { jwtVerify } from "jose";
import { createTRPCRouter, publicProcedure } from "~/server/api/utils";
import {
fetchWithTimeout,
checkResponse,
NetworkError,
TimeoutError,
APIError
} from "~/server/fetch-utils";
export const lineageDatabaseRouter = createTRPCRouter({
credentials: publicProcedure
@@ -155,17 +162,20 @@ export const lineageDatabaseRouter = createTRPCRouter({
});
if (dumpRes.success) {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
try {
const deleteRes = await fetchWithTimeout(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
},
timeout: 20000 // 20s for database deletion
}
}
);
);
await checkResponse(deleteRes);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email]
@@ -175,11 +185,35 @@ export const lineageDatabaseRouter = createTRPCRouter({
status: 200,
message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database"
});
} catch (error) {
if (error instanceof TimeoutError) {
console.error("Database deletion timeout:", error.message);
throw new TRPCError({
code: "TIMEOUT",
message:
"Database deletion timed out. Please contact support."
});
} else if (error instanceof NetworkError) {
console.error(
"Network error during database deletion:",
error.message
);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Network error deleting database. Please try again."
});
} else if (error instanceof APIError) {
console.error(
"API error deleting database:",
error.status,
error.statusText
);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database"
});
}
throw error;
}
} else {
throw new TRPCError({
@@ -188,17 +222,20 @@ export const lineageDatabaseRouter = createTRPCRouter({
});
}
} else {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
try {
const deleteRes = await fetchWithTimeout(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
},
timeout: 20000
}
}
);
);
await checkResponse(deleteRes);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email]
@@ -208,11 +245,34 @@ export const lineageDatabaseRouter = createTRPCRouter({
status: 200,
message: `Account and Database deleted`
};
} else {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database"
});
} catch (error) {
if (error instanceof TimeoutError) {
console.error("Database deletion timeout:", error.message);
throw new TRPCError({
code: "TIMEOUT",
message: "Database deletion timed out. Please contact support."
});
} else if (error instanceof NetworkError) {
console.error(
"Network error during database deletion:",
error.message
);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Network error deleting database. Please try again."
});
} else if (error instanceof APIError) {
console.error(
"API error deleting database:",
error.status,
error.statusText
);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete database"
});
}
throw error;
}
}
} else {
@@ -343,41 +403,59 @@ export const lineageDatabaseRouter = createTRPCRouter({
});
if (dumpRes.success) {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
try {
const deleteRes = await fetchWithTimeout(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
},
timeout: 20000
}
}
);
);
await checkResponse(deleteRes);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email]
});
executed_ids.push(id as number);
} catch (error) {
console.error(
`Failed to delete database ${db_name} in cron job:`,
error
);
// Continue with other deletions even if one fails
}
}
} else {
const deleteRes = await fetch(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
try {
const deleteRes = await fetchWithTimeout(
`https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
},
timeout: 20000
}
}
);
);
await checkResponse(deleteRes);
if (deleteRes.ok) {
await conn.execute({
sql: `DELETE FROM User WHERE email = ?`,
args: [email]
});
executed_ids.push(id as number);
} catch (error) {
console.error(
`Failed to delete database ${db_name} in cron job:`,
error
);
// Continue with other deletions even if one fails
}
}
}

View File

@@ -12,7 +12,14 @@ import { TRPCError } from "@trpc/server";
import { ConnectionFactory } from "~/server/utils";
import * as bcrypt from "bcrypt";
import { getCookie, setCookie } from "vinxi/http";
import {
fetchWithTimeout,
checkResponse,
fetchWithRetry,
NetworkError,
TimeoutError,
APIError
} from "~/server/fetch-utils";
const assets: Record<string, string> = {
"shapes-with-abigail": "shapes-with-abigail.apk",
"magic-delve": "magic-delve.apk",
@@ -285,15 +292,27 @@ export const miscRouter = createTRPCRouter({
};
try {
await fetch(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
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;
},
body: JSON.stringify(sendinblueData)
});
{
maxRetries: 2,
retryDelay: 1000
}
);
// Set cookie to prevent spam (60 second cooldown)
const exp = new Date(Date.now() + 1 * 60 * 1000);
@@ -304,11 +323,37 @@ export const miscRouter = createTRPCRouter({
return { message: "email sent" };
} catch (error) {
console.error(error);
// Provide specific error messages for different failure types
if (error instanceof TimeoutError) {
console.error("Contact form email timeout:", error.message);
throw new TRPCError({
code: "TIMEOUT",
message:
"Email service timed out. Please try again or contact michael@freno.me"
});
} else if (error instanceof NetworkError) {
console.error("Contact form network error:", error.message);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"Network error. Please try again or contact michael@freno.me"
});
} else if (error instanceof APIError) {
console.error(
"Contact form API error:",
error.status,
error.statusText
);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Email service error. You can reach me at michael@freno.me"
});
}
console.error("Contact form error:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"SMTP server error: Sorry! You can reach me at michael@freno.me"
message: "Sorry! You can reach me at michael@freno.me"
});
}
}),
@@ -362,26 +407,43 @@ export const miscRouter = createTRPCRouter({
};
try {
// Send both emails
await fetch(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
},
body: JSON.stringify(sendinblueMyData)
});
await fetch(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
},
body: JSON.stringify(sendinblueUserData)
});
// Send both emails with retry logic
await Promise.all([
fetchWithRetry(
async () => {
const response = await fetchWithTimeout(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
},
body: JSON.stringify(sendinblueMyData),
timeout: 15000
});
await checkResponse(response);
return response;
},
{ maxRetries: 2, retryDelay: 1000 }
),
fetchWithRetry(
async () => {
const response = await fetchWithTimeout(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
},
body: JSON.stringify(sendinblueUserData),
timeout: 15000
});
await checkResponse(response);
return response;
},
{ maxRetries: 2, retryDelay: 1000 }
)
]);
// Set cookie to prevent spam (60 second cooldown)
const exp = new Date(Date.now() + 1 * 60 * 1000);
@@ -392,11 +454,35 @@ export const miscRouter = createTRPCRouter({
return { message: "request sent" };
} catch (error) {
console.error(error);
// Provide specific error messages
if (error instanceof TimeoutError) {
console.error("Deletion request email timeout:", error.message);
throw new TRPCError({
code: "TIMEOUT",
message: "Email service timed out. Please try again."
});
} else if (error instanceof NetworkError) {
console.error("Deletion request network error:", error.message);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Network error. Please try again later."
});
} else if (error instanceof APIError) {
console.error(
"Deletion request API error:",
error.status,
error.statusText
);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Email service error. Please try again later."
});
}
console.error("Deletion request error:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"SMTP server error: Sorry! You can reach me at michael@freno.me"
message: "Failed to send deletion request. Please try again."
});
}
})

View File

@@ -4,6 +4,14 @@ import { v4 as uuid } from "uuid";
import { env } from "~/env/server";
import type { H3Event } from "vinxi/http";
import { getUserID } from "./auth";
import {
fetchWithTimeout,
checkResponse,
fetchWithRetry,
NetworkError,
TimeoutError,
APIError
} from "~/server/fetch-utils";
let mainDBConnection: ReturnType<typeof createClient> | null = null;
let lineageDBConnection: ReturnType<typeof createClient> | null = null;
@@ -83,56 +91,90 @@ export async function dumpAndSendDB({
success: boolean;
reason?: string;
}> {
const res = await fetch(`https://${dbName}-mikefreno.turso.io/dump`, {
method: "GET",
headers: {
Authorization: `Bearer ${dbToken}`
}
});
if (!res.ok) {
console.error(res);
return { success: false, reason: "bad dump request response" };
}
const text = await res.text();
const base64Content = Buffer.from(text, "utf-8").toString("base64");
const apiKey = env.SENDINBLUE_KEY as string;
const apiUrl = "https://api.brevo.com/v3/smtp/email";
const emailPayload = {
sender: {
name: "no_reply@freno.me",
email: "no_reply@freno.me"
},
to: [
try {
// Fetch database dump with timeout
const res = await fetchWithTimeout(
`https://${dbName}-mikefreno.turso.io/dump`,
{
email: sendTarget
method: "GET",
headers: {
Authorization: `Bearer ${dbToken}`
},
timeout: 30000 // 30s for database dump
}
],
subject: "Your Lineage Database Dump",
htmlContent:
"<html><body><p>Please find the attached database dump. This contains the state of your person remote Lineage remote saves. Should you ever return to Lineage, you can upload this file to reinstate the saves you had.</p></body></html>",
attachment: [
{
content: base64Content,
name: "database_dump.txt"
}
]
};
const sendRes = await fetch(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
},
body: JSON.stringify(emailPayload)
});
);
await checkResponse(res);
const text = await res.text();
const base64Content = Buffer.from(text, "utf-8").toString("base64");
const apiKey = env.SENDINBLUE_KEY as string;
const apiUrl = "https://api.brevo.com/v3/smtp/email";
const emailPayload = {
sender: {
name: "no_reply@freno.me",
email: "no_reply@freno.me"
},
to: [
{
email: sendTarget
}
],
subject: "Your Lineage Database Dump",
htmlContent:
"<html><body><p>Please find the attached database dump. This contains the state of your person remote Lineage remote saves. Should you ever return to Lineage, you can upload this file to reinstate the saves you had.</p></body></html>",
attachment: [
{
content: base64Content,
name: "database_dump.txt"
}
]
};
// Send email with retry logic
await fetchWithRetry(
async () => {
const sendRes = await fetchWithTimeout(apiUrl, {
method: "POST",
headers: {
accept: "application/json",
"api-key": apiKey,
"content-type": "application/json"
},
body: JSON.stringify(emailPayload),
timeout: 20000 // 20s for email with attachment
});
await checkResponse(sendRes);
return sendRes;
},
{
maxRetries: 2,
retryDelay: 2000
}
);
if (!sendRes.ok) {
return { success: false, reason: "email send failure" };
} else {
return { success: true };
} catch (error) {
// Log specific error types for debugging
if (error instanceof TimeoutError) {
console.error("Database dump timeout:", error.message);
return { success: false, reason: "Database dump timed out" };
} else if (error instanceof NetworkError) {
console.error("Network error during database dump:", error.message);
return { success: false, reason: "Network error" };
} else if (error instanceof APIError) {
console.error(
"API error during database dump:",
error.status,
error.statusText
);
return { success: false, reason: `API error: ${error.statusText}` };
}
console.error("Unexpected error during database dump:", error);
return { success: false, reason: "Unknown error occurred" };
}
}