removed excess comments
This commit is contained in:
@@ -2,7 +2,6 @@ import { createTRPCProxyClient, httpBatchLink, loggerLink } from "@trpc/client";
|
||||
import type { AppRouter } from "~/server/api/root";
|
||||
|
||||
const getBaseUrl = () => {
|
||||
// Browser: use relative URL
|
||||
if (typeof window !== "undefined") return "";
|
||||
|
||||
const domain = import.meta.env.VITE_DOMAIN;
|
||||
@@ -27,12 +26,10 @@ function getCSRFToken(): string | undefined {
|
||||
|
||||
export const api = createTRPCProxyClient<AppRouter>({
|
||||
links: [
|
||||
// Only enable logging in development mode
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? [
|
||||
loggerLink({
|
||||
enabled: (opts) => {
|
||||
// Suppress 401 UNAUTHORIZED errors from logs
|
||||
const is401 =
|
||||
opts.direction === "down" &&
|
||||
opts.result instanceof Error &&
|
||||
@@ -42,7 +39,6 @@ export const api = createTRPCProxyClient<AppRouter>({
|
||||
})
|
||||
]
|
||||
: []),
|
||||
// identifies what url will handle trpc requests
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers: () => {
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Safe fetch wrapper that suppresses console errors for expected 401 responses
|
||||
* Use this instead of direct fetch() calls when 401s are expected (e.g., auth checks)
|
||||
* @param input - URL or Request object
|
||||
* @param init - Fetch options
|
||||
* @returns Promise<Response>
|
||||
* Fetch wrapper for auth checks where 401s are expected and should not trigger console errors
|
||||
*/
|
||||
export async function safeFetch(
|
||||
input: RequestInfo | URL,
|
||||
@@ -18,17 +14,12 @@ export async function safeFetch(
|
||||
const response = await fetch(input, init);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Re-throw the error - this is for actual network failures
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts soft hyphens (­) into long words to enable manual hyphenation
|
||||
* Works with Typewriter component since it uses actual characters
|
||||
* @param text - The text to add hyphens to
|
||||
* @param minWordLength - Minimum word length to hyphenate (default 8)
|
||||
* @returns Text with soft hyphens inserted
|
||||
* Inserts soft hyphens (­) for manual hyphenation. Uses actual characters for Typewriter compatibility.
|
||||
*/
|
||||
export function insertSoftHyphens(
|
||||
text: string,
|
||||
@@ -37,26 +28,20 @@ export function insertSoftHyphens(
|
||||
return text
|
||||
.split(" ")
|
||||
.map((word) => {
|
||||
// Skip short words
|
||||
if (word.length < minWordLength) return word;
|
||||
|
||||
// Common English hyphenation patterns
|
||||
const patterns = [
|
||||
// Prefixes (break after)
|
||||
{
|
||||
pattern:
|
||||
/^(un|re|in|dis|en|non|pre|pro|anti|de|mis|over|sub|super|trans|under)(.+)/i,
|
||||
split: 1
|
||||
},
|
||||
// Suffixes (break before)
|
||||
{
|
||||
pattern:
|
||||
/(.+)(ing|tion|sion|ness|ment|able|ible|ful|less|ship|hood|ward|like)$/i,
|
||||
split: 1
|
||||
},
|
||||
// Double consonants (break between)
|
||||
{ pattern: /(.+[aeiou])([bcdfghjklmnpqrstvwxyz])\2(.+)/i, split: 2 },
|
||||
// Compound words with common parts
|
||||
{
|
||||
pattern:
|
||||
/(.+)(stand|work|time|place|where|thing|back|over|under|out)$/i,
|
||||
@@ -68,16 +53,13 @@ export function insertSoftHyphens(
|
||||
const match = word.match(pattern);
|
||||
if (match) {
|
||||
if (split === 1) {
|
||||
// Break after first capture group
|
||||
return match[1] + "\u00AD" + match[2];
|
||||
} else if (split === 2) {
|
||||
// Break between doubled consonants
|
||||
return match[1] + match[2] + "\u00AD" + match[2] + match[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Insert soft hyphen every 6-8 characters in very long words
|
||||
if (word.length > 12) {
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < word.length; i += 6) {
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
/**
|
||||
* Comment System Utility Functions
|
||||
*
|
||||
* Shared utility functions for:
|
||||
* - Comment sorting algorithms
|
||||
* - Comment filtering and tree building
|
||||
* - Debouncing
|
||||
*/
|
||||
|
||||
import type { Comment, CommentReaction, SortingMode } from "~/types/comment";
|
||||
import { getSQLFormattedDate } from "./date-utils";
|
||||
|
||||
export { getSQLFormattedDate };
|
||||
|
||||
// ============================================================================
|
||||
// Comment Tree Utilities
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Gets all child comments for a given parent comment ID
|
||||
*/
|
||||
export function getChildComments(
|
||||
parentCommentID: number,
|
||||
allComments: Comment[] | undefined
|
||||
@@ -30,9 +14,6 @@ export function getChildComments(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of comments including all nested children
|
||||
*/
|
||||
export function getTotalCommentCount(
|
||||
topLevelComments: Comment[],
|
||||
allComments: Comment[]
|
||||
@@ -40,10 +21,6 @@ export function getTotalCommentCount(
|
||||
return allComments.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the nesting level of a comment in the tree
|
||||
* Top-level comments (parent_comment_id = -1 or null) are level 0
|
||||
*/
|
||||
export function getCommentLevel(
|
||||
comment: Comment,
|
||||
allComments: Comment[]
|
||||
@@ -66,17 +43,9 @@ export function getCommentLevel(
|
||||
return level;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Comment Sorting Algorithms
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @deprecated Server-side sorting is now implemented in the blog post route.
|
||||
* Comments are sorted by SQL queries for better performance.
|
||||
* This function remains for backward compatibility only.
|
||||
*
|
||||
* Calculates "hot" score for a comment based on votes and time
|
||||
* Uses logarithmic decay for older comments
|
||||
* @deprecated Server-side SQL sorting preferred for performance
|
||||
* Logarithmic decay formula: score / log(age + 2)
|
||||
*/
|
||||
function calculateHotScore(
|
||||
upvotes: number,
|
||||
@@ -88,17 +57,11 @@ function calculateHotScore(
|
||||
const commentTime = new Date(date).getTime();
|
||||
const ageInHours = (now - commentTime) / (1000 * 60 * 60);
|
||||
|
||||
// Logarithmic decay: score / log(age + 2)
|
||||
// Adding 2 prevents division by zero for very new comments
|
||||
return score / Math.log10(ageInHours + 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Server-side sorting is now implemented in the blog post route.
|
||||
* Use SQL-based sorting instead for better performance.
|
||||
* This function remains for backward compatibility only.
|
||||
*
|
||||
* Counts upvotes for a comment from reaction map
|
||||
* @deprecated Server-side SQL sorting preferred for performance
|
||||
*/
|
||||
function getUpvoteCount(
|
||||
commentID: number,
|
||||
@@ -111,11 +74,7 @@ function getUpvoteCount(
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Server-side sorting is now implemented in the blog post route.
|
||||
* Use SQL-based sorting instead for better performance.
|
||||
* This function remains for backward compatibility only.
|
||||
*
|
||||
* Counts downvotes for a comment from reaction map
|
||||
* @deprecated Server-side SQL sorting preferred for performance
|
||||
*/
|
||||
function getDownvoteCount(
|
||||
commentID: number,
|
||||
@@ -128,18 +87,7 @@ function getDownvoteCount(
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Server-side sorting is now implemented in the blog post route.
|
||||
* Comments are now sorted by SQL queries in src/routes/blog/[title]/index.tsx
|
||||
* for better performance and reduced client-side processing.
|
||||
* This function remains for backward compatibility only.
|
||||
*
|
||||
* Sorts comments based on the selected sorting mode
|
||||
*
|
||||
* Modes:
|
||||
* - newest: Most recent first
|
||||
* - oldest: Oldest first
|
||||
* - highest_rated: Most upvotes minus downvotes
|
||||
* - hot: Combines votes and recency (Reddit-style)
|
||||
* @deprecated Use server-side SQL sorting in routes/blog/[title]/index.tsx for better performance
|
||||
*/
|
||||
export function sortComments(
|
||||
comments: Comment[],
|
||||
@@ -190,20 +138,10 @@ export function sortComments(
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation Utilities
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Validates that a comment body meets requirements
|
||||
*/
|
||||
export function isValidCommentBody(body: string): boolean {
|
||||
return body.trim().length > 0 && body.length <= 10000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user can modify (edit/delete) a comment
|
||||
*/
|
||||
export function canModifyComment(
|
||||
userID: string,
|
||||
commenterID: string,
|
||||
@@ -214,9 +152,6 @@ export function canModifyComment(
|
||||
return userID === commenterID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user can delete with database-level deletion
|
||||
*/
|
||||
export function canDatabaseDelete(
|
||||
privilegeLevel: "admin" | "user" | "anonymous"
|
||||
): boolean {
|
||||
|
||||
@@ -12,7 +12,6 @@ export function createWindowWidth(debounceMs?: number): Accessor<number> {
|
||||
const [width, setWidth] = createSignal(initialWidth);
|
||||
|
||||
onMount(() => {
|
||||
// Sync to actual client width immediately on mount to avoid hydration mismatch
|
||||
setWidth(window.innerWidth);
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
@@ -86,7 +85,6 @@ export async function resizeImage(
|
||||
img.onload = () => {
|
||||
let { width, height } = img;
|
||||
|
||||
// Calculate new dimensions maintaining aspect ratio
|
||||
if (width > maxWidth || height > maxHeight) {
|
||||
const aspectRatio = width / height;
|
||||
|
||||
@@ -102,10 +100,8 @@ export async function resizeImage(
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// Draw image on canvas with new dimensions
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Convert canvas to blob
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
@@ -123,7 +119,6 @@ export async function resizeImage(
|
||||
reject(new Error("Failed to load image"));
|
||||
};
|
||||
|
||||
// Load image from file
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
|
||||
@@ -14,7 +14,6 @@ export default async function AddImageToS3(
|
||||
try {
|
||||
const filename = (file as File).name;
|
||||
|
||||
// Get pre-signed URL from tRPC endpoint
|
||||
const { uploadURL, key } = await api.misc.getPreSignedURL.mutate({
|
||||
type,
|
||||
title,
|
||||
@@ -23,11 +22,9 @@ export default async function AddImageToS3(
|
||||
|
||||
console.log("url: " + uploadURL, "key: " + key);
|
||||
|
||||
// Extract content type from filename extension
|
||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
const contentType = ext ? `image/${ext[1]}` : "application/octet-stream";
|
||||
|
||||
// Upload original file to S3 using pre-signed URL
|
||||
const uploadResponse = await fetch(uploadURL, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
@@ -40,19 +37,12 @@ export default async function AddImageToS3(
|
||||
throw new Error("Failed to upload file to S3");
|
||||
}
|
||||
|
||||
// For blog cover images, also create and upload a thumbnail
|
||||
if (type === "blog") {
|
||||
try {
|
||||
// Create thumbnail (max 200x200px for sidebar display)
|
||||
const thumbnail = await resizeImage(file, 200, 200, 0.8);
|
||||
|
||||
// Generate thumbnail filename: insert "-small" before extension
|
||||
const thumbnailFilename = filename.replace(
|
||||
/(\.[^.]+)$/,
|
||||
"-small$1"
|
||||
);
|
||||
const thumbnailFilename = filename.replace(/(\.[^.]+)$/, "-small$1");
|
||||
|
||||
// Get pre-signed URL for thumbnail
|
||||
const { uploadURL: thumbnailUploadURL } =
|
||||
await api.misc.getPreSignedURL.mutate({
|
||||
type,
|
||||
@@ -60,24 +50,21 @@ export default async function AddImageToS3(
|
||||
filename: thumbnailFilename
|
||||
});
|
||||
|
||||
// Upload thumbnail to S3
|
||||
const thumbnailUploadResponse = await fetch(thumbnailUploadURL, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "image/jpeg" // Thumbnails are always JPEG
|
||||
"Content-Type": "image/jpeg"
|
||||
},
|
||||
body: thumbnail
|
||||
});
|
||||
|
||||
if (!thumbnailUploadResponse.ok) {
|
||||
console.error("Failed to upload thumbnail to S3");
|
||||
// Don't fail the entire upload if thumbnail fails
|
||||
} else {
|
||||
console.log("Thumbnail uploaded successfully");
|
||||
}
|
||||
} catch (thumbnailError) {
|
||||
console.error("Thumbnail creation/upload failed:", thumbnailError);
|
||||
// Don't fail the entire upload if thumbnail fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ export interface CommandContext {
|
||||
}
|
||||
|
||||
export const createTerminalCommands = (context: CommandContext) => {
|
||||
// Define available routes
|
||||
const routes = [
|
||||
{ path: "/", name: "home" },
|
||||
{ path: "/blog", name: "blog" },
|
||||
@@ -99,7 +98,6 @@ export const createTerminalCommands = (context: CommandContext) => {
|
||||
clear: {
|
||||
action: () => {
|
||||
context.addToHistory("clear", "", "info");
|
||||
// Clear will be handled by the component
|
||||
},
|
||||
description: "Clear terminal history"
|
||||
},
|
||||
@@ -231,7 +229,6 @@ export const createTerminalCommands = (context: CommandContext) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Add all cd variants for each route
|
||||
routes.forEach((route) => {
|
||||
commands[`cd ${route.name}`] = {
|
||||
action: () => context.navigate(route.path),
|
||||
|
||||
@@ -8,14 +8,11 @@ import { VALIDATION_CONFIG } from "~/config";
|
||||
* Validate email format
|
||||
*/
|
||||
export function isValidEmail(email: string): boolean {
|
||||
// Basic email format check
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional checks for invalid patterns
|
||||
// Reject consecutive dots
|
||||
if (email.includes("..")) {
|
||||
return false;
|
||||
}
|
||||
@@ -39,24 +36,20 @@ export function validatePassword(password: string): {
|
||||
const errors: string[] = [];
|
||||
let includesSpecial = false;
|
||||
|
||||
// Minimum length from config
|
||||
if (password.length < VALIDATION_CONFIG.MIN_PASSWORD_LENGTH) {
|
||||
errors.push(
|
||||
`Password must be at least ${VALIDATION_CONFIG.MIN_PASSWORD_LENGTH} characters long`
|
||||
);
|
||||
}
|
||||
|
||||
// Require uppercase letter (if configured)
|
||||
if (VALIDATION_CONFIG.PASSWORD_REQUIRE_UPPERCASE && !/[A-Z]/.test(password)) {
|
||||
errors.push("Password must contain at least one uppercase letter");
|
||||
}
|
||||
|
||||
// Require lowercase letter (always required for balanced security)
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push("Password must contain at least one lowercase letter");
|
||||
}
|
||||
|
||||
// Require number (if configured)
|
||||
if (VALIDATION_CONFIG.PASSWORD_REQUIRE_NUMBER && !/[0-9]/.test(password)) {
|
||||
errors.push("Password must contain at least one number");
|
||||
}
|
||||
@@ -64,12 +57,11 @@ export function validatePassword(password: string): {
|
||||
if (/[^A-Za-z0-9]/.test(password)) {
|
||||
includesSpecial = true;
|
||||
}
|
||||
// Require special character (if configured)
|
||||
|
||||
if (VALIDATION_CONFIG.PASSWORD_REQUIRE_SPECIAL && !includesSpecial) {
|
||||
errors.push("Password must contain at least one special character");
|
||||
}
|
||||
|
||||
// Check for common weak passwords
|
||||
const commonPasswords = [
|
||||
"password",
|
||||
"1234",
|
||||
@@ -94,7 +86,6 @@ export function validatePassword(password: string): {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate password strength
|
||||
let strength: PasswordStrength = "weak";
|
||||
|
||||
if (errors.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user