First Commit - Portfolio Page
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import ReactGA from 'react-ga4';
|
||||
|
||||
const TRACKING_ID = 'G-N0PZBL7X18';
|
||||
|
||||
export const initGA = () => {
|
||||
ReactGA.initialize(TRACKING_ID);
|
||||
};
|
||||
|
||||
export const logPageView = () => {
|
||||
ReactGA.send({ hitType: "pageview", page: window.location.pathname });
|
||||
};
|
||||
|
||||
export const trackEvent = (category: string, action: string, label?: string) => {
|
||||
ReactGA.event({
|
||||
category,
|
||||
action,
|
||||
label
|
||||
});
|
||||
};
|
||||
|
||||
export const trackConversion = (value?: number) => {
|
||||
ReactGA.event({
|
||||
category: 'Conversion',
|
||||
action: 'Complete',
|
||||
value
|
||||
});
|
||||
};
|
||||
|
||||
export const trackTiming = (category: string, variable: string, value: number) => {
|
||||
ReactGA.event({
|
||||
category,
|
||||
action: 'timing',
|
||||
label: variable,
|
||||
value
|
||||
});
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
initializeAnalytics: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Make analytics initialization available globally for the cookie consent
|
||||
window.initializeAnalytics = () => {
|
||||
initGA();
|
||||
logPageView();
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { handleError, createAPIError } from './errorHandling';
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export async function fetchWithTimeout(
|
||||
resource: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<Response> {
|
||||
const { timeout = 8000, ...fetchOptions } = options;
|
||||
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(resource, {
|
||||
...fetchOptions,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw createAPIError(
|
||||
response.status,
|
||||
`HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw handleError(error, 'API Request');
|
||||
} finally {
|
||||
clearTimeout(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { supabase } from './supabaseClient';
|
||||
import { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
export const checkAuth = async (navigate: NavigateFunction) => {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
navigate('/login');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const signOut = async (navigate: NavigateFunction) => {
|
||||
await supabase.auth.signOut();
|
||||
navigate('/login');
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
type CacheItem<T> = {
|
||||
value: T;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
};
|
||||
|
||||
class Cache {
|
||||
private storage: Map<string, CacheItem<any>>;
|
||||
private readonly defaultTTL: number;
|
||||
|
||||
constructor(defaultTTL = 5 * 60 * 1000) { // 5 minutes default TTL
|
||||
this.storage = new Map();
|
||||
this.defaultTTL = defaultTTL;
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T, ttl = this.defaultTTL): void {
|
||||
this.storage.set(key, {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
ttl
|
||||
});
|
||||
}
|
||||
|
||||
get<T>(key: string): T | null {
|
||||
const item = this.storage.get(key);
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
if (Date.now() > item.timestamp + item.ttl) {
|
||||
this.storage.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.value;
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.get(key) !== null;
|
||||
}
|
||||
|
||||
delete(key: string): void {
|
||||
this.storage.delete(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const cache = new Cache();
|
||||
@@ -0,0 +1,23 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// CSRF token storage
|
||||
let csrfToken: string | null = null;
|
||||
|
||||
// Generate a new CSRF token
|
||||
export const generateCsrfToken = (): string => {
|
||||
csrfToken = uuidv4();
|
||||
return csrfToken;
|
||||
};
|
||||
|
||||
// Validate a CSRF token
|
||||
export const validateCsrfToken = (token: string): boolean => {
|
||||
return token === csrfToken;
|
||||
};
|
||||
|
||||
// Get the current CSRF token
|
||||
export const getCsrfToken = (): string => {
|
||||
if (!csrfToken) {
|
||||
return generateCsrfToken();
|
||||
}
|
||||
return csrfToken;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { trackEvent } from './analytics';
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public severity: 'error' | 'warning' | 'info' = 'error',
|
||||
public metadata?: Record<string, any>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AppError';
|
||||
}
|
||||
}
|
||||
|
||||
export const errorCodes = {
|
||||
NETWORK_ERROR: 'ERR_NETWORK',
|
||||
API_ERROR: 'ERR_API',
|
||||
AUTH_ERROR: 'ERR_AUTH',
|
||||
VALIDATION_ERROR: 'ERR_VALIDATION',
|
||||
UNKNOWN_ERROR: 'ERR_UNKNOWN'
|
||||
} as const;
|
||||
|
||||
export const handleError = (error: unknown, context: string) => {
|
||||
let appError: AppError;
|
||||
|
||||
if (error instanceof AppError) {
|
||||
appError = error;
|
||||
} else if (error instanceof Error) {
|
||||
appError = new AppError(
|
||||
error.message,
|
||||
errorCodes.UNKNOWN_ERROR,
|
||||
'error',
|
||||
{ originalError: error }
|
||||
);
|
||||
} else {
|
||||
appError = new AppError(
|
||||
'An unexpected error occurred',
|
||||
errorCodes.UNKNOWN_ERROR,
|
||||
'error',
|
||||
{ originalError: error }
|
||||
);
|
||||
}
|
||||
|
||||
// Track error in analytics
|
||||
trackEvent('Error', appError.code, `${context}: ${appError.message}`);
|
||||
|
||||
// Log error for debugging
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(`[${context}]`, appError);
|
||||
}
|
||||
|
||||
return appError;
|
||||
};
|
||||
|
||||
export const createAPIError = (status: number, message: string) => {
|
||||
return new AppError(
|
||||
message,
|
||||
errorCodes.API_ERROR,
|
||||
'error',
|
||||
{ status }
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
interface RateLimitEntry {
|
||||
count: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const WINDOW_SIZE_MS = 3600000; // 1 hour
|
||||
const MAX_REQUESTS = 5; // Maximum requests per hour
|
||||
|
||||
class RateLimiter {
|
||||
private cache: Map<string, RateLimitEntry>;
|
||||
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
isRateLimited(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const entry = this.cache.get(ip);
|
||||
|
||||
if (!entry) {
|
||||
this.cache.set(ip, { count: 1, timestamp: now });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (now - entry.timestamp > WINDOW_SIZE_MS) {
|
||||
this.cache.set(ip, { count: 1, timestamp: now });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.count >= MAX_REQUESTS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
return false;
|
||||
}
|
||||
|
||||
getRemainingAttempts(ip: string): number {
|
||||
const entry = this.cache.get(ip);
|
||||
if (!entry) {
|
||||
return MAX_REQUESTS;
|
||||
}
|
||||
return Math.max(0, MAX_REQUESTS - entry.count);
|
||||
}
|
||||
|
||||
getTimeToReset(ip: string): number {
|
||||
const entry = this.cache.get(ip);
|
||||
if (!entry) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, WINDOW_SIZE_MS - (Date.now() - entry.timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
export const rateLimiter = new RateLimiter();
|
||||
@@ -0,0 +1,27 @@
|
||||
export function register() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${import.meta.env.BASE_URL}sw.js`;
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((_registration) => {
|
||||
console.log('ServiceWorker registration successful');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('ServiceWorker registration failed:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = 'https://mxadgucxhmstlzsbgmoz.supabase.co';
|
||||
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im14YWRndWN4aG1zdGx6c2JnbW96Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwMDgwNjIsImV4cCI6MjA1NDU4NDA2Mn0.MmFqEm7OfwzOlSegLYl9YWLCmIp8YajzK3Aozubn66Q';
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
Reference in New Issue
Block a user