Mobile support to come.
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function validateRepoSlug(slug: string): boolean {
return /^[a-zA-Z0-9_-]+$/.test(slug);
}
/**
* helper to serialize objects that have non-string values into url parameter queries
*/
export function toQueryString(
params: Record<string, string | number | boolean> | undefined,
): string {
if (!params) {
return "";
}
const stringParams = Object.fromEntries(
Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => [key, String(value)]),
);
return new URLSearchParams(stringParams).toString();
}
export function timeAgo(date: Date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 60) return "just now";
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
if (seconds < 2592000) return `${Math.floor(seconds / 86400)}d ago`;
if (seconds < 31536000) return `${Math.floor(seconds / 2592000)}mo ago`;
return `${Math.floor(seconds / 31536000)}y ago`;
}
export function timeAgoFull(date: Date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 60) return "just now";
const minutes = Math.floor(seconds / 60);
if (seconds < 3600) {
return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
}
const hours = Math.floor(seconds / 3600);
if (seconds < 86400) {
return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
}
const days = Math.floor(seconds / 86400);
if (seconds < 2592000) {
return days === 1 ? "1 day ago" : `${days} days ago`;
}
const months = Math.floor(seconds / 2592000);
if (seconds < 31536000) {
return months === 1 ? "1 month ago" : `${months} months ago`;
}
const years = Math.floor(seconds / 31536000);
return years === 1 ? "1 year ago" : `${years} years ago`;
}
/**
* Format date header: "Today", "Yesterday", or "Jan 12"
*/
export function formatDateKey(dateKey: string): string {
const date = new Date(`${dateKey}T00:00:00`);
const now = new Date();
now.setHours(0, 0, 0, 0);
const diffTime = now.getTime() - date.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Yesterday";
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
});
}
/**
* Format date as "Jan 12, 2025"
*/
export function formatDate(date: Date): string {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
/**
* Format time from date: "2:30 PM"
*/
export function formatTime(date: Date): string {
return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
/**
* For use in formal settings
*/
export function formatDateTime(date: Date): string {
const datePart = date.toDateString(); // "Wed Jan 14 2026"
const monthDay = datePart.slice(4, 10); // "Jan 14"
const year = date.getFullYear();
let hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
const ampm = hours >= 12 ? "PM" : "AM";
hours = hours % 12 || 12; // Convert to 12-hour format, 0 becomes 12
return `${monthDay}, ${year} ${hours}:${minutes}:${seconds} ${ampm}`;
}
export function pluralize(count: number, word: string): string {
return `${count} ${word}${count === 1 ? "" : "s"}`;
}
const emailTester =
/^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
export function validateEmail(email: string): boolean {
if (!email) {
return false;
}
const emailParts = email.split("@");
if (emailParts.length !== 2) {
return false;
}
const account = emailParts[0];
const address = emailParts[1];
if (account.length > 64) {
return false;
} else if (address.length > 255) {
return false;
}
const domainParts = address.split(".");
if (domainParts.some((part) => part.length > 63)) {
return false;
}
return emailTester.test(email);
}
export function validateUsername(username: string): boolean {
return !!username && username.length >= 2;
}
export function validatePassword(password: string): boolean {
return !!password && password.length >= 8;
}
export function delay<T>(ms: number, value: T): Promise<T> {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
refactoring validate user -> hasUser and wiring up signup form
baepaul•3fea7211d ago
created temporary signup page
mikkel•5a9138b3d ago
styling login form + re organizing auth paths
baepaul•b49ac0c7d ago
landing page + subscribe-button
baepaul•72a686211d ago
yay comment styling largely there now...
baepaul•5af451e11d ago
wiring up question page and answers-card
baepaul•aabb8e712d ago
updating getQuestions to align with new schema + biome format updates
baepaul•0725b3712d ago
pluralize
baepaul•d53f98f12d ago
sat stat bar visualization and chunky header now
baepaul•091691925d ago
claening up code and highlighting entry on click
baepaul•378e40426d ago
wiring up commit history on sidepanel
baepaul•4f819e626d ago
claude code's poor prototypes.
baepaul•bc2263a27d ago
mocking repo-sidebar-commits to group by date
baepaul•ea11c5927d ago
interactive file header
baepaul•a26134d1mo ago
wiring up getRepositoryCommits into DAL
baepaul•39f349c1mo ago
wiring up file viewer
baepaul•c9556f11mo ago