feat: migrate to DragonflyDB and clean up environment configuration

- Replace Redis with DragonflyDB for better performance and memory efficiency
- Remove redundant environment variables (POSTGRES_*, ENCRYPTION_KEY, OAUTH_SERVICE_URL)
- Consolidate database configuration to use single DB_* variables
- Use JWT_SECRET for both JWT tokens and encryption
- Remove PORT variable redundancy, use BACKEND_PORT consistently
- Clean up docker-compose configurations for dev/prod consistency
- Add DragonflyDB configuration with optimized memory usage
- Remove redis.conf as it's no longer needed
- Update health checks to use Redis-compatible CLI for DragonflyDB
- Add missing VITE_API_URL to production frontend
- Fix GitHub Actions to use correct go.sum path
- Clean up development directories and unused files
This commit is contained in:
Tomas Dvorak
2026-03-05 23:51:34 +01:00
parent f3a835caa2
commit 954a1a1080
146 changed files with 5800 additions and 25846 deletions
+3 -1
View File
@@ -6,6 +6,7 @@ WORKDIR /app
# Accept build arguments for VITE environment variables
ARG VITE_DEMO_MODE=false
ARG VITE_API_URL=http://localhost:8080
ARG VITE_OAUTH_SERVICE_URL=https://oauth.trackeep.org
# Copy package files
COPY frontend/package*.json ./frontend/
@@ -16,7 +17,8 @@ COPY frontend/ ./frontend/
# Create a .env.production file with build arguments
RUN cd frontend && echo "VITE_DEMO_MODE=${VITE_DEMO_MODE}" >> .env.production && \
echo "VITE_API_URL=${VITE_API_URL}" >> .env.production
echo "VITE_API_URL=${VITE_API_URL}" >> .env.production && \
echo "VITE_OAUTH_SERVICE_URL=${VITE_OAUTH_SERVICE_URL}" >> .env.production
# Build the application (frontend only)
RUN cd frontend && npm run build
+8
View File
@@ -25,10 +25,18 @@ cp /usr/share/nginx/html/index.html /usr/share/nginx/html/index.html.orig
# Replace environment variables in the HTML file
sed -i "s|VITE_DEMO_MODE_PLACEHOLDER|$VITE_DEMO_MODE|g" $HTML_FILE
sed -i "s|VITE_API_URL_PLACEHOLDER|$VITE_API_URL|g" $HTML_FILE
sed -i "s|VITE_OAUTH_SERVICE_URL_PLACEHOLDER|$VITE_OAUTH_SERVICE_URL|g" $HTML_FILE
# Find the actual CSS file name and update the stylesheet link
CSS_FILE=$(find /usr/share/nginx/html/assets -name "*.css" | head -1 | xargs basename)
if [ ! -z "$CSS_FILE" ]; then
sed -i "s|href=\"/assets/index-LnCEqXC_.css\"|href=\"/assets/$CSS_FILE\"|g" $HTML_FILE
fi
echo "Environment variables injected:"
echo "VITE_DEMO_MODE=$VITE_DEMO_MODE"
echo "VITE_API_URL=$VITE_API_URL"
echo "VITE_OAUTH_SERVICE_URL=$VITE_OAUTH_SERVICE_URL"
echo "FRONTEND_PORT=$FRONTEND_PORT"
EOF
+6 -5
View File
@@ -9,24 +9,25 @@
<meta name="theme-color" content="#39b9ff" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trackeep - Your Self-Hosted Productivity & Knowledge Hub</title>
<link rel="stylesheet" crossorigin href="/assets/index-LnCEqXC_.css">
<script>
// Runtime environment variable injection
window.ENV = {
VITE_DEMO_MODE: 'VITE_DEMO_MODE_PLACEHOLDER',
VITE_API_URL: 'VITE_API_URL_PLACEHOLDER'
VITE_API_URL: 'VITE_API_URL_PLACEHOLDER',
VITE_OAUTH_SERVICE_URL: 'VITE_OAUTH_SERVICE_URL_PLACEHOLDER'
};
// Make them available to import.meta.env by overriding it
if (typeof window !== 'undefined') {
window.importMetaEnv = {
VITE_DEMO_MODE: window.ENV.VITE_DEMO_MODE,
VITE_API_URL: window.ENV.VITE_API_URL
VITE_API_URL: window.ENV.VITE_API_URL,
VITE_OAUTH_SERVICE_URL: window.ENV.VITE_OAUTH_SERVICE_URL
};
}
// Log for debugging
console.log('[Env Injection] window.ENV:', window.ENV);
console.log('[Env Injection] Demo mode should be:', window.ENV.VITE_DEMO_MODE);
// Debug logs removed for production
</script>
</head>
<body>
@@ -29,6 +29,7 @@ import { Switch } from '../ui/Switch'
import { ModalPortal } from '../ui/ModalPortal'
import { useAuth } from '@/lib/auth'
import { getApiV1BaseUrl } from '@/lib/api-url'
import { useHaptics } from '@/lib/haptics'
const navigation = [
{ name: 'Home', href: '/app', icon: IconHome },
@@ -74,6 +75,7 @@ export interface SidebarProps {
export function Sidebar(props: SidebarProps) {
const location = useLocation()
const { logout } = useAuth()
const haptics = useHaptics()
const [isWorkspaceDropdownOpen, setIsWorkspaceDropdownOpen] = createSignal(false)
const [workspaces, setWorkspaces] = createSignal<WorkspaceOption[]>([])
const [selectedWorkspaceId, setSelectedWorkspaceId] = createSignal<string>('')
@@ -421,6 +423,7 @@ export function Sidebar(props: SidebarProps) {
"bg-[#262626] text-white font-medium shadow-sm": active,
"hover:bg-[#262626] hover:text-white text-[#a3a3a3]": !active
}}
onClick={() => haptics.navigation()}
>
<div class="relative z-10 flex items-center gap-2">
<Icon class={`size-5 transition-colors ${
+16 -1
View File
@@ -2,6 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { splitProps, Show } from 'solid-js'
import { cn } from '@/lib/utils'
import { IconLoader2 } from '@tabler/icons-solidjs'
import { useHaptics, type HapticPattern } from '@/lib/haptics'
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-papra focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
@@ -46,6 +47,7 @@ export interface ButtonProps
loading?: boolean
onClick?: (e: MouseEvent) => void
children: any
haptic?: HapticPattern
}
export function Button(props: ButtonProps) {
@@ -58,15 +60,28 @@ export function Button(props: ButtonProps) {
'loading',
'onClick',
'children',
'haptic',
])
const haptics = useHaptics()
const isDisabled = () => local.disabled || local.loading
const handleClick = (e: MouseEvent) => {
// Trigger haptic feedback if specified
if (local.haptic && !isDisabled()) {
haptics[local.haptic]?.()
}
// Call original onClick handler
local.onClick?.(e)
}
return (
<button
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
disabled={isDisabled()}
onClick={local.onClick}
onClick={handleClick}
{...others}
>
<Show when={local.loading}>
+18
View File
@@ -39,5 +39,23 @@ declare module "*.bmp" {
export default content;
}
interface ImportMetaEnv {
readonly VITE_API_URL?: string;
readonly VITE_DEMO_MODE?: string;
readonly VITE_OAUTH_SERVICE_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
interface Window {
importMetaEnv?: {
VITE_API_URL?: string;
VITE_DEMO_MODE?: string;
VITE_OAUTH_SERVICE_URL?: string;
};
}
// Global build-time constants
declare const __APP_VERSION__: string;
-14
View File
@@ -72,12 +72,9 @@ export const AuthProvider: ParentComponent = (props) => {
// Initialize auth state from localStorage
onMount(() => {
console.log('[Auth] onMount: Initializing auth state');
console.log('[Auth] onMount: Demo mode check:', isDemoMode());
// First check if demo mode should be cleared
if (!isDemoMode()) {
console.log('[Auth] onMount: Demo mode disabled, clearing demo-specific data only');
// Only clear demo mode data, not legitimate user auth data
localStorage.removeItem('demoMode');
@@ -88,7 +85,6 @@ export const AuthProvider: ParentComponent = (props) => {
if (token && userStr) {
try {
const user = JSON.parse(userStr);
console.log('[Auth] onMount: Found existing auth, restoring:', user);
setAuthState({
user,
token,
@@ -107,7 +103,6 @@ export const AuthProvider: ParentComponent = (props) => {
clearAuth();
}
} else {
console.log('[Auth] onMount: No existing auth found, setting isLoading to false');
setAuthState('isLoading', false);
// Set dark mode by default when not authenticated
document.documentElement.setAttribute('data-kb-theme', 'dark');
@@ -116,7 +111,6 @@ export const AuthProvider: ParentComponent = (props) => {
}
// Demo mode is enabled - use in-memory auth only
console.log('[Auth] onMount: Demo mode enabled, using in-memory auth');
const mockUser = {
id: 1,
email: '[email protected]',
@@ -128,8 +122,6 @@ export const AuthProvider: ParentComponent = (props) => {
};
const mockToken = 'demo-token-' + Date.now();
console.log('[Auth] onMount: Setting mock auth state:', { mockUser, mockToken });
setAuthState({
user: mockUser,
token: mockToken,
@@ -146,7 +138,6 @@ export const AuthProvider: ParentComponent = (props) => {
// Apply theme
document.documentElement.setAttribute('data-kb-theme', 'dark');
document.title = 'Trackeep - Demo Mode';
console.log('[Auth] onMount: Demo mode setup complete');
});
@@ -167,23 +158,18 @@ export const AuthProvider: ParentComponent = (props) => {
};
const setAuth = (token: string, user: User) => {
console.log('[Auth] setAuth called with:', { token, user });
localStorage.setItem('trackeep_token', token);
localStorage.setItem('trackeep_user', JSON.stringify(user));
// Also set the legacy keys for compatibility
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
console.log('[Auth] setAuth: Updating auth state');
setAuthState({
user,
token,
isAuthenticated: true,
isLoading: false,
});
console.log('[Auth] setAuth: Auth state updated');
// Apply theme immediately
if (user.theme === 'dark') {
document.documentElement.setAttribute('data-kb-theme', 'dark');
+1 -16
View File
@@ -1,6 +1,6 @@
// Demo mode API interceptor to provide mock data instead of making real API calls
import { hasAnyCredentials, isBackendAvailable, isSearchAvailable, hasSearchCredentials, hasDatabaseCredentials, hasAICredentials } from './credentials';
import { hasAnyCredentials, isBackendAvailable, isSearchAvailable } from './credentials';
// Check if demo mode is enabled via environment variable
export const isEnvDemoMode = (): boolean => {
@@ -14,7 +14,6 @@ export const isEnvDemoMode = (): boolean => {
const buildTimeResult = import.meta.env.VITE_DEMO_MODE === 'true';
const result = runtimeResult || importMetaEnvResult || buildTimeResult;
console.log('[Demo Mode] isEnvDemoMode:', result, 'runtime:', runtimeResult, 'importMetaEnv:', importMetaEnvResult, 'buildTime:', buildTimeResult, 'VITE_DEMO_MODE:', import.meta.env.VITE_DEMO_MODE, 'window.ENV:', (window as any).ENV, 'window.importMetaEnv:', (window as any).importMetaEnv);
return result;
};
@@ -29,14 +28,6 @@ export const shouldUseRealAPIs = (): boolean => {
const hasCredentials = hasAnyCredentials();
const hasBackend = shouldUseRealBackend();
const result = hasCredentials || hasBackend;
console.log('[Demo Mode] shouldUseRealAPIs:', {
hasCredentials,
hasBackend,
result,
searchCreds: hasSearchCredentials(),
dbCreds: hasDatabaseCredentials(),
aiCreds: hasAICredentials()
});
return result;
};
@@ -944,11 +935,9 @@ const parseBody = (options?: RequestInit): Record<string, any> => {
// Demo mode fetch interceptor
export const demoFetch = async (url: string, options?: RequestInit): Promise<Response> => {
const shouldUseReal = shouldUseRealAPIs();
console.log('[Demo Mode] demoFetch called:', { url, shouldUseReal });
if (shouldUseReal) {
if (url.includes('youtube') && Math.random() < 0.02) {
console.log('[Demo Mode] Real credentials detected, using real API for:', url);
}
if (url.includes('youtube')) {
@@ -970,7 +959,6 @@ export const demoFetch = async (url: string, options?: RequestInit): Promise<Res
}
if (!isDemoMode()) {
console.log('[Demo Mode] Not in demo mode, using real fetch for:', url);
return (originalFetch || window.fetch)(url, options);
}
@@ -986,8 +974,6 @@ export const demoFetch = async (url: string, options?: RequestInit): Promise<Res
const method = (options?.method || 'GET').toUpperCase();
const body = parseBody(options);
console.log(`[Demo Mode] Intercepting request to: ${method} ${path}`);
await new Promise((resolve) => setTimeout(resolve, 180));
// Auth endpoints
@@ -1946,7 +1932,6 @@ export const initializeDemoMode = () => {
// Store original fetch to use for real API calls and restore later if needed
originalFetch = window.fetch;
window.fetch = demoFetch as typeof fetch;
console.log('[Demo Mode] API interceptor initialized');
return originalFetch;
}
return null;
+100
View File
@@ -0,0 +1,100 @@
import { createSignal, onMount } from 'solid-js';
// Haptic feedback patterns for different interactions
export type HapticPattern =
| "success" // Successful action completion
| "error" // Error or failure
| "warning" // Warning or caution
| "notification" // New message or notification
| "selection" // Item selection
| "navigation" // Page navigation
| "impact" // Light impact for button clicks
| "completion" // Task or form completion
| "delete" // Delete or destructive action
| "longPress" // Long press gesture;
// Check if haptics/vibration is supported
const isSupported = () => {
if (typeof window === 'undefined') return false;
return 'vibrate' in navigator;
};
// Vibration patterns for different haptic types
const vibrationPatterns: Record<HapticPattern, number[]> = {
success: [10, 50, 10],
error: [100, 50, 100],
warning: [50, 30, 50],
notification: [20, 100, 20],
selection: [10],
navigation: [15],
impact: [5],
completion: [10, 30, 10, 30, 10],
delete: [100],
longPress: [50],
};
// Haptic feedback utility hook for SolidJS
export const useHaptics = () => {
const [isSupported, setIsSupported] = createSignal(false);
onMount(() => {
setIsSupported(isSupported());
});
// Trigger haptic feedback with fallback for non-supported devices
const triggerHaptic = (pattern: HapticPattern) => {
if (isSupported() && navigator.vibrate) {
try {
navigator.vibrate(vibrationPatterns[pattern] || [10]);
} catch (error) {
console.debug("Haptics failed:", error);
}
}
};
// Specific haptic methods for common actions
const haptics = {
// Success feedback for completed actions
success: () => triggerHaptic("success"),
// Error feedback for failed actions
error: () => triggerHaptic("error"),
// Warning feedback for caution states
warning: () => triggerHaptic("warning"),
// New notification or message
notification: () => triggerHaptic("notification"),
// Item selection (checkboxes, radio buttons, etc.)
selection: () => triggerHaptic("selection"),
// Navigation between pages or sections
navigation: () => triggerHaptic("navigation"),
// Light impact for button clicks
impact: () => triggerHaptic("impact"),
// Task or form completion
completion: () => triggerHaptic("completion"),
// Delete or destructive actions
delete: () => triggerHaptic("delete"),
// Long press gestures
longPress: () => triggerHaptic("longPress"),
};
return haptics;
};
// Utility function for direct haptic triggering (outside of components)
export const triggerHaptic = (pattern: HapticPattern) => {
if (isSupported() && navigator.vibrate) {
try {
navigator.vibrate(vibrationPatterns[pattern] || [10]);
} catch (error) {
console.debug("Haptics failed:", error);
}
}
};
+13
View File
@@ -0,0 +1,13 @@
import { getApiV1BaseUrl } from '@/lib/api-url';
export const getOAuthCallbackUrl = (): string => {
return new URL('/auth/callback', window.location.origin).toString();
};
export const startGitHubOAuth = (): void => {
const apiBase = getApiV1BaseUrl();
const frontendRedirect = getOAuthCallbackUrl();
window.location.href =
`${apiBase}/auth/github?frontend_redirect=${encodeURIComponent(frontendRedirect)}`;
};
+9 -1
View File
@@ -12,6 +12,7 @@ import {
Bot
} from 'lucide-solid'
import { AIProviderIcon } from '@/components/AIProviderIcon'
import { useHaptics } from '@/lib/haptics'
interface AIModel {
id: string
@@ -30,6 +31,7 @@ interface Message {
}
export const AIChat = () => {
const haptics = useHaptics();
const [activeView, setActiveView] = createSignal<'chat' | 'settings'>('chat')
const [isSidebarOpen, setIsSidebarOpen] = createSignal(true)
@@ -96,6 +98,7 @@ export const AIChat = () => {
setMessages(prev => [...prev, userMessage])
setInputMessage('')
setIsLoading(true)
haptics.impact(); // Impact feedback for sending message
try {
// Call AI API
@@ -109,8 +112,10 @@ export const AIChat = () => {
}
setMessages(prev => [...prev, aiMessage])
haptics.success(); // Success feedback for AI response
} catch (error) {
console.error('AI API call failed:', error)
haptics.error(); // Error feedback
// Fallback response
const errorMessage: Message = {
@@ -488,7 +493,10 @@ export const AIChat = () => {
</div>
<Button
disabled={isLoading() || !inputMessage().trim()}
onClick={handleSendMessage}
onClick={() => {
handleSendMessage();
haptics.impact();
}}
class="rounded-xl px-4 py-2.5 bg-gradient-to-r from-primary to-primary/90 hover:from-primary/90 hover:to-primary shadow-sm hover:shadow-md transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Send class="h-4 w-4" />
+8 -2
View File
@@ -10,13 +10,16 @@ import {
IconDownload,
IconSettings
} from '@tabler/icons-solidjs';
import { useHaptics } from '@/lib/haptics';
export const Activity = () => {
const haptics = useHaptics();
const [refreshKey, setRefreshKey] = createSignal(0);
const [showFilters, setShowFilters] = createSignal(false);
const handleRefresh = () => {
setRefreshKey(prev => prev + 1);
haptics.selection(); // Selection feedback for refresh
};
return (
@@ -33,7 +36,10 @@ export const Activity = () => {
<Button
variant="outline"
size="sm"
onClick={() => setShowFilters(!showFilters())}
onClick={() => {
setShowFilters(!showFilters());
haptics.selection();
}}
>
<IconFilter class="size-4 mr-2" />
Filters
@@ -46,7 +52,7 @@ export const Activity = () => {
<IconRefresh class="size-4 mr-2" />
Refresh
</Button>
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" onClick={() => haptics.impact()}>
<IconDownload class="size-4 mr-2" />
Export
</Button>
+6
View File
@@ -16,6 +16,7 @@ import {
IconChecklist
} from '@tabler/icons-solidjs';
import { ColorSwitcher } from './ColorSwitcher';
import { useHaptics } from '@/lib/haptics';
interface ProjectStats {
totalUsers: number;
@@ -51,6 +52,7 @@ interface GitHubActivity {
}
export const AdminDashboard = () => {
const haptics = useHaptics();
const [stats, setStats] = createSignal<ProjectStats>({
totalUsers: 0,
totalDocuments: 0,
@@ -161,18 +163,22 @@ export const AdminDashboard = () => {
const handleBackupDatabase = async () => {
try {
alert('Database backup initiated successfully!');
haptics.success(); // Success feedback for backup
// In real app, this would call the backup API
} catch (error) {
alert('Failed to backup database');
haptics.error(); // Error feedback
}
};
const handleManageUsers = () => {
window.open('/app/members', '_blank');
haptics.navigation(); // Navigation feedback
};
const handleSystemSettings = () => {
window.open('/app/admin-settings', '_blank');
haptics.navigation(); // Navigation feedback
};
const getActivityIcon = (type: string) => {
+10 -2
View File
@@ -12,6 +12,7 @@ import {
} from '@tabler/icons-solidjs';
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { useHaptics } from '@/lib/haptics';
interface AnalyticsData {
period: {
@@ -117,6 +118,7 @@ interface AnalyticsData {
}
export const Analytics = () => {
const haptics = useHaptics();
const [analytics, setAnalytics] = createSignal<AnalyticsData | null>(null);
const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal<string | null>(null);
@@ -238,14 +240,20 @@ export const Analytics = () => {
<div class="flex gap-2">
<select
value={selectedPeriod()}
onChange={(e) => setSelectedPeriod(e.target.value)}
onChange={(e) => {
setSelectedPeriod(e.target.value);
haptics.selection();
}}
class="px-3 py-2 border rounded-md bg-background"
>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
<Button onClick={fetchAnalytics}>Refresh</Button>
<Button onClick={() => {
fetchAnalytics();
haptics.selection();
}}>Refresh</Button>
</div>
</div>
+35 -14
View File
@@ -1,28 +1,49 @@
import { createSignal, onMount } from 'solid-js';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { getApiV1BaseUrl } from '@/lib/api-url';
import { useAuth } from '@/lib/auth';
export const AuthCallback = () => {
const [status, setStatus] = createSignal<'loading' | 'success' | 'error'>('loading');
const [message, setMessage] = createSignal('Processing authentication...');
const { setAuth } = useAuth();
onMount(async () => {
try {
const apiBase = getApiV1BaseUrl();
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
if (!token) {
throw new Error('Missing token');
}
window.history.replaceState({}, '', '/auth/callback');
const res = await fetch(`${apiBase}/auth/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) {
throw new Error('Authentication failed');
}
const data = await res.json();
if (!data?.user) {
throw new Error('Invalid authentication response');
}
setAuth(token, data.user);
onMount(() => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
// Store the token from Trackeep backend
localStorage.setItem('token', token);
setStatus('success');
setMessage('Authentication successful! Redirecting...');
// Redirect to dashboard after a short delay
setTimeout(() => {
window.location.href = '/app';
}, 2000);
} else {
window.location.replace('/app');
} catch (error) {
setStatus('error');
setMessage('Authentication failed. Please try again.');
setMessage(error instanceof Error ? error.message : 'Authentication failed');
}
});
+18 -4
View File
@@ -8,6 +8,7 @@ import { DropdownMenu, DropdownMenuItem } from '@/components/ui/DropdownMenu';
import { SearchTagFilterBar } from '@/components/ui/SearchTagFilterBar';
import { IconDotsVertical, IconStar, IconEdit, IconTrash, IconExternalLink, IconVideo, IconBookmark } from '@tabler/icons-solidjs';
import { getApiV1BaseUrl } from '@/lib/api-url';
import { useHaptics } from '@/lib/haptics';
const API_BASE_URL = getApiV1BaseUrl();
@@ -102,6 +103,8 @@ export const Bookmarks = () => {
const [showVideoModal, setShowVideoModal] = createSignal(false);
const [editingBookmark, setEditingBookmark] = createSignal<Bookmark | null>(null);
const [activeTab, setActiveTab] = createSignal<'bookmarks' | 'videos'>('bookmarks');
const haptics = useHaptics();
// We no longer show inline HTML content previews, only the bookmark cards themselves
onMount(async () => {
@@ -229,7 +232,9 @@ export const Bookmarks = () => {
const newBookmark = adaptBookmarkFromApi(raw);
setBookmarks(prev => [newBookmark, ...prev]);
setShowAddModal(false);
haptics.success(); // Success feedback for adding bookmark
} catch (error) {
haptics.error(); // Error feedback
alert(error instanceof Error ? error.message : 'Failed to add bookmark');
}
};
@@ -242,6 +247,7 @@ export const Bookmarks = () => {
: bookmark
)
);
haptics.selection(); // Selection feedback for starring
};
const deleteBookmark = async (bookmarkId: number) => {
@@ -261,7 +267,9 @@ export const Bookmarks = () => {
}
setBookmarks(prev => prev.filter(bookmark => bookmark.id !== bookmarkId));
haptics.delete(); // Delete feedback
} catch (error) {
haptics.error(); // Error feedback
alert(error instanceof Error ? error.message : 'Failed to delete bookmark');
}
}
@@ -356,13 +364,13 @@ export const Bookmarks = () => {
<h1 class="text-3xl font-bold text-foreground">Bookmarks</h1>
</div>
<Show when={activeTab() === 'bookmarks'}>
<Button onClick={() => setShowAddModal(true)}>
<Button onClick={() => setShowAddModal(true)} haptic="impact">
<IconBookmark class="size-4 mr-2" />
Add Bookmark
</Button>
</Show>
<Show when={activeTab() === 'videos'}>
<Button onClick={() => setShowVideoModal(true)}>
<Button onClick={() => setShowVideoModal(true)} haptic="impact">
<IconVideo class="size-4 mr-2" />
Add Video
</Button>
@@ -373,7 +381,10 @@ export const Bookmarks = () => {
<div class="border-b border-border">
<nav class="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('bookmarks')}
onClick={() => {
setActiveTab('bookmarks');
haptics.navigation();
}}
class={`py-2 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2 ${
activeTab() === 'bookmarks'
? 'border-primary text-primary'
@@ -384,7 +395,10 @@ export const Bookmarks = () => {
Web Bookmarks
</button>
<button
onClick={() => setActiveTab('videos')}
onClick={() => {
setActiveTab('videos');
haptics.navigation();
}}
class={`py-2 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2 ${
activeTab() === 'videos'
? 'border-primary text-primary'
+36 -7
View File
@@ -14,6 +14,7 @@ import {
} from '@tabler/icons-solidjs'
import { getMockCalendarEvents } from '@/lib/mockData';
import { isDemoMode as isDemoModeEnabled } from '@/lib/demo-mode';
import { useHaptics } from '@/lib/haptics';
interface CalendarEvent {
id: number
@@ -52,6 +53,7 @@ interface NewEvent {
}
export function Calendar() {
const haptics = useHaptics();
const [upcomingEvents, setUpcomingEvents] = createSignal<CalendarEvent[]>([])
const [todayEvents, setTodayEvents] = createSignal<CalendarEvent[]>([])
const [deadlines, setDeadlines] = createSignal<CalendarEvent[]>([])
@@ -238,6 +240,7 @@ export function Calendar() {
is_all_day: false
});
fetchCalendarData();
haptics.success(); // Success feedback for event creation
return;
}
@@ -266,6 +269,9 @@ export function Calendar() {
is_all_day: false
})
fetchCalendarData()
haptics.success(); // Success feedback for event creation
} else {
haptics.error(); // Error feedback
}
} catch (error) {
console.error('Failed to create event:', error)
@@ -281,7 +287,7 @@ export function Calendar() {
location: '',
is_all_day: false
});
fetchCalendarData();
haptics.error(); // Error feedback
}
}
@@ -291,6 +297,7 @@ export function Calendar() {
// Simulate event completion toggle in demo mode
console.log('Toggling event completion (demo mode):', eventId);
fetchCalendarData();
haptics.completion(); // Completion feedback
return;
}
@@ -307,11 +314,15 @@ export function Calendar() {
if (response.ok) {
fetchCalendarData()
haptics.completion(); // Completion feedback
} else {
haptics.error(); // Error feedback
}
} catch (error) {
console.error('Failed to toggle event completion:', error)
// Fallback to demo mode behavior
fetchCalendarData();
haptics.error(); // Error feedback
}
}
@@ -385,7 +396,10 @@ export function Calendar() {
</p>
</div>
<button
onClick={() => openEventModal(new Date())}
onClick={() => {
openEventModal(new Date());
haptics.impact();
}}
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
<IconPlus class="size-4" />
@@ -400,7 +414,10 @@ export function Calendar() {
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<button
onClick={() => navigateMonth(-1)}
onClick={() => {
navigateMonth(-1);
haptics.selection();
}}
class="p-2 hover:bg-accent rounded-lg transition-colors"
>
<IconChevronLeft class="size-4" />
@@ -409,7 +426,10 @@ export function Calendar() {
{currentDate().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
</h2>
<button
onClick={() => navigateMonth(1)}
onClick={() => {
navigateMonth(1);
haptics.selection();
}}
class="p-2 hover:bg-accent rounded-lg transition-colors"
>
<IconChevronRight class="size-4" />
@@ -417,7 +437,10 @@ export function Calendar() {
</div>
<div class="flex gap-2">
<button
onClick={() => setView('month')}
onClick={() => {
setView('month');
haptics.selection();
}}
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
view() === 'month'
? 'bg-primary text-primary-foreground'
@@ -427,7 +450,10 @@ export function Calendar() {
Month
</button>
<button
onClick={() => setView('week')}
onClick={() => {
setView('week');
haptics.selection();
}}
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
view() === 'week'
? 'bg-primary text-primary-foreground'
@@ -437,7 +463,10 @@ export function Calendar() {
Week
</button>
<button
onClick={() => setView('day')}
onClick={() => {
setView('day');
haptics.selection();
}}
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
view() === 'day'
? 'bg-primary text-primary-foreground'
+22 -7
View File
@@ -48,6 +48,7 @@ import {
} from '@/lib/credentials';
import { isDemoMode } from '@/lib/demo-mode';
import { getApiV1BaseUrl } from '@/lib/api-url';
import { useHaptics } from '@/lib/haptics';
const API_BASE_URL = getApiV1BaseUrl();
@@ -174,6 +175,7 @@ const deriveFileType = (fileName: string): string => {
export const Dashboard = () => {
const navigate = useNavigate();
const haptics = useHaptics();
const [documents, setDocuments] = createSignal<Document[]>([]);
const [, setRecentActivity] = createSignal<RecentActivity[]>([]);
const [githubActivityEvents, setGithubActivityEvents] = createSignal<GitHubActivityEvent[]>([]);
@@ -810,7 +812,10 @@ export const Dashboard = () => {
<div class="lg:col-span-2 space-y-4">
<button
type="button"
onClick={() => setShowUploadModal(true)}
onClick={() => {
setShowUploadModal(true);
haptics.impact();
}}
class="w-full h-32 inline-flex justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 items-start flex-col gap-4 py-6 px-6 text-left"
>
<IconUpload class="size-6" />
@@ -824,7 +829,10 @@ export const Dashboard = () => {
<div class="grid grid-cols-2 gap-4">
<button
type="button"
onClick={() => setShowVideoModal(true)}
onClick={() => {
setShowVideoModal(true);
haptics.impact();
}}
class="w-full inline-flex justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-secondary text-secondary-foreground shadow hover:bg-secondary/90 items-start flex-col gap-4 py-4 px-4 text-left"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-video size-5">
@@ -840,7 +848,10 @@ export const Dashboard = () => {
<button
type="button"
onClick={() => setShowBookmarkModal(true)}
onClick={() => {
setShowBookmarkModal(true);
haptics.impact();
}}
class="w-full inline-flex justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-secondary text-secondary-foreground shadow hover:bg-secondary/90 items-start flex-col gap-4 py-4 px-4 text-left"
>
<IconBookmark class="size-5" />
@@ -922,7 +933,10 @@ export const Dashboard = () => {
<h3 class="font-semibold">Activity Feed</h3>
</div>
<button
onClick={() => setActivityRefreshKey(prev => prev + 1)}
onClick={() => {
setActivityRefreshKey(prev => prev + 1);
haptics.selection();
}}
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-inherit hover:bg-accent/50 hover:text-accent-foreground h-8 w-8"
>
<IconRefresh class="size-4" />
@@ -993,11 +1007,12 @@ export const Dashboard = () => {
{stats().topTags.map((tag) => (
<button
class="inline-flex gap-2 px-2.5 py-1 rounded-lg items-center bg-muted group hover:underline text-xs"
onClick={() =>
onClick={() => {
navigate(
`/app/search?tag=${encodeURIComponent(tag.name)}&query=${encodeURIComponent(tag.name)}`
)
}
);
haptics.navigation();
}}
>
<span
class="size-1.5 rounded-full"
File diff suppressed because it is too large Load Diff
+470 -101
View File
@@ -3,6 +3,7 @@ import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { GitHubActivity } from '@/components/ui/GitHubActivity';
import { getApiV1BaseUrl } from '@/lib/api-url';
import { startGitHubOAuth } from '@/lib/oauth';
import {
IconBrandGithub,
IconTrendingUp,
@@ -14,6 +15,7 @@ import {
IconRefresh,
IconActivity
} from '@tabler/icons-solidjs';
import { useHaptics } from '@/lib/haptics';
interface GitHubRepo {
id: number;
@@ -32,6 +34,40 @@ interface GitHubRepo {
default_branch: string;
}
interface GitHubAppInstallation {
installation_id: number;
account_login: string;
account_type: string;
}
interface GitHubAppStatus {
app_slug: string;
install_enabled: boolean;
credentials_configured: boolean;
installed: boolean;
installation?: GitHubAppInstallation;
}
interface GitHubBackupRecord {
id: number;
repository_full_name: string;
local_path: string;
source: string;
last_backup_status: string;
last_backup_error?: string;
last_backup_at?: string;
last_backup_size: number;
}
interface GitHubBackupResult {
repository: string;
status: string;
local_path?: string;
source: string;
size_bytes?: number;
error?: string;
}
interface GitHubStats {
totalRepos: number;
totalStars: number;
@@ -54,6 +90,7 @@ interface GitHubStats {
const API_BASE_URL = getApiV1BaseUrl();
export const GitHub = () => {
const haptics = useHaptics();
const [githubStats, setGithubStats] = createSignal<GitHubStats>({
totalRepos: 0,
totalStars: 0,
@@ -65,13 +102,40 @@ export const GitHub = () => {
});
const [weeklyActivity, setWeeklyActivity] = createSignal([0, 0, 0, 0, 0, 0, 0]);
const [username, setUsername] = createSignal('');
const [isConnected, setIsConnected] = createSignal(false);
const [appStatus, setAppStatus] = createSignal<GitHubAppStatus>({
app_slug: '',
install_enabled: false,
credentials_configured: false,
installed: false
});
const [backups, setBackups] = createSignal<GitHubBackupRecord[]>([]);
const [backupRoot, setBackupRoot] = createSignal('');
const [selectedRepos, setSelectedRepos] = createSignal<string[]>([]);
const [isBackingUp, setIsBackingUp] = createSignal(false);
const [isInstallingApp, setIsInstallingApp] = createSignal(false);
const [backupMessage, setBackupMessage] = createSignal('');
const [backupError, setBackupError] = createSignal('');
const weeklyTotal = () => weeklyActivity().reduce((a, b) => a + b, 0);
const selectedCount = () => selectedRepos().length;
const getAuthToken = () => {
return localStorage.getItem('trackeep_token') || localStorage.getItem('token') || '';
};
onMount(() => {
// Check if user is authenticated and has GitHub connected
const params = new URLSearchParams(window.location.search);
if (params.get('github_app_installed') === '1') {
setBackupMessage('GitHub App installation completed successfully.');
window.history.replaceState({}, '', window.location.pathname);
}
const callbackError = params.get('github_app_error');
if (callbackError) {
setBackupError(`GitHub App setup failed: ${callbackError}`);
window.history.replaceState({}, '', window.location.pathname);
}
checkGitHubConnection();
});
@@ -86,86 +150,12 @@ export const GitHub = () => {
recentActivity: [],
repos: []
});
};
const checkGitHubConnection = async () => {
try {
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
if (!token) {
resetGitHubData();
return;
}
const response = await fetch(`${API_BASE_URL}/auth/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const userData = await response.json();
if (userData.user.github_id) {
setIsConnected(true);
setUsername(userData.user.username);
await fetchGitHubStats();
} else {
resetGitHubData();
}
} else {
resetGitHubData();
}
} catch (error) {
console.error('Failed to check GitHub connection:', error);
resetGitHubData();
}
};
const fetchGitHubStats = async () => {
try {
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
if (!token) {
throw new Error('No authentication token');
}
const response = await fetch(`${API_BASE_URL}/github/repos`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch GitHub stats');
}
const data = await response.json();
const repos = Array.isArray(data) ? data : (Array.isArray(data?.repos) ? data.repos : []);
// Process real GitHub data
const languages = processLanguages(repos);
const recentActivity = generateRecentActivity(repos);
const totalStars = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.stargazers_count, 0);
const totalForks = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.forks_count, 0);
const totalWatchers = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.watchers_count, 0);
setGithubStats({
totalRepos: repos.length,
totalStars,
totalForks,
totalWatchers,
languages,
recentActivity,
repos
});
} catch (error) {
console.error('Failed to fetch GitHub stats:', error);
resetGitHubData();
}
setSelectedRepos([]);
};
const processLanguages = (repos: GitHubRepo[]) => {
const languageMap = new Map<string, number>();
repos.forEach(repo => {
if (repo.language) {
languageMap.set(repo.language, (languageMap.get(repo.language) || 0) + 1);
@@ -180,8 +170,7 @@ export const GitHub = () => {
};
const generateRecentActivity = (repos: GitHubRepo[]) => {
// Sort repos by updated_at and take recent ones
const sortedRepos = repos
const sortedRepos = [...repos]
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
.slice(0, 5);
@@ -193,15 +182,291 @@ export const GitHub = () => {
}));
};
const updateStatsFromRepos = (repos: GitHubRepo[]) => {
const languages = processLanguages(repos);
const recentActivity = generateRecentActivity(repos);
const totalStars = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.stargazers_count, 0);
const totalForks = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.forks_count, 0);
const totalWatchers = repos.reduce((sum: number, repo: GitHubRepo) => sum + repo.watchers_count, 0);
setGithubStats({
totalRepos: repos.length,
totalStars,
totalForks,
totalWatchers,
languages,
recentActivity,
repos
});
setSelectedRepos(prev => prev.filter(repoName => repos.some(repo => repo.full_name === repoName)));
};
const extractRepos = (payload: unknown): GitHubRepo[] => {
if (Array.isArray(payload)) {
return payload as GitHubRepo[];
}
if (payload && typeof payload === 'object' && Array.isArray((payload as { repos?: unknown[] }).repos)) {
return (payload as { repos: GitHubRepo[] }).repos;
}
return [];
};
const fetchGitHubAppStatus = async (): Promise<GitHubAppStatus | null> => {
try {
const token = getAuthToken();
if (!token) {
return null;
}
const response = await fetch(`${API_BASE_URL}/github/app/status`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
return null;
}
const data = (await response.json()) as GitHubAppStatus;
setAppStatus({
app_slug: data.app_slug || '',
install_enabled: Boolean(data.install_enabled),
credentials_configured: Boolean(data.credentials_configured),
installed: Boolean(data.installed),
installation: data.installation
});
return data;
} catch (error) {
console.error('Failed to fetch GitHub App status:', error);
return null;
}
};
const fetchGitHubBackups = async () => {
try {
const token = getAuthToken();
if (!token) {
return;
}
const response = await fetch(`${API_BASE_URL}/github/backups`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
return;
}
const data = await response.json();
const backupList = Array.isArray(data?.backups) ? (data.backups as GitHubBackupRecord[]) : [];
setBackups(backupList);
setBackupRoot(typeof data?.backup_root === 'string' ? data.backup_root : '');
} catch (error) {
console.error('Failed to fetch GitHub backups:', error);
}
};
const fetchRepos = async (endpoint: string): Promise<boolean> => {
try {
const token = getAuthToken();
if (!token) {
throw new Error('No authentication token');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch GitHub repositories');
}
const data = await response.json();
const repos = extractRepos(data);
updateStatsFromRepos(repos);
return true;
} catch (error) {
console.error('Failed to fetch GitHub repositories:', error);
return false;
}
};
const fetchGitHubStats = async () => {
const loaded = await fetchRepos('/github/repos');
if (!loaded) {
resetGitHubData();
}
};
const fetchGitHubAppRepos = async () => {
const loaded = await fetchRepos('/github/app/repos');
if (!loaded) {
resetGitHubData();
}
};
const checkGitHubConnection = async () => {
try {
const token = getAuthToken();
if (!token) {
setIsConnected(false);
setUsername('');
setAppStatus({
app_slug: '',
install_enabled: false,
credentials_configured: false,
installed: false
});
setBackups([]);
setBackupRoot('');
setBackupMessage('');
setBackupError('');
resetGitHubData();
return;
}
const response = await fetch(`${API_BASE_URL}/auth/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const appInfo = await fetchGitHubAppStatus();
await fetchGitHubBackups();
if (!response.ok) {
resetGitHubData();
return;
}
const userData = await response.json();
const hasOAuthConnection = Boolean(userData?.user?.github_id);
setUsername(typeof userData?.user?.username === 'string' ? userData.user.username : '');
setIsConnected(hasOAuthConnection);
if (hasOAuthConnection) {
await fetchGitHubStats();
return;
}
if (appInfo?.installed && appInfo.credentials_configured) {
await fetchGitHubAppRepos();
return;
}
resetGitHubData();
} catch (error) {
console.error('Failed to check GitHub connection:', error);
resetGitHubData();
}
};
const connectGitHub = () => {
// Redirect to centralized OAuth service
window.location.href = 'https://oauth.trackeep.org/auth/github?redirect_uri=' + encodeURIComponent(window.location.origin + '/api/v1/auth/oauth/callback');
startGitHubOAuth();
};
const installGitHubApp = async () => {
try {
setIsInstallingApp(true);
setBackupError('');
const token = getAuthToken();
if (!token) {
throw new Error('No authentication token');
}
const response = await fetch(`${API_BASE_URL}/github/app/install-url`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (!response.ok || !data?.install_url) {
const message = typeof data?.error === 'string' ? data.error : 'Failed to create install URL';
throw new Error(message);
}
window.location.href = data.install_url as string;
} catch (error) {
console.error('Failed to start GitHub App installation:', error);
setBackupError(error instanceof Error ? error.message : 'Failed to start GitHub App installation');
haptics.warning();
} finally {
setIsInstallingApp(false);
}
};
const toggleRepoSelection = (repoFullName: string) => {
setSelectedRepos(prev => (
prev.includes(repoFullName)
? prev.filter(name => name !== repoFullName)
: [...prev, repoFullName]
));
};
const selectAllRepos = () => {
setSelectedRepos(githubStats().repos.map(repo => repo.full_name));
};
const clearRepoSelection = () => {
setSelectedRepos([]);
};
const backupSelectedRepositories = async () => {
try {
const token = getAuthToken();
if (!token) {
throw new Error('No authentication token');
}
if (selectedRepos().length === 0) {
return;
}
setIsBackingUp(true);
setBackupMessage('');
setBackupError('');
const source = appStatus().installed ? 'github_app' : 'oauth';
const response = await fetch(`${API_BASE_URL}/github/backups`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
repositories: selectedRepos(),
source
})
});
const data = await response.json();
if (!response.ok) {
const errorMessage = typeof data?.error === 'string' ? data.error : 'Backup failed';
throw new Error(errorMessage);
}
const results = Array.isArray(data?.results) ? (data.results as GitHubBackupResult[]) : [];
const failed = Number(data?.failed ?? 0);
const backedUp = Number(data?.backed_up ?? 0);
const firstFailure = results.find(result => result.status !== 'success');
if (failed > 0 && firstFailure?.error) {
setBackupError(firstFailure.error);
}
setBackupMessage(`Backups completed: ${backedUp} succeeded, ${failed} failed.`);
await fetchGitHubBackups();
haptics.impact();
} catch (error) {
console.error('Failed to backup repositories:', error);
setBackupError(error instanceof Error ? error.message : 'Failed to backup repositories');
haptics.warning();
} finally {
setIsBackingUp(false);
}
};
const disconnectGitHub = async () => {
try {
// In a real implementation, you might want to disconnect the GitHub account
// For now, we'll just clear the local state
setIsConnected(false);
setUsername('');
resetGitHubData();
@@ -228,18 +493,25 @@ export const GitHub = () => {
<p class="text-muted-foreground mt-2">Track your GitHub repositories and activity</p>
</div>
<div class="flex gap-2 flex-shrink-0">
<Button variant="outline" size="sm" onClick={() => {
checkGitHubConnection();
haptics.selection();
}}>
<IconRefresh class="size-4 mr-2" />
Refresh
</Button>
{isConnected() ? (
<>
<Button variant="outline" size="sm" onClick={() => fetchGitHubStats()}>
<IconRefresh class="size-4 mr-2" />
Refresh
</Button>
<Button variant="outline" size="sm" onClick={disconnectGitHub}>
Disconnect
</Button>
</>
<Button variant="outline" size="sm" onClick={() => {
disconnectGitHub();
haptics.warning();
}}>
Disconnect
</Button>
) : (
<Button onClick={connectGitHub}>
<Button onClick={() => {
connectGitHub();
haptics.impact();
}}>
<IconBrandGithub class="size-4 mr-2" />
Connect GitHub
</Button>
@@ -248,20 +520,61 @@ export const GitHub = () => {
</div>
{/* Connection Status */}
{isConnected() && (
{(isConnected() || (appStatus().installed && appStatus().credentials_configured)) && (
<Card class="p-4">
<div class="flex items-center gap-3">
<div class="bg-primary/10 flex items-center justify-center p-2 rounded-lg">
<IconBrandGithub class="size-5 text-primary" />
</div>
<div>
<p class="text-sm font-medium text-foreground">Connected as @{username()}</p>
<p class="text-xs text-muted-foreground">Syncing data from GitHub API</p>
<p class="text-sm font-medium text-foreground">
{isConnected() ? `Connected via OAuth as @${username()}` : `Connected via GitHub App as @${username()}`}
</p>
<p class="text-xs text-muted-foreground">
{isConnected() ? 'Syncing data from GitHub OAuth API' : 'Syncing data from GitHub App installation'}
</p>
</div>
</div>
</Card>
)}
{/* GitHub App Status */}
<Card class="p-4">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<p class="text-sm font-medium text-foreground">GitHub App Backup Access</p>
{appStatus().install_enabled ? (
<p class="text-xs text-muted-foreground mt-1">
{appStatus().installed
? `Installed${appStatus().installation?.account_login ? ` for ${appStatus().installation?.account_login}` : ''}`
: 'Not installed yet'}
</p>
) : (
<p class="text-xs text-muted-foreground mt-1">
GitHub App install is not configured on this server.
</p>
)}
{appStatus().install_enabled && !appStatus().credentials_configured && (
<p class="text-xs text-amber-500 mt-1">
App credentials are missing (`GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY`).
</p>
)}
</div>
<div class="flex gap-2 flex-wrap">
<Button variant="outline" size="sm" onClick={() => fetchGitHubAppStatus()}>
Refresh App Status
</Button>
<Button
size="sm"
disabled={!appStatus().install_enabled || isInstallingApp()}
onClick={() => installGitHubApp()}
>
{isInstallingApp() ? 'Opening...' : (appStatus().installed ? 'Reinstall GitHub App' : 'Install GitHub App')}
</Button>
</div>
</div>
</Card>
{/* Stats Overview - 2-column layout with larger left column */}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column - Main Stats */}
@@ -456,15 +769,66 @@ export const GitHub = () => {
{/* Repositories */}
<Card class="p-6">
<h3 class="text-lg font-semibold text-foreground mb-4">Repositories</h3>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-4">
<h3 class="text-lg font-semibold text-foreground">Repositories</h3>
{githubStats().repos.length > 0 && (
<div class="flex gap-2 flex-wrap">
<Button variant="outline" size="sm" onClick={() => selectAllRepos()}>
Select All
</Button>
<Button variant="outline" size="sm" onClick={() => clearRepoSelection()} disabled={selectedCount() === 0}>
Clear
</Button>
<Button
size="sm"
onClick={() => backupSelectedRepositories()}
disabled={selectedCount() === 0 || isBackingUp()}
>
{isBackingUp() ? 'Backing Up...' : `Backup Selected (${selectedCount()})`}
</Button>
</div>
)}
</div>
{backupMessage() && (
<p class="text-sm text-emerald-500 mb-3">{backupMessage()}</p>
)}
{backupError() && (
<p class="text-sm text-red-500 mb-3">{backupError()}</p>
)}
{backupRoot() && (
<p class="text-xs text-muted-foreground mb-3">
Backup root: <span class="font-mono break-all">{backupRoot()}</span>
</p>
)}
{backups().length > 0 && (
<div class="mb-4 p-3 border border-border rounded-lg bg-muted/30 space-y-2">
<p class="text-xs font-medium text-foreground">Recent Local Backups</p>
{backups().slice(0, 5).map((backup) => (
<div class="flex items-center justify-between text-xs text-muted-foreground">
<span class="truncate pr-2">{backup.repository_full_name}</span>
<span class={backup.last_backup_status === 'success' ? 'text-emerald-500' : 'text-red-500'}>
{backup.last_backup_status}
</span>
</div>
))}
</div>
)}
{githubStats().repos.length === 0 ? (
<p class="text-sm text-muted-foreground">No repositories available yet.</p>
) : (
<div class="space-y-4">
{githubStats().repos.map((repo) => (
<div class="border border-border rounded-lg p-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-start gap-3">
<input
type="checkbox"
class="mt-1 h-4 w-4 rounded border-border accent-primary"
checked={selectedRepos().includes(repo.full_name)}
onChange={() => toggleRepoSelection(repo.full_name)}
/>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<h4 class="text-lg font-medium text-foreground">{repo.name}</h4>
{repo.language && (
@@ -493,7 +857,12 @@ export const GitHub = () => {
<span>Updated {formatDate(repo.updated_at)}</span>
</div>
</div>
<Button variant="ghost" size="sm">
<Button
variant="ghost"
size="sm"
onClick={() => window.open(repo.html_url, '_blank', 'noopener,noreferrer')}
aria-label={`Open ${repo.full_name}`}
>
<IconExternalLink class="size-4" />
</Button>
</div>
+11 -2
View File
@@ -26,6 +26,7 @@ import {
IconBrain,
IconBook
} from '@tabler/icons-solidjs';
import { useHaptics } from '@/lib/haptics';
const API_BASE_URL = getApiV1BaseUrl();
@@ -65,6 +66,7 @@ interface LearningPath {
}
export const LearningPaths = () => {
const haptics = useHaptics();
const [learningPaths, setLearningPaths] = createSignal<LearningPath[]>([]);
const [categories, setCategories] = createSignal<string[]>([]);
const [isLoading, setIsLoading] = createSignal(true);
@@ -207,6 +209,7 @@ export const LearningPaths = () => {
setEnrolledPaths(prev => new Set(prev).add(pathId));
setSuccessMessage('Successfully enrolled in learning path!');
setTimeout(() => setSuccessMessage(''), 3000);
haptics.success(); // Success feedback for enrollment
return;
}
@@ -221,6 +224,7 @@ export const LearningPaths = () => {
setEnrolledPaths(prev => new Set(prev).add(pathId));
setSuccessMessage('Successfully enrolled in learning path!');
setTimeout(() => setSuccessMessage(''), 3000);
haptics.success(); // Success feedback for enrollment
} else {
throw new Error('Failed to enroll');
}
@@ -228,6 +232,7 @@ export const LearningPaths = () => {
console.error('Error enrolling in learning path:', error);
setErrorMessage('Failed to enroll. Please try again.');
setTimeout(() => setErrorMessage(''), 3000);
haptics.error(); // Error feedback
}
};
@@ -327,7 +332,10 @@ export const LearningPaths = () => {
<option value="advanced">Advanced</option>
</select>
<Button onClick={handleSearch} class="whitespace-nowrap">
<Button onClick={() => {
handleSearch();
haptics.selection();
}} class="whitespace-nowrap">
<IconFilter class="size-4 mr-2" />
Apply Filters
</Button>
@@ -442,6 +450,7 @@ export const LearningPaths = () => {
onClick={(e) => {
e.stopPropagation();
handleEnroll(path.id);
haptics.impact();
}}
disabled={enrolledPaths().has(path.id)}
class="flex-1"
@@ -465,7 +474,7 @@ export const LearningPaths = () => {
setSearchTerm('');
setSelectedCategory('');
setSelectedDifficulty('');
fetchData();
haptics.selection();
}}>
Clear Filters
</Button>
+21
View File
@@ -2,7 +2,9 @@ import { createSignal, onMount } from 'solid-js';
import { useAuth, type LoginRequest, type RegisterRequest } from '@/lib/auth';
import { isEnvDemoMode } from '@/lib/demo-mode';
import { getApiV1BaseUrl } from '@/lib/api-url';
import { startGitHubOAuth } from '@/lib/oauth';
import { useNavigate } from '@solidjs/router';
import { IconBrandGithub } from '@tabler/icons-solidjs';
const API_BASE_URL = getApiV1BaseUrl();
@@ -81,6 +83,10 @@ export const Login = () => {
fullName: '',
});
}
} else {
// Default to login mode if backend returns error
setIsLogin(true);
setRegistrationDisabled(true);
}
} catch (err) {
console.warn('Failed to check if users exist:', err);
@@ -283,6 +289,21 @@ export const Login = () => {
>
{loading() ? 'Please wait...' : isLogin() ? 'Sign In' : 'Sign Up'}
</button>
<div class="flex items-center gap-3">
<div class="h-px flex-1 bg-[#262626]"></div>
<span class="text-xs uppercase tracking-[0.2em] text-[#6b7280]">or</span>
<div class="h-px flex-1 bg-[#262626]"></div>
</div>
<button
type="button"
onClick={startGitHubOAuth}
class="w-full flex items-center justify-center gap-2 bg-[#0f0f10] text-[#fafafa] py-2 px-4 rounded-md border border-[#262626] hover:border-[#39b9ff]/50 hover:bg-[#18181b] focus:outline-none focus:ring-2 focus:ring-[#39b9ff] focus:ring-offset-2 focus:ring-offset-[#141415] transition-colors"
>
<IconBrandGithub class="size-4" />
Continue with GitHub
</button>
</form>
<div class="mt-6 text-center">
+18 -3
View File
@@ -38,6 +38,7 @@ import {
IconUpload,
IconX,
} from '@tabler/icons-solidjs';
import { useHaptics } from '@/lib/haptics';
import './Messages.css';
interface MemberOption {
@@ -158,6 +159,7 @@ type TriStateFilter = 'any' | 'yes' | 'no';
type CallStatus = 'idle' | 'starting' | 'calling' | 'in_call' | 'error';
export const Messages = () => {
const haptics = useHaptics();
const [conversations, setConversations] = createSignal<ConversationListItem[]>([]);
const [messages, setMessages] = createSignal<Message[]>([]);
const [selectedConversationId, setSelectedConversationId] = createSignal<number | null>(null);
@@ -283,6 +285,7 @@ export const Messages = () => {
const openConversation = (conversationId: number) => {
setSelectedConversationId(conversationId);
setActiveScreen('conversation');
haptics.selection(); // Selection feedback for opening conversation
};
const closeConversation = () => {
@@ -1680,8 +1683,10 @@ export const Messages = () => {
setMentionOptions([]);
setMentionOpen(false);
setMentionQuery('');
haptics.success(); // Success feedback for sending message
} catch (error) {
toast.error('Failed to send message', error instanceof Error ? error.message : 'Unknown error');
haptics.error(); // Error feedback
} finally {
setUploadProgress(null);
}
@@ -1943,7 +1948,9 @@ export const Messages = () => {
onCleanup(() => clearInterval(wsWatcher));
return (
<div class={`messages-shell ${activeScreen() === 'conversation' ? 'messages-shell-conversation' : 'messages-shell-list'}`}>
<div class="mt-4 pb-32 max-w-7xl mx-auto">
<div class="bg-background rounded-lg border shadow-sm">
<div class={`messages-shell ${activeScreen() === 'conversation' ? 'messages-shell-conversation' : 'messages-shell-list'}`}>
<aside class="messages-sidebar">
<div class="messages-sidebar-header">
<div class="messages-title-row">
@@ -2021,7 +2028,10 @@ export const Messages = () => {
<section class="messages-main">
<header class="messages-main-header">
<div class="messages-header-main">
<Button size="sm" variant="ghost" class="messages-back-button" onClick={closeConversation}>
<Button size="sm" variant="ghost" class="messages-back-button" onClick={() => {
closeConversation();
haptics.navigation();
}}>
<IconChevronLeft class="size-4" />
</Button>
<div class="messages-header-meta">
@@ -2491,7 +2501,10 @@ export const Messages = () => {
</div>
<Button
onClick={() => sendMessage()}
onClick={() => {
sendMessage();
haptics.impact();
}}
disabled={
sendingMessage() ||
(!inputText().trim() && selectedFiles().length === 0 && attachedLibraryFiles().length === 0 && composerAiReferences().length === 0)
@@ -2976,6 +2989,8 @@ export const Messages = () => {
</div>
</Show>
</div>
</div>
</div>
);
};
+6 -1
View File
@@ -7,6 +7,7 @@ import { IconPin, IconTrash, IconEdit, IconCopy, IconDownload, IconPaperclip } f
import { getMockNotes } from '@/lib/mockData';
import { isDemoMode, shouldUseRealBackend } from '@/lib/demo-mode';
import { getApiV1BaseUrl } from '@/lib/api-url';
import { useHaptics } from '@/lib/haptics';
const API_BASE_URL = getApiV1BaseUrl();
@@ -66,6 +67,7 @@ const renderPlainTextPreviewHtml = (content: string): string => {
};
export const Notes = () => {
const haptics = useHaptics();
const [notes, setNotes] = createSignal<Note[]>([]);
const [isLoading, setIsLoading] = createSignal(true);
const [searchTerm, setSearchTerm] = createSignal('');
@@ -518,7 +520,10 @@ export const Notes = () => {
<p class="text-sm text-[#b9b9b9]">Your personal notes</p>
</div>
<Button
onClick={() => setShowAddModal(true)}
onClick={() => {
setShowAddModal(true);
haptics.impact();
}}
class="bg-[#5865f2] hover:bg-[#4752c4] text-white border-0"
>
Add Note
+3 -1
View File
@@ -1,8 +1,10 @@
import { Button } from '@/components/ui/Button';
import { GitHubActivity } from '@/components/ui/GitHubActivity';
import { IconSettings } from '@tabler/icons-solidjs';
import { useHaptics } from '@/lib/haptics';
export const Profile = () => {
const haptics = useHaptics();
// Custom events for Profile page
const profileEvents = [
{
@@ -72,7 +74,7 @@ export const Profile = () => {
<p class="text-muted-foreground mt-2">Track your contributions and activity over time</p>
</div>
<div class="flex gap-2">
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" onClick={() => haptics.impact()}>
<IconSettings class="size-4 mr-2" />
Settings
</Button>
+14 -3
View File
@@ -4,9 +4,11 @@ import { IconUser, IconLock, IconKey, IconBrain, IconMail, IconSend, IconShield,
import { TwoFactorAuth } from '@/components/TwoFactorAuth';
import { Button } from '@/components/ui/Button';
import { AIProviderIcon } from '@/components/AIProviderIcon';
import { useHaptics } from '@/lib/haptics';
export const Settings = () => {
const { authState, updateProfile, changePassword } = useAuth();
const haptics = useHaptics();
const [isLoading, setIsLoading] = createSignal(false);
const [message, setMessage] = createSignal('');
const [profileData, setProfileData] = createSignal({
@@ -276,8 +278,10 @@ export const Settings = () => {
localStorage.setItem('showBrowserSearch', profileData().showBrowserSearch.toString());
setMessage('Profile updated successfully!');
haptics.success();
} catch (error) {
setMessage(error instanceof Error ? error.message : 'Failed to update profile');
haptics.error();
} finally {
setIsLoading(false);
}
@@ -286,6 +290,7 @@ export const Settings = () => {
const handleChangePassword = async () => {
if (passwordData().newPassword !== passwordData().confirmPassword) {
setMessage('New passwords do not match');
haptics.warning();
return;
}
@@ -299,8 +304,10 @@ export const Settings = () => {
});
setMessage('Password changed successfully!');
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
haptics.success();
} catch (error) {
setMessage(error instanceof Error ? error.message : 'Failed to change password');
haptics.error();
} finally {
setIsLoading(false);
}
@@ -358,7 +365,10 @@ export const Settings = () => {
<For each={tabs}>
{(tab) => (
<button
onClick={() => setActiveTab(tab.id)}
onClick={() => {
setActiveTab(tab.id);
haptics.selection();
}}
class={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab() === tab.id
? 'border-primary text-primary'
@@ -480,7 +490,7 @@ export const Settings = () => {
type="button"
onClick={handleUpdateProfile}
disabled={isLoading()}
class="inline-flex justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-auto items-center gap-2 py-2 px-4 w-full"
class="inline-flex justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-10 px-4 py-2 disabled:opacity-50"
>
{isLoading() ? 'Updating...' : 'Update Profile'}
</button>
@@ -626,8 +636,9 @@ export const Settings = () => {
...settings,
mistral: { ...settings.mistral, enabled: !settings.mistral.enabled }
});
haptics.selection();
}}
class="flex items-center gap-2"
class="justify-start"
>
<AIProviderIcon
providerId="mistral"
+15 -1
View File
@@ -5,6 +5,7 @@ import { SearchTagFilterBar } from '@/components/ui/SearchTagFilterBar';
import { TaskModal } from '@/components/ui/TaskModal';
import { IconEdit, IconTrash } from '@tabler/icons-solidjs';
import { getApiV1BaseUrl } from '@/lib/api-url';
import { useHaptics } from '@/lib/haptics';
const API_BASE_URL = getApiV1BaseUrl();
@@ -27,6 +28,8 @@ export const Tasks = () => {
const [filter, setFilter] = createSignal<'all' | 'active' | 'completed'>('all');
const [searchTerm, setSearchTerm] = createSignal('');
const [selectedPriority, setSelectedPriority] = createSignal('');
const haptics = useHaptics();
onMount(async () => {
try {
@@ -91,7 +94,9 @@ export const Tasks = () => {
const newTask = await response.json();
setTasks(prev => [newTask, ...prev]);
setShowAddModal(false);
haptics.success(); // Success feedback for adding task
} catch (error) {
haptics.error(); // Error feedback
alert(error instanceof Error ? error.message : 'Failed to add task');
}
};
@@ -122,7 +127,9 @@ export const Tasks = () => {
);
setShowEditModal(false);
setEditingTask(null);
haptics.success(); // Success feedback for editing task
} catch (error) {
haptics.error(); // Error feedback
alert(error instanceof Error ? error.message : 'Failed to update task');
}
};
@@ -133,7 +140,9 @@ export const Tasks = () => {
setTasks(prev => prev.map(task =>
task.id === taskId ? { ...task, completed: !task.completed } : task
));
haptics.completion(); // Completion feedback for toggling task
} catch (error) {
haptics.error(); // Error feedback
console.error('Failed to update task:', error);
}
};
@@ -154,7 +163,9 @@ export const Tasks = () => {
}
setTasks(prev => prev.filter(task => task.id !== taskId));
haptics.delete(); // Delete feedback
} catch (error) {
haptics.error(); // Error feedback
alert(error instanceof Error ? error.message : 'Failed to delete task');
}
}
@@ -188,7 +199,7 @@ export const Tasks = () => {
<div class="p-6 space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold text-[#fafafa]">Tasks</h1>
<Button onClick={() => setShowAddModal(true)}>
<Button onClick={() => setShowAddModal(true)} haptic="impact">
Add Task
</Button>
</div>
@@ -245,6 +256,7 @@ export const Tasks = () => {
variant={filter() === filterOption ? 'default' : 'outline'}
onClick={() => setFilter(filterOption)}
class="capitalize"
haptic="selection"
>
{filterOption}
</Button>
@@ -297,6 +309,7 @@ export const Tasks = () => {
editTask(task);
}}
class="text-blue-400 hover:text-blue-300"
haptic="impact"
>
<IconEdit class="w-4 h-4" />
</Button>
@@ -308,6 +321,7 @@ export const Tasks = () => {
deleteTask(task.id);
}}
class="text-red-400 hover:text-red-300"
haptic="warning"
>
<IconTrash class="w-4 h-4" />
</Button>
+5 -2
View File
@@ -4,8 +4,10 @@ import { TimeEntriesList } from '@/components/TimeEntriesList';
import { type TimeEntry, timeEntriesApi, demoTimeEntriesApi } from '@/lib/api';
import { IconClock, IconActivity, IconCurrencyDollar } from '@tabler/icons-solidjs';
import { isDemoMode } from '@/lib/demo-mode';
import { useHaptics } from '@/lib/haptics';
export const TimeTracking = () => {
const haptics = useHaptics();
const [refreshTrigger, setRefreshTrigger] = createSignal(0);
const [timeEntries, setTimeEntries] = createSignal<TimeEntry[]>([]);
const [loading, setLoading] = createSignal(true);
@@ -100,9 +102,10 @@ export const TimeTracking = () => {
return `$${amount.toFixed(2)}`;
};
const handleTimeEntryCreated = (_timeEntry: TimeEntry) => {
// Trigger refresh of the time entries list
const handleTimeEntryCreated = (newEntry: TimeEntry) => {
setTimeEntries(prev => [newEntry, ...prev]);
setRefreshTrigger(prev => prev + 1);
haptics.success(); // Success feedback for time entry creation
};
// Handle real-time timer updates
+24 -7
View File
@@ -11,6 +11,7 @@ import { getApiV1BaseUrl } from '@/lib/api-url';
import {
IconAlertCircle
} from '@tabler/icons-solidjs';
import { useHaptics } from '@/lib/haptics';
type TabType = 'search' | 'predefined' | 'bookmarked';
@@ -40,6 +41,7 @@ interface VideoCardProps {
video: YouTubeVideo;
onPreview: (video: YouTubeVideo) => void;
onSave?: (video: YouTubeVideo) => void;
haptics: ReturnType<typeof useHaptics>;
}
const VideoCard = (props: VideoCardProps) => (
@@ -58,7 +60,10 @@ const VideoCard = (props: VideoCardProps) => (
{/* Play button overlay */}
<div
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer"
onClick={() => props.onPreview(props.video)}
onClick={() => {
props.onPreview(props.video);
props.haptics.impact();
}}
role="button"
aria-label="Play video"
tabIndex={0}
@@ -100,14 +105,20 @@ const VideoCard = (props: VideoCardProps) => (
<Button
variant="outline"
size="sm"
onClick={() => props.onPreview(props.video)}
onClick={() => {
props.onPreview(props.video);
props.haptics.impact();
}}
>
Preview
</Button>
{props.onSave && (
<Button
size="sm"
onClick={() => props.onSave?.(props.video)}
onClick={() => {
props.onSave?.(props.video);
props.haptics.success();
}}
>
Save
</Button>
@@ -119,6 +130,7 @@ const VideoCard = (props: VideoCardProps) => (
);
export const Youtube = () => {
const haptics = useHaptics();
const [activeTab, setActiveTab] = createSignal<TabType>('search');
const [searchQuery, setSearchQuery] = createSignal('');
const [videos, setVideos] = createSignal<YouTubeVideo[]>([]);
@@ -438,6 +450,7 @@ export const Youtube = () => {
// Load predefined videos when tab is switched to predefined
const handleTabChange = (tab: TabType) => {
setActiveTab(tab);
haptics.selection(); // Selection feedback for tab switching
if (tab === 'predefined' && predefinedVideos().length === 0) {
loadPredefinedVideos();
}
@@ -449,6 +462,7 @@ export const Youtube = () => {
setIsLoading(true);
setError('');
haptics.impact(); // Impact feedback for search
try {
// Always use real data, no demo mode check
@@ -473,6 +487,7 @@ export const Youtube = () => {
};
setVideos([video]);
haptics.success(); // Success feedback for found video
} else {
// It's a regular search query - use backend API
try {
@@ -499,6 +514,7 @@ export const Youtube = () => {
}));
setVideos(videos);
haptics.success(); // Success feedback for search results
} else {
throw new Error('Search API failed');
}
@@ -511,6 +527,7 @@ export const Youtube = () => {
console.error('Search failed:', err);
setError(err instanceof Error ? err.message : 'Failed to search YouTube');
setTimeout(() => setError(''), 3000);
haptics.error(); // Error feedback
} finally {
setIsLoading(false);
}
@@ -807,7 +824,7 @@ export const Youtube = () => {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={videos()}>
{(video) => (
<VideoCard video={video} onPreview={handlePreviewVideo} onSave={handleSaveVideo} />
<VideoCard video={video} onPreview={handlePreviewVideo} onSave={handleSaveVideo} haptics={haptics} />
)}
</For>
</div>
@@ -821,7 +838,7 @@ export const Youtube = () => {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={predefinedVideos()}>
{(video) => (
<VideoCard video={video} onPreview={handlePreviewVideo} onSave={handleSaveVideo} />
<VideoCard video={video} onPreview={handlePreviewVideo} onSave={handleSaveVideo} haptics={haptics} />
)}
</For>
</div>
@@ -835,7 +852,7 @@ export const Youtube = () => {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={savedVideos()}>
{(video) => (
<VideoCard video={video} onPreview={handlePreviewVideo} />
<VideoCard video={video} onPreview={handlePreviewVideo} haptics={haptics} />
)}
</For>
</div>
@@ -862,7 +879,7 @@ export const Youtube = () => {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={savedVideos()}>
{(video) => (
<VideoCard video={video} onPreview={handlePreviewVideo} />
<VideoCard video={video} onPreview={handlePreviewVideo} haptics={haptics} />
)}
</For>
</div>
-3
View File
@@ -32,7 +32,6 @@ export const updateService = {
async checkForUpdates(): Promise<UpdateCheckResponse> {
// If in demo mode, return mock update data
if (isDemoMode()) {
console.log('[Demo Mode] Using mock update data');
return {
updateAvailable: true,
currentVersion: '1.0.0',
@@ -84,7 +83,6 @@ export const updateService = {
async installUpdate(version: string): Promise<{ message: string; version: string }> {
// If in demo mode, simulate update installation
if (isDemoMode()) {
console.log('[Demo Mode] Simulating update installation for version:', version);
return {
message: 'Update started',
version: version
@@ -116,7 +114,6 @@ export const updateService = {
async getUpdateProgress(): Promise<UpdateStatus> {
// If in demo mode, return mock progress
if (isDemoMode()) {
console.log('[Demo Mode] Using mock update progress');
return {
available: true,
downloading: false,