gitea
This commit is contained in:
@@ -1,90 +1,173 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3002';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: `${API_URL}/api`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Handle auth errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Auth API
|
||||
export const authAPI = {
|
||||
register: async (email: string, password: string) => {
|
||||
const response = await api.post('/auth/register', { email, password });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
login: async (email: string, password: string) => {
|
||||
const response = await api.post('/auth/login', { email, password });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Monitor API
|
||||
export const monitorAPI = {
|
||||
list: async () => {
|
||||
const response = await api.get('/monitors');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (id: string) => {
|
||||
const response = await api.get(`/monitors/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: any) => {
|
||||
const response = await api.post('/monitors', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: any) => {
|
||||
const response = await api.put(`/monitors/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string) => {
|
||||
const response = await api.delete(`/monitors/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
check: async (id: string) => {
|
||||
const response = await api.post(`/monitors/${id}/check`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
history: async (id: string, limit = 50) => {
|
||||
const response = await api.get(`/monitors/${id}/history`, {
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
snapshot: async (id: string, snapshotId: string) => {
|
||||
const response = await api.get(`/monitors/${id}/history/${snapshotId}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3002';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: `${API_URL}/api`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Handle auth errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Auth API
|
||||
export const authAPI = {
|
||||
register: async (email: string, password: string) => {
|
||||
const response = await api.post('/auth/register', { email, password });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
login: async (email: string, password: string) => {
|
||||
const response = await api.post('/auth/login', { email, password });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
forgotPassword: async (email: string) => {
|
||||
const response = await api.post('/auth/forgot-password', { email });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
resetPassword: async (token: string, newPassword: string) => {
|
||||
const response = await api.post('/auth/reset-password', { token, newPassword });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
verifyEmail: async (token: string) => {
|
||||
const response = await api.post('/auth/verify-email', { token });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
resendVerification: async (email: string) => {
|
||||
const response = await api.post('/auth/resend-verification', { email });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Monitor API
|
||||
export const monitorAPI = {
|
||||
list: async () => {
|
||||
const response = await api.get('/monitors');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (id: string) => {
|
||||
const response = await api.get(`/monitors/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: any) => {
|
||||
const response = await api.post('/monitors', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: any) => {
|
||||
const response = await api.put(`/monitors/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: string) => {
|
||||
const response = await api.delete(`/monitors/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
check: async (id: string) => {
|
||||
const response = await api.post(`/monitors/${id}/check`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
history: async (id: string, limit = 50) => {
|
||||
const response = await api.get(`/monitors/${id}/history`, {
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
snapshot: async (id: string, snapshotId: string) => {
|
||||
const response = await api.get(`/monitors/${id}/history/${snapshotId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
exportAuditTrail: async (id: string, format: 'json' | 'csv' = 'json') => {
|
||||
const token = localStorage.getItem('token');
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3002';
|
||||
const url = `${API_URL}/api/monitors/${id}/export?format=${format}`;
|
||||
|
||||
// Create a hidden link and trigger download
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Export failed');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const filename = response.headers.get('Content-Disposition')?.split('filename="')[1]?.replace('"', '')
|
||||
|| `export.${format}`;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(a.href);
|
||||
},
|
||||
};
|
||||
|
||||
// Settings API
|
||||
export const settingsAPI = {
|
||||
get: async () => {
|
||||
const response = await api.get('/settings');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
changePassword: async (currentPassword: string, newPassword: string) => {
|
||||
const response = await api.post('/settings/change-password', {
|
||||
currentPassword,
|
||||
newPassword,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateNotifications: async (data: {
|
||||
emailEnabled?: boolean;
|
||||
webhookUrl?: string | null;
|
||||
webhookEnabled?: boolean;
|
||||
slackWebhookUrl?: string | null;
|
||||
slackEnabled?: boolean;
|
||||
}) => {
|
||||
const response = await api.put('/settings/notifications', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteAccount: async (password: string) => {
|
||||
const response = await api.delete('/settings/account', {
|
||||
data: { password },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
export function saveAuth(token: string, user: any) {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
export function getAuth() {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const userStr = localStorage.getItem('user');
|
||||
|
||||
if (!token || !userStr) return null;
|
||||
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
return { token, user };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
|
||||
export function isAuthenticated() {
|
||||
return !!getAuth();
|
||||
}
|
||||
export function saveAuth(token: string, user: any) {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
export function getAuth() {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const userStr = localStorage.getItem('user');
|
||||
|
||||
if (!token || !userStr) return null;
|
||||
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
return { token, user };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
|
||||
export function isAuthenticated() {
|
||||
return !!getAuth();
|
||||
}
|
||||
|
||||
203
frontend/lib/templates.ts
Normal file
203
frontend/lib/templates.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
// Monitor Templates - Pre-configured monitoring setups for popular sites
|
||||
|
||||
export interface MonitorTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'ecommerce' | 'social' | 'news' | 'dev' | 'business' | 'other';
|
||||
icon: string;
|
||||
urlPattern: string; // Regex pattern for URL matching
|
||||
urlPlaceholder: string; // Example URL with placeholder
|
||||
selector?: string;
|
||||
ignoreRules: Array<{
|
||||
type: 'css' | 'regex' | 'text';
|
||||
value: string;
|
||||
description?: string;
|
||||
}>;
|
||||
keywordRules?: Array<{
|
||||
keyword: string;
|
||||
type: 'appears' | 'disappears' | 'count_increases' | 'count_decreases';
|
||||
}>;
|
||||
frequency: number; // in minutes
|
||||
}
|
||||
|
||||
export const monitorTemplates: MonitorTemplate[] = [
|
||||
// E-Commerce
|
||||
{
|
||||
id: 'amazon-price',
|
||||
name: 'Amazon Product Price',
|
||||
description: 'Track price changes on Amazon product pages',
|
||||
category: 'ecommerce',
|
||||
icon: '🛒',
|
||||
urlPattern: 'amazon\\.(com|de|co\\.uk|fr|es|it)/.*dp/[A-Z0-9]+',
|
||||
urlPlaceholder: 'https://amazon.com/dp/PRODUCTID',
|
||||
selector: '#priceblock_ourprice, .a-price .a-offscreen, #corePrice_feature_div .a-price-whole',
|
||||
ignoreRules: [
|
||||
{ type: 'css', value: '#nav, #rhf, #navFooter', description: 'Navigation and footer' },
|
||||
{ type: 'css', value: '.a-carousel, #recommendations', description: 'Recommendations' },
|
||||
{ type: 'regex', value: '\\d+ customer reviews?', description: 'Review count changes' }
|
||||
],
|
||||
frequency: 60
|
||||
},
|
||||
{
|
||||
id: 'ebay-price',
|
||||
name: 'eBay Listing Price',
|
||||
description: 'Monitor eBay listing prices and availability',
|
||||
category: 'ecommerce',
|
||||
icon: '🏷️',
|
||||
urlPattern: 'ebay\\.(com|de|co\\.uk)/itm/',
|
||||
urlPlaceholder: 'https://www.ebay.com/itm/ITEMID',
|
||||
selector: '.x-price-primary',
|
||||
ignoreRules: [
|
||||
{ type: 'css', value: '#vi-VR, #STORE_INFORMATION', description: 'Store info' }
|
||||
],
|
||||
frequency: 30
|
||||
},
|
||||
|
||||
// Developer Tools
|
||||
{
|
||||
id: 'github-releases',
|
||||
name: 'GitHub Releases',
|
||||
description: 'Get notified when new releases are published',
|
||||
category: 'dev',
|
||||
icon: '📦',
|
||||
urlPattern: 'github\\.com/[\\w-]+/[\\w-]+/releases',
|
||||
urlPlaceholder: 'https://github.com/owner/repo/releases',
|
||||
selector: '.release, [data-hpc] .Box-row',
|
||||
ignoreRules: [
|
||||
{ type: 'css', value: 'footer, .js-stale-session-flash', description: 'Footer' }
|
||||
],
|
||||
keywordRules: [
|
||||
{ keyword: 'Latest', type: 'appears' }
|
||||
],
|
||||
frequency: 360 // 6 hours
|
||||
},
|
||||
{
|
||||
id: 'npm-package',
|
||||
name: 'NPM Package',
|
||||
description: 'Track new versions of NPM packages',
|
||||
category: 'dev',
|
||||
icon: '📦',
|
||||
urlPattern: 'npmjs\\.com/package/[\\w@/-]+',
|
||||
urlPlaceholder: 'https://www.npmjs.com/package/package-name',
|
||||
selector: '#top h3, .css-1t74l4c',
|
||||
ignoreRules: [
|
||||
{ type: 'css', value: 'footer, .downloads', description: 'Footer and download stats' }
|
||||
],
|
||||
frequency: 1440 // Daily
|
||||
},
|
||||
|
||||
// News & Content
|
||||
{
|
||||
id: 'reddit-thread',
|
||||
name: 'Reddit Thread',
|
||||
description: 'Monitor a Reddit thread for new comments',
|
||||
category: 'social',
|
||||
icon: '📰',
|
||||
urlPattern: 'reddit\\.com/r/\\w+/comments/',
|
||||
urlPlaceholder: 'https://www.reddit.com/r/subreddit/comments/...',
|
||||
ignoreRules: [
|
||||
{ type: 'regex', value: '\\d+ points?', description: 'Vote counts' },
|
||||
{ type: 'regex', value: '\\d+ (minute|hour|day)s? ago', description: 'Timestamps' }
|
||||
],
|
||||
frequency: 30
|
||||
},
|
||||
{
|
||||
id: 'hackernews-front',
|
||||
name: 'Hacker News Front Page',
|
||||
description: 'Track top stories on Hacker News',
|
||||
category: 'news',
|
||||
icon: '📰',
|
||||
urlPattern: 'news\\.ycombinator\\.com/?$',
|
||||
urlPlaceholder: 'https://news.ycombinator.com/',
|
||||
selector: '.titleline',
|
||||
ignoreRules: [
|
||||
{ type: 'regex', value: '\\d+ points?', description: 'Points' },
|
||||
{ type: 'regex', value: '\\d+ comments?', description: 'Comment count' }
|
||||
],
|
||||
frequency: 60
|
||||
},
|
||||
|
||||
// Business & Jobs
|
||||
{
|
||||
id: 'job-board',
|
||||
name: 'Job Board',
|
||||
description: 'Monitor job postings on a company career page',
|
||||
category: 'business',
|
||||
icon: '💼',
|
||||
urlPattern: '.*/(careers?|jobs?)/?$',
|
||||
urlPlaceholder: 'https://company.com/careers',
|
||||
ignoreRules: [
|
||||
{ type: 'css', value: 'footer, nav, header', description: 'Navigation' }
|
||||
],
|
||||
keywordRules: [
|
||||
{ keyword: 'Senior', type: 'appears' },
|
||||
{ keyword: 'Remote', type: 'appears' }
|
||||
],
|
||||
frequency: 360 // 6 hours
|
||||
},
|
||||
{
|
||||
id: 'competitor-pricing',
|
||||
name: 'Competitor Pricing',
|
||||
description: 'Track competitor pricing page changes',
|
||||
category: 'business',
|
||||
icon: '💰',
|
||||
urlPattern: '.*/pricing/?$',
|
||||
urlPlaceholder: 'https://competitor.com/pricing',
|
||||
selector: '.price, .pricing-card, [class*="price"]',
|
||||
ignoreRules: [
|
||||
{ type: 'css', value: 'footer, nav', description: 'Navigation' }
|
||||
],
|
||||
frequency: 1440 // Daily
|
||||
},
|
||||
|
||||
// Generic
|
||||
{
|
||||
id: 'generic-page',
|
||||
name: 'Generic Web Page',
|
||||
description: 'Monitor any web page for changes',
|
||||
category: 'other',
|
||||
icon: '🌐',
|
||||
urlPattern: '.*',
|
||||
urlPlaceholder: 'https://example.com/page',
|
||||
ignoreRules: [
|
||||
{ type: 'css', value: 'script, style, noscript', description: 'Scripts' }
|
||||
],
|
||||
frequency: 60
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Find matching templates for a given URL
|
||||
*/
|
||||
export function findMatchingTemplates(url: string): MonitorTemplate[] {
|
||||
return monitorTemplates.filter(template => {
|
||||
try {
|
||||
const regex = new RegExp(template.urlPattern, 'i');
|
||||
return regex.test(url);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get templates by category
|
||||
*/
|
||||
export function getTemplatesByCategory(category: MonitorTemplate['category']): MonitorTemplate[] {
|
||||
return monitorTemplates.filter(t => t.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a template to create monitor configuration
|
||||
*/
|
||||
export function applyTemplate(template: MonitorTemplate, url: string) {
|
||||
return {
|
||||
url,
|
||||
name: `${template.name} Monitor`,
|
||||
frequency: template.frequency,
|
||||
elementSelector: template.selector || null,
|
||||
ignoreRules: template.ignoreRules,
|
||||
keywordRules: template.keywordRules || [],
|
||||
};
|
||||
}
|
||||
17
frontend/lib/types.ts
Normal file
17
frontend/lib/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface Monitor {
|
||||
id: string
|
||||
url: string
|
||||
name: string
|
||||
frequency: number
|
||||
status: 'active' | 'paused' | 'error'
|
||||
last_checked_at?: string
|
||||
last_changed_at?: string
|
||||
consecutive_errors: number
|
||||
recentSnapshots?: {
|
||||
id: string
|
||||
responseTime: number
|
||||
importanceScore?: number
|
||||
changed: boolean
|
||||
createdAt: string
|
||||
}[]
|
||||
}
|
||||
43
frontend/lib/use-plan.ts
Normal file
43
frontend/lib/use-plan.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { getAuth } from './auth'
|
||||
|
||||
export type UserPlan = 'free' | 'pro' | 'business' | 'enterprise'
|
||||
|
||||
export const PLAN_LIMITS = {
|
||||
free: {
|
||||
maxMonitors: 3,
|
||||
minFrequency: 60, // minutes
|
||||
features: ['email_alerts', 'basic_noise_filtering'],
|
||||
},
|
||||
pro: {
|
||||
maxMonitors: 20,
|
||||
minFrequency: 5,
|
||||
features: ['email_alerts', 'slack_integration', 'webhook_integration', 'keyword_alerts', 'smart_noise_filtering', 'audit_export'],
|
||||
},
|
||||
business: {
|
||||
maxMonitors: 100,
|
||||
minFrequency: 1,
|
||||
features: ['email_alerts', 'slack_integration', 'webhook_integration', 'keyword_alerts', 'smart_noise_filtering', 'audit_export', 'api_access', 'team_members'],
|
||||
},
|
||||
enterprise: {
|
||||
maxMonitors: Infinity,
|
||||
minFrequency: 1,
|
||||
features: ['email_alerts', 'slack_integration', 'webhook_integration', 'keyword_alerts', 'smart_noise_filtering', 'audit_export', 'api_access', 'team_members', 'custom_integrations', 'sla'],
|
||||
},
|
||||
} as const
|
||||
|
||||
export function usePlan() {
|
||||
const auth = getAuth()
|
||||
const plan = (auth?.user?.plan as UserPlan) || 'free'
|
||||
const limits = PLAN_LIMITS[plan] || PLAN_LIMITS.free
|
||||
|
||||
return {
|
||||
plan,
|
||||
limits,
|
||||
canUseSlack: limits.features.includes('slack_integration' as any),
|
||||
canUseWebhook: limits.features.includes('webhook_integration' as any),
|
||||
canUseKeywords: limits.features.includes('keyword_alerts' as any),
|
||||
canUseSmartNoise: limits.features.includes('smart_noise_filtering' as any),
|
||||
maxMonitors: limits.maxMonitors,
|
||||
minFrequency: limits.minFrequency,
|
||||
}
|
||||
}
|
||||
6
frontend/lib/utils.ts
Normal file
6
frontend/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user