Version 1.0 squash

This commit is contained in:
Kenan Alić
2025-10-21 18:45:02 +02:00
parent 4d52f14287
commit 6fdaa16610
30 changed files with 2534 additions and 15 deletions

51
web/src/lib/pocketbase.ts Normal file
View File

@@ -0,0 +1,51 @@
/**
* PocketBase Client Configuration
*
* Singleton PocketBase client instance with automatic environment detection.
* Connects to local instance in development, production URL in production.
*/
import PocketBase from "pocketbase";
/**
* PocketBase URL configuration based on environment
* Development: Local PocketBase instance (http://127.0.0.1:8090)
* Production: Production PocketBase instance
*
* @returns PocketBase API base URL
*/
const getPocketBaseUrl = (): string => {
const isDev = import.meta.env.MODE === "development";
if (isDev) {
return "http://localhost:8090";
}
/* Production URL - configure via build process or use relative URL */
return import.meta.env.VITE_POCKETBASE_URL || "http://localhost:8090";
};
/**
* Singleton PocketBase client instance
* Import this instance throughout the application for all PocketBase operations
*/
export const pb = new PocketBase(getPocketBaseUrl());
/**
* Disable auto-cancellation to prevent request interruptions
* By default, PocketBase cancels pending requests when new ones are made,
* which can cause issues with concurrent requests and real-time subscriptions
*/
pb.autoCancellation(false);
/**
* PocketBase collection names
* Centralized collection name constants to avoid typos and enable refactoring
*/
export const Collections = {
About: "about",
Companies: "companies",
Notices: "notices",
FloorPlans: "floor_plans",
Rooms: "rooms",
} as const;

65
web/src/lib/result.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* Result Utility
*
* Go-style error handling utilities for safer, more explicit error handling.
* Provides Result type and tryCatch wrappers to eliminate try-catch boilerplate.
*/
/**
* Go-style Result type for explicit error handling
* Returns [data, null] on success or [null, error] on failure
*
* @example
* const [data, error] = tryCatch(() => riskyOperation());
* if (error) {
* // Handle error
* return;
* }
* // Use data safely
*/
export type Result<T, E = Error> = [T, null] | [null, E];
/**
* Wraps a function in try-catch and returns Result tuple
* Similar to Go's (value, error) pattern
*
* @param fn Function to execute
* @returns Tuple of [result, null] or [null, error]
*/
export function tryCatch<T>(fn: () => T): Result<T> {
try {
const result = fn();
return [result, null];
} catch (error) {
return [null, error instanceof Error ? error : new Error(String(error))];
}
}
/**
* Async version of tryCatch for promises
*
* @param fn Async function to execute
* @returns Promise resolving to tuple of [result, null] or [null, error]
*/
export async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T>> {
try {
const result = await fn();
return [result, null];
} catch (error) {
return [null, error instanceof Error ? error : new Error(String(error))];
}
}
/**
* Safely executes a void function, ignoring any errors
* Perfect for cleanup operations like unsubscribe where errors don't matter
*
* @param fn Function to execute safely
*/
export function safely(fn: () => void): void {
try {
fn();
} catch {
/* Ignore errors during cleanup */
}
}

View File

@@ -1,6 +1,73 @@
/**
* Utility Functions
*
* Common helper functions for the application including:
* - UI styling utilities (shadcn)
* - Date formatting for notices
* - PocketBase file URL resolution
*/
/* Shadcn */
import { twMerge } from "tailwind-merge";
import { clsx, type ClassValue } from "clsx";
import type { RecordModel } from "pocketbase";
import { pb } from "./pocketbase";
/* Combines Tailwind classes with proper conflict resolution */
const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export { cn };
/**
* Formats PocketBase created timestamp to human-readable Bosnian format
* Examples: "Danas, 9:30", "Juče, 15:15", "18. Okt, 10:45"
*
* @param dateStr ISO 8601 datetime string from PocketBase
* @returns Formatted date string in Bosnian
*/
export function formatPostedDate(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const timeStr = date.toLocaleTimeString("bs-BA", {
hour: "2-digit",
minute: "2-digit",
});
if (diffDays === 0) {
return `Danas, ${timeStr}`;
}
if (diffDays === 1) {
return `Juče, ${timeStr}`;
}
const dateStr2 = date.toLocaleDateString("bs-BA", {
day: "numeric",
month: "short",
});
return `${dateStr2}, ${timeStr}`;
}
/**
* Checks if a notice has expired
*
* @param expiresAt ISO 8601 datetime string
* @returns true if the expiry date has passed, false otherwise
*/
export function isExpired(expiresAt: string): boolean {
return new Date() > new Date(expiresAt);
}
/**
* Generates PocketBase file URL for a record's file field
*
* @param record PocketBase record containing the file
* @param filename Name of the file field
* @returns Full URL to the file, or empty string if no file
*/
export function getFileUrl(record: RecordModel, filename: string): string {
return filename ? pb.files.getURL(record, filename) : "";
}