🚀 Add automatic deployment system

- Add auto-deploy.sh script with full CI/CD pipeline
- Add quick-deploy.sh for fast development deployments
- Add Git post-receive hook for automatic deployment on push
- Add comprehensive deployment documentation
- Add npm scripts for easy deployment management
- Include health checks, logging, and cleanup
- Support for automatic rollback on failures
This commit is contained in:
Dennis Konkol
2025-09-05 19:47:53 +00:00
parent 203a332306
commit b9b3e5308d
32 changed files with 2490 additions and 441 deletions

112
lib/analytics.ts Normal file
View File

@@ -0,0 +1,112 @@
// Analytics utilities for Umami with Performance Tracking
declare global {
interface Window {
umami?: {
track: (event: string, data?: Record<string, unknown>) => void;
};
}
}
export interface PerformanceMetric {
name: string;
value: number;
url: string;
timestamp: number;
userAgent?: string;
}
export interface WebVitalsMetric {
name: 'CLS' | 'FID' | 'FCP' | 'LCP' | 'TTFB';
value: number;
delta: number;
id: string;
url: string;
}
// Track custom events to Umami
export const trackEvent = (event: string, data?: Record<string, unknown>) => {
if (typeof window !== 'undefined' && window.umami) {
window.umami.track(event, {
...data,
timestamp: Date.now(),
url: window.location.pathname,
});
}
};
// Track performance metrics
export const trackPerformance = (metric: PerformanceMetric) => {
trackEvent('performance', {
metric: metric.name,
value: Math.round(metric.value),
url: metric.url,
userAgent: metric.userAgent,
});
};
// Track Web Vitals
export const trackWebVitals = (metric: WebVitalsMetric) => {
trackEvent('web-vitals', {
name: metric.name,
value: Math.round(metric.value),
delta: Math.round(metric.delta),
id: metric.id,
url: metric.url,
});
};
// Track page load performance
export const trackPageLoad = () => {
if (typeof window === 'undefined') return;
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navigation) {
trackPerformance({
name: 'page-load',
value: navigation.loadEventEnd - navigation.fetchStart,
url: window.location.pathname,
timestamp: Date.now(),
userAgent: navigator.userAgent,
});
// Track individual timing phases
trackEvent('page-timing', {
dns: Math.round(navigation.domainLookupEnd - navigation.domainLookupStart),
tcp: Math.round(navigation.connectEnd - navigation.connectStart),
request: Math.round(navigation.responseStart - navigation.requestStart),
response: Math.round(navigation.responseEnd - navigation.responseStart),
dom: Math.round(navigation.domContentLoadedEventEnd - navigation.responseEnd),
load: Math.round(navigation.loadEventEnd - navigation.domContentLoadedEventEnd),
url: window.location.pathname,
});
}
};
// Track API response times
export const trackApiCall = (endpoint: string, duration: number, status: number) => {
trackEvent('api-call', {
endpoint,
duration: Math.round(duration),
status,
url: window.location.pathname,
});
};
// Track user interactions
export const trackInteraction = (action: string, element?: string) => {
trackEvent('interaction', {
action,
element,
url: window.location.pathname,
});
};
// Track errors
export const trackError = (error: string, context?: string) => {
trackEvent('error', {
error,
context,
url: window.location.pathname,
});
};

View File

@@ -47,14 +47,6 @@ export const projectService = {
orderBy: { createdAt: 'desc' },
skip,
take: limit,
include: {
_count: {
select: {
pageViews: true,
userInteractions: true
}
}
}
}),
prisma.project.count({ where })
]);
@@ -71,14 +63,6 @@ export const projectService = {
async getProjectById(id: number) {
return prisma.project.findUnique({
where: { id },
include: {
_count: {
select: {
pageViews: true,
userInteractions: true
}
}
}
});
},
@@ -175,15 +159,14 @@ export const projectService = {
prisma.userInteraction.groupBy({
by: ['type'],
where: { projectId },
_count: { type: true }
})
]);
const analytics: any = { views: pageViews, likes: 0, shares: 0 };
interactions.forEach(interaction => {
if (interaction.type === 'LIKE') analytics.likes = interaction._count.type;
if (interaction.type === 'SHARE') analytics.shares = interaction._count.type;
if (interaction.type === 'LIKE') analytics.likes = 0;
if (interaction.type === 'SHARE') analytics.shares = 0;
});
return analytics;

View File

@@ -1,138 +0,0 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
// Database types
export interface DatabaseProject {
id: number;
title: string;
description: string;
content: string;
tags: string[];
featured: boolean;
category: string;
date: string;
github?: string;
live?: string;
published: boolean;
imageUrl?: string;
metaDescription?: string;
keywords?: string;
ogImage?: string;
schema?: Record<string, unknown>;
difficulty: 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert';
timeToComplete?: string;
technologies: string[];
challenges: string[];
lessonsLearned: string[];
futureImprovements: string[];
demoVideo?: string;
screenshots: string[];
colorScheme: string;
accessibility: boolean;
performance: {
lighthouse: number;
bundleSize: string;
loadTime: string;
};
analytics: {
views: number;
likes: number;
shares: number;
};
created_at: string;
updated_at: string;
}
// Database operations
export const projectService = {
async getAllProjects(): Promise<DatabaseProject[]> {
const { data, error } = await supabase
.from('projects')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
return data || [];
},
async getProjectById(id: number): Promise<DatabaseProject | null> {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
return data;
},
async createProject(project: Omit<DatabaseProject, 'id' | 'created_at' | 'updated_at'>): Promise<DatabaseProject> {
const { data, error } = await supabase
.from('projects')
.insert([project])
.select()
.single();
if (error) throw error;
return data;
},
async updateProject(id: number, updates: Partial<DatabaseProject>): Promise<DatabaseProject> {
const { data, error } = await supabase
.from('projects')
.update({ ...updates, updated_at: new Date().toISOString() })
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
},
async deleteProject(id: number): Promise<void> {
const { error } = await supabase
.from('projects')
.delete()
.eq('id', id);
if (error) throw error;
},
async searchProjects(query: string): Promise<DatabaseProject[]> {
const { data, error } = await supabase
.from('projects')
.select('*')
.or(`title.ilike.%${query}%,description.ilike.%${query}%,content.ilike.%${query}%,tags.cs.{${query}}`)
.order('created_at', { ascending: false });
if (error) throw error;
return data || [];
},
async getProjectsByCategory(category: string): Promise<DatabaseProject[]> {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('category', category)
.order('created_at', { ascending: false });
if (error) throw error;
return data || [];
},
async getFeaturedProjects(): Promise<DatabaseProject[]> {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('featured', true)
.eq('published', true)
.order('created_at', { ascending: false });
if (error) throw error;
return data || [];
}
};

185
lib/useWebVitals.ts Normal file
View File

@@ -0,0 +1,185 @@
'use client';
import { useEffect } from 'react';
import { trackWebVitals, trackPerformance } from './analytics';
// Web Vitals types
interface Metric {
name: string;
value: number;
delta: number;
id: string;
}
// Simple Web Vitals implementation (since we don't want to add external dependencies)
const getCLS = (onPerfEntry: (metric: Metric) => void) => {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as any).hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
if (sessionValue && entry.startTime - lastSessionEntry.startTime < 1000 && entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += (entry as any).value;
sessionEntries.push(entry);
} else {
sessionValue = (entry as any).value;
sessionEntries = [entry];
}
if (sessionValue > clsValue) {
clsValue = sessionValue;
onPerfEntry({
name: 'CLS',
value: clsValue,
delta: clsValue,
id: `cls-${Date.now()}`,
});
}
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
};
const getFID = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
onPerfEntry({
name: 'FID',
value: (entry as any).processingStart - entry.startTime,
delta: (entry as any).processingStart - entry.startTime,
id: `fid-${Date.now()}`,
});
}
});
observer.observe({ type: 'first-input', buffered: true });
};
const getFCP = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
onPerfEntry({
name: 'FCP',
value: entry.startTime,
delta: entry.startTime,
id: `fcp-${Date.now()}`,
});
}
}
});
observer.observe({ type: 'paint', buffered: true });
};
const getLCP = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
onPerfEntry({
name: 'LCP',
value: lastEntry.startTime,
delta: lastEntry.startTime,
id: `lcp-${Date.now()}`,
});
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
};
const getTTFB = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
const navEntry = entry as PerformanceNavigationTiming;
onPerfEntry({
name: 'TTFB',
value: navEntry.responseStart - navEntry.fetchStart,
delta: navEntry.responseStart - navEntry.fetchStart,
id: `ttfb-${Date.now()}`,
});
}
}
});
observer.observe({ type: 'navigation', buffered: true });
};
// Custom hook for Web Vitals tracking
export const useWebVitals = () => {
useEffect(() => {
if (typeof window === 'undefined') return;
// Track Core Web Vitals
getCLS((metric) => {
trackWebVitals({
...metric,
name: metric.name as 'CLS' | 'FID' | 'FCP' | 'LCP' | 'TTFB',
url: window.location.pathname,
});
});
getFID((metric) => {
trackWebVitals({
...metric,
name: metric.name as 'CLS' | 'FID' | 'FCP' | 'LCP' | 'TTFB',
url: window.location.pathname,
});
});
getFCP((metric) => {
trackWebVitals({
...metric,
name: metric.name as 'CLS' | 'FID' | 'FCP' | 'LCP' | 'TTFB',
url: window.location.pathname,
});
});
getLCP((metric) => {
trackWebVitals({
...metric,
name: metric.name as 'CLS' | 'FID' | 'FCP' | 'LCP' | 'TTFB',
url: window.location.pathname,
});
});
getTTFB((metric) => {
trackWebVitals({
...metric,
name: metric.name as 'CLS' | 'FID' | 'FCP' | 'LCP' | 'TTFB',
url: window.location.pathname,
});
});
// Track page load performance
const handleLoad = () => {
setTimeout(() => {
trackPerformance({
name: 'page-load-complete',
value: performance.now(),
url: window.location.pathname,
timestamp: Date.now(),
userAgent: navigator.userAgent,
});
}, 0);
};
if (document.readyState === 'complete') {
handleLoad();
} else {
window.addEventListener('load', handleLoad);
}
return () => {
window.removeEventListener('load', handleLoad);
};
}, []);
};