mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-07-29 14:03:51 +00:00
feat(frontend): enhance API credentials system and build configuration
Add real API support in demo mode with credential checking, implement build-time version injection from package.json, and refactor update checking with 24-hour caching. Migrate landing page from Vue to Astro with comprehensive UI components including Hero, Features, Benefits, and Tech Stack sections. Update CI/CD workflow with expanded cache paths and security scanner version pinned.
This commit is contained in:
@@ -4,7 +4,8 @@ import { type BraveSearchResult } from '@/lib/brave-search';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { isEnvDemoMode } from '@/lib/demo-mode';
|
||||
import { isEnvDemoMode, shouldUseRealSearch } from '@/lib/demo-mode';
|
||||
import { getSearchProvider, getApiBaseUrl } from '@/lib/credentials';
|
||||
|
||||
export const BrowserSearch = () => {
|
||||
const [searchQuery, setSearchQuery] = createSignal('');
|
||||
@@ -15,10 +16,15 @@ export const BrowserSearch = () => {
|
||||
const [searchType, setSearchType] = createSignal<'web' | 'news'>('web');
|
||||
|
||||
// Check if we're in demo mode
|
||||
const isDemoMode = () => {
|
||||
const isDemo = () => {
|
||||
return isEnvDemoMode();
|
||||
};
|
||||
|
||||
// Check if we should use real search APIs
|
||||
const shouldUseReal = () => {
|
||||
return shouldUseRealSearch();
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
const query = searchQuery().trim();
|
||||
if (!query) return;
|
||||
@@ -28,12 +34,65 @@ export const BrowserSearch = () => {
|
||||
setHasSearched(true);
|
||||
|
||||
try {
|
||||
const isDemo = isDemoMode();
|
||||
const isDemoMode = isDemo();
|
||||
const useRealAPIs = shouldUseReal();
|
||||
|
||||
// In demo mode, use the demo mode API interceptor
|
||||
if (isDemo) {
|
||||
console.log(`[BrowserSearch] Demo mode: ${isDemoMode}, Use real APIs: ${useRealAPIs}`);
|
||||
|
||||
// If we have credentials and should use real APIs, try them first
|
||||
if (useRealAPIs) {
|
||||
console.log('Using real search APIs...');
|
||||
|
||||
// Try the configured search provider first
|
||||
const searchProvider = getSearchProvider();
|
||||
console.log(`Using search provider: ${searchProvider}`);
|
||||
|
||||
if (searchProvider === 'brave' && import.meta.env.VITE_BRAVE_API_KEY) {
|
||||
try {
|
||||
const { searchBrave } = await import('@/lib/brave-search');
|
||||
const results = await searchBrave(query, 8, searchType());
|
||||
if (results && results.length > 0) {
|
||||
setSearchResults(results);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Brave Search failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Try backend as fallback
|
||||
const API_BASE_URL = getApiBaseUrl();
|
||||
const token = localStorage.getItem('token') ||
|
||||
localStorage.getItem('auth_token') ||
|
||||
localStorage.getItem('trackeep_token');
|
||||
const endpoint = searchType() === 'news' ? '/api/v1/search/news' : '/api/v1/search/web';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
},
|
||||
body: JSON.stringify({ query, count: 8 }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.results && data.results.length > 0) {
|
||||
setSearchResults(data.results);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Backend search failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// In demo mode or as fallback, use the demo mode API interceptor
|
||||
if (isDemoMode) {
|
||||
console.log('Demo mode detected, using demo API interceptor...');
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL?.replace('/api/v1', '') || 'http://localhost:8080';
|
||||
const API_BASE_URL = getApiBaseUrl();
|
||||
const endpoint = searchType() === 'news' ? '/api/v1/search/news' : '/api/v1/search/web';
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
@@ -53,43 +112,7 @@ export const BrowserSearch = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.warn('Demo API failed, falling back to direct Brave API...');
|
||||
}
|
||||
|
||||
// Try Brave Search API directly (for production mode or as fallback)
|
||||
const { searchBrave } = await import('@/lib/brave-search');
|
||||
const results = await searchBrave(query, 8, searchType());
|
||||
|
||||
if (results && results.length > 0) {
|
||||
setSearchResults(results);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no results from Brave API, try backend as last resort (only in non-demo mode)
|
||||
if (!isDemo) {
|
||||
console.warn('Brave Search returned no results, trying backend...');
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL?.replace('/api/v1', '') || 'http://localhost:8080';
|
||||
const token = localStorage.getItem('token') ||
|
||||
localStorage.getItem('auth_token') ||
|
||||
localStorage.getItem('trackeep_token');
|
||||
const endpoint = searchType() === 'news' ? '/api/v1/search/news' : '/api/v1/search/web';
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
},
|
||||
body: JSON.stringify({ query, count: 8 }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.results && data.results.length > 0) {
|
||||
setSearchResults(data.results);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.warn('Demo API failed, falling back to mock results...');
|
||||
}
|
||||
|
||||
// If all APIs fail or return no results, show appropriate message
|
||||
@@ -148,14 +171,14 @@ export const BrowserSearch = () => {
|
||||
|
||||
const bookmarkResult = async (result: BraveSearchResult) => {
|
||||
// If in demo mode, just show success message
|
||||
if (isDemoMode()) {
|
||||
if (isDemo()) {
|
||||
// In demo mode, just show success without actual API call
|
||||
console.log('Demo mode: Bookmark created for', result.title);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1';
|
||||
const API_BASE_URL = getApiBaseUrl();
|
||||
const bookmarkData = {
|
||||
title: result.title,
|
||||
url: result.url,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { createSignal, For, onMount, createEffect, Show } from 'solid-js';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { createSignal, For, onMount, createEffect } from 'solid-js';
|
||||
import {
|
||||
IconBookmark,
|
||||
IconChecklist,
|
||||
@@ -42,10 +39,6 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
|
||||
const [activities, setActivities] = createSignal<ActivityItem[]>([]);
|
||||
const [filter, setFilter] = createSignal<'all' | 'trackeep' | 'github'>('all');
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
|
||||
// Debounce filter changes to prevent excessive re-renders
|
||||
const debouncedFilter = useDebounce(filter, 300);
|
||||
|
||||
const getActivityIcon = (type: string) => {
|
||||
switch (type) {
|
||||
|
||||
@@ -55,7 +55,7 @@ export const BookmarkModal = (props: BookmarkModalProps) => {
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
{props.isOpen && (
|
||||
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
|
||||
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const EditBookmarkModal = (props: EditBookmarkModalProps) => {
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
{props.isOpen && (
|
||||
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
|
||||
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, For, Show } from 'solid-js';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { RichTextEditor } from '@/components/ui/RichTextEditor';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSignal, For, Show, createEffect } from 'solid-js';
|
||||
import { IconTag, IconX, IconChevronDown } from '@tabler/icons-solidjs';
|
||||
import { IconTag, IconX } from '@tabler/icons-solidjs';
|
||||
|
||||
interface TagPickerProps {
|
||||
availableTags: string[];
|
||||
|
||||
@@ -81,7 +81,7 @@ export const TaskModal = (props: TaskModalProps) => {
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
{props.isOpen && (
|
||||
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
|
||||
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, createEffect, onCleanup, Show } from 'solid-js';
|
||||
import { createSignal, createEffect, onCleanup } from 'solid-js';
|
||||
import { IconX, IconCheck, IconAlertTriangle, IconInfoCircle } from '@tabler/icons-solidjs';
|
||||
|
||||
interface Toast {
|
||||
|
||||
@@ -39,6 +39,9 @@ export function UpdateChecker(props: UpdateCheckerProps) {
|
||||
setUpdateInfo(response.updateInfo || null);
|
||||
setCurrentVersion(response.currentVersion);
|
||||
|
||||
// Save last check time
|
||||
localStorage.setItem('lastUpdateCheck', Date.now().toString());
|
||||
|
||||
if (response.updateAvailable && response.updateInfo) {
|
||||
setUpdateStatus(prev => ({ ...prev, available: true }));
|
||||
}
|
||||
@@ -96,14 +99,22 @@ export function UpdateChecker(props: UpdateCheckerProps) {
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
// Check for updates on component mount
|
||||
checkForUpdates();
|
||||
|
||||
// Set current version
|
||||
setCurrentVersion(updateService.getCurrentVersion());
|
||||
|
||||
// Check for updates periodically (every 30 minutes)
|
||||
const intervalId = setInterval(checkForUpdates, 30 * 60 * 1000);
|
||||
// Check for updates periodically (every 24 hours)
|
||||
const intervalId = setInterval(checkForUpdates, 24 * 60 * 60 * 1000);
|
||||
|
||||
// Check if last check was more than 24 hours ago
|
||||
const lastCheckTime = localStorage.getItem('lastUpdateCheck');
|
||||
const now = Date.now();
|
||||
const twentyFourHours = 24 * 60 * 60 * 1000;
|
||||
|
||||
if (!lastCheckTime || (now - parseInt(lastCheckTime)) > twentyFourHours) {
|
||||
// Check for updates on component mount if it's been more than 24 hours
|
||||
checkForUpdates();
|
||||
localStorage.setItem('lastUpdateCheck', now.toString());
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(intervalId);
|
||||
@@ -134,7 +145,13 @@ export function UpdateChecker(props: UpdateCheckerProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class={`flex items-center gap-2 ${props.class || ''}`}>
|
||||
<div class={`flex flex-col gap-2 ${props.class || ''}`}>
|
||||
{/* Current Version Display */}
|
||||
<div class="text-xs text-muted-foreground px-2 text-center">
|
||||
Version {currentVersion()}
|
||||
</div>
|
||||
|
||||
{/* Check Updates Button */}
|
||||
<button
|
||||
onClick={() => updateAvailable() ? setShowUpdateModal(true) : checkForUpdates()}
|
||||
class="group inline-flex rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 justify-start items-center gap-2 truncate relative overflow-hidden w-full"
|
||||
|
||||
Reference in New Issue
Block a user