Configure Docker publishing with correct GitHub username

This commit is contained in:
Tomas Dvorak
2026-02-27 17:34:20 +01:00
parent 4c812e376d
commit 0a80ecd9f7
138 changed files with 12128 additions and 7829 deletions
@@ -27,11 +27,13 @@ export const AuthenticationWarning = () => {
<div class="text-center mb-8">
<div class="mb-6">
<div class="inline-flex items-center justify-center mb-4">
<img
src="/trackeepfavi_bg.png"
alt="Trackeep Logo"
class="w-12 h-12 rounded-xl"
/>
<div class="inline-flex items-center justify-center p-2.5 rounded-xl border border-border bg-muted/40">
<img
src="/trackeep.svg"
alt="Trackeep Logo"
class="w-9 h-9 app-logo-mono"
/>
</div>
</div>
<h1 class="text-2xl font-bold tracking-tight mb-2 text-foreground">Authentication Required</h1>
<p class="text-muted-foreground">Please sign in to access Trackeep</p>
+20 -20
View File
@@ -1,32 +1,32 @@
import { useAuth } from '@/lib/auth';
import { AuthenticationWarning } from '@/components/AuthenticationWarning';
import { isDemoMode } from '@/lib/demo-mode';
import { Show } from 'solid-js';
interface ProtectedRouteProps {
children: any;
}
export const ProtectedRoute = (props: ProtectedRouteProps) => {
// In demo mode, show UI immediately without any checks
if (isDemoMode()) {
console.log('[ProtectedRoute] Demo mode active - showing UI immediately');
return props.children;
}
const { authState } = useAuth();
console.log('[ProtectedRoute] Render:', {
isDemoMode: isDemoMode(),
isAuthenticated: authState.isAuthenticated,
isLoading: authState.isLoading
});
// If not authenticated, show authentication warning (no loading state)
if (!authState.isAuthenticated) {
console.log('[ProtectedRoute] Rendering authentication warning');
return <AuthenticationWarning />;
}
console.log('[ProtectedRoute] Rendering children');
return props.children;
return (
<Show when={!isDemoMode()} fallback={props.children}>
<Show
when={!authState.isLoading}
fallback={
<div class="min-h-screen bg-background flex items-center justify-center px-4 py-8">
<div class="text-center">
<div class="inline-block w-8 h-8 border-2 border-primary border-r-transparent rounded-full animate-spin mb-3"></div>
<p class="text-sm text-muted-foreground">Checking authentication...</p>
</div>
</div>
}
>
<Show when={authState.isAuthenticated} fallback={<AuthenticationWarning />}>
{props.children}
</Show>
</Show>
</Show>
);
};
+1 -7
View File
@@ -16,6 +16,7 @@ import {
type TimeEntry
} from '../lib/api';
import { TagPicker } from '@/components/ui/TagPicker';
import { isDemoMode } from '@/lib/demo-mode';
interface TimerProps {
onTimeEntryCreated?: (timeEntry: TimeEntry) => void;
@@ -38,13 +39,6 @@ export const Timer = (props: TimerProps) => {
const [showSettings, setShowSettings] = createSignal(false);
const [availableTags, setAvailableTags] = createSignal<string[]>([]);
// Check if we're in demo mode
const isDemoMode = () => {
return localStorage.getItem('demoMode') === 'true' ||
document.title.includes('Demo Mode') ||
window.location.search.includes('demo=true');
};
// Use appropriate API based on demo mode
const getApi = () => isDemoMode() ? demoTimeEntriesApi : timeEntriesApi;
@@ -1,6 +1,7 @@
import { createSignal, Show } from 'solid-js'
import { IconX, IconSend, IconUser, IconChevronDown } from '@tabler/icons-solidjs'
import longcatIcon from '@/assets/longcat-color.svg'
import { ModalPortal } from '@/components/ui/ModalPortal'
interface FloatingAIProps {
onToggleChat: () => void
@@ -79,8 +80,9 @@ export function FloatingAI(props: FloatingAIProps) {
{/* AI Chat Modal */}
<Show when={props.isChatOpen}>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 mt-0 p-4">
<div class="bg-card border border-border rounded-lg shadow-xl max-w-md w-full max-h-[600px] flex flex-col" style="width: 420px;">
<ModalPortal>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-card border border-border rounded-lg shadow-xl max-w-md w-full max-h-[600px] flex flex-col" style="width: 420px;">
{/* Header */}
<div class="flex items-center justify-between p-4 border-b border-border bg-gradient-to-r from-primary/10 to-primary/5">
<div class="flex items-center gap-3">
@@ -177,8 +179,9 @@ export function FloatingAI(props: FloatingAIProps) {
</button>
</div>
</div>
</div>
</div>
</div>
</ModalPortal>
</Show>
</>
)
+12
View File
@@ -56,6 +56,18 @@ export function Header(props: HeaderProps) {
<div class="flex justify-between px-6 pt-4 pb-4">
{/* Left side */}
<div class="flex items-center">
<a
href="/app"
class="hidden sm:inline-flex items-center gap-2 rounded-md px-2 py-1.5 mr-2 hover:bg-accent/40 transition-colors"
>
<img
src="/trackeep.svg"
alt="Trackeep Logo"
class="w-6 h-6 app-logo-mono"
/>
<span class="text-sm font-semibold tracking-tight text-foreground">Trackeep</span>
</a>
{/* Menu button */}
<button
type="button"
+13 -3
View File
@@ -14,9 +14,16 @@ export interface LayoutProps {
export function Layout(props: LayoutProps) {
const resolved = children(() => props.children)
const [isChatOpen, setIsChatOpen] = createSignal(false)
const [isSidebarOpen, setIsSidebarOpen] = createSignal(false)
const [isSidebarOpen, setIsSidebarOpen] = createSignal(true)
onMount(() => {
const savedSidebarState = localStorage.getItem('trackeep_sidebar_open')
if (savedSidebarState !== null) {
setIsSidebarOpen(savedSidebarState === 'true')
} else {
setIsSidebarOpen(window.innerWidth >= 768)
}
// Initialize dark mode from localStorage or system preference
const savedTheme = localStorage.getItem('theme')
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
@@ -143,11 +150,14 @@ export function Layout(props: LayoutProps) {
}
const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen())
const nextValue = !isSidebarOpen()
setIsSidebarOpen(nextValue)
localStorage.setItem('trackeep_sidebar_open', String(nextValue))
}
const closeSidebar = () => {
setIsSidebarOpen(false)
localStorage.setItem('trackeep_sidebar_open', 'false')
}
return (
@@ -157,7 +167,7 @@ export function Layout(props: LayoutProps) {
{/* Mobile Sidebar Overlay */}
{isSidebarOpen() && (
<div
class="fixed inset-0 bg-black/50 z-40"
class="fixed inset-0 bg-black/50 z-40 md:hidden"
onClick={closeSidebar}
/>
)}
+351 -31
View File
@@ -1,4 +1,4 @@
import { For, createSignal, onMount, Show } from 'solid-js'
import { For, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { A, useLocation } from '@solidjs/router'
import {
IconBookmark,
@@ -21,10 +21,15 @@ import {
IconMessageCircle,
IconLogout,
IconBuilding,
IconPlus,
IconX
IconPlus
} from '@tabler/icons-solidjs'
import { UpdateChecker } from '../ui/UpdateChecker'
import { Input } from '../ui/Input'
import { Button } from '../ui/Button'
import { Switch } from '../ui/Switch'
import { ModalPortal } from '../ui/ModalPortal'
import { useAuth } from '@/lib/auth'
import { getApiV1BaseUrl } from '@/lib/api-url'
const navigation = [
{ name: 'Home', href: '/app', icon: IconHome },
@@ -43,11 +48,23 @@ const navigation = [
{ name: 'AI Assistant', href: '/app/chat', icon: IconBrain },
]
const mockWorkspaces = [
{ id: '1', name: 'Trackeep Workspace', icon: IconFileText },
{ id: '2', name: 'Personal Projects', icon: IconBuilding },
{ id: '3', name: 'Team Collaboration', icon: IconUsers },
]
const API_BASE_URL = getApiV1BaseUrl()
const DEFAULT_WORKSPACE_NAME = 'Trackeep Workspace'
interface WorkspaceOption {
id: string
name: string
icon: typeof IconFileText
}
const getWorkspaceIcon = (name: string) => {
const lower = name.toLowerCase()
if (lower.includes('team')) return IconUsers
if (lower.includes('personal')) return IconBuilding
return IconFileText
}
const getAuthToken = () => localStorage.getItem('trackeep_token') || localStorage.getItem('token') || ''
export interface SidebarProps {
class?: string
@@ -57,8 +74,35 @@ export interface SidebarProps {
export function Sidebar(props: SidebarProps) {
const location = useLocation()
const { logout } = useAuth()
const [isWorkspaceDropdownOpen, setIsWorkspaceDropdownOpen] = createSignal(false)
const [selectedWorkspace, setSelectedWorkspace] = createSignal(mockWorkspaces[0])
const [workspaces, setWorkspaces] = createSignal<WorkspaceOption[]>([])
const [selectedWorkspaceId, setSelectedWorkspaceId] = createSignal<string>('')
const [isCreateWorkspaceModalOpen, setIsCreateWorkspaceModalOpen] = createSignal(false)
const [workspaceName, setWorkspaceName] = createSignal('')
const [workspaceDescription, setWorkspaceDescription] = createSignal('')
const [workspaceIsPublic, setWorkspaceIsPublic] = createSignal(false)
const [isCreatingWorkspace, setIsCreatingWorkspace] = createSignal(false)
const [createWorkspaceError, setCreateWorkspaceError] = createSignal('')
const selectedWorkspace = () => {
const list = workspaces()
const found = list.find((workspace) => workspace.id === selectedWorkspaceId())
return found || list[0] || { id: 'default', name: DEFAULT_WORKSPACE_NAME, icon: IconFileText }
}
const persistSelectedWorkspace = (workspace: WorkspaceOption) => {
localStorage.setItem('trackeep_workspace_id', workspace.id)
localStorage.setItem('trackeep_workspace_name', workspace.name)
window.dispatchEvent(
new CustomEvent('trackeep:workspace-changed', {
detail: {
id: workspace.id,
name: workspace.name,
},
}),
)
}
const isActive = (href: string) => {
const currentPath = location.pathname
@@ -66,17 +110,206 @@ export function Sidebar(props: SidebarProps) {
return currentPath === href
}
const handleWorkspaceSelect = (workspace: typeof mockWorkspaces[0]) => {
setSelectedWorkspace(workspace)
const handleWorkspaceSelect = (workspace: WorkspaceOption) => {
setSelectedWorkspaceId(workspace.id)
persistSelectedWorkspace(workspace)
setIsWorkspaceDropdownOpen(false)
}
const resetCreateWorkspaceForm = () => {
setWorkspaceName('')
setWorkspaceDescription('')
setWorkspaceIsPublic(false)
setCreateWorkspaceError('')
}
const openCreateWorkspaceModal = () => {
setIsWorkspaceDropdownOpen(false)
resetCreateWorkspaceForm()
setIsCreateWorkspaceModalOpen(true)
}
const closeCreateWorkspaceModal = () => {
if (isCreatingWorkspace()) return
setIsCreateWorkspaceModalOpen(false)
resetCreateWorkspaceForm()
}
const toggleWorkspaceDropdown = () => {
setIsWorkspaceDropdownOpen(!isWorkspaceDropdownOpen())
}
const normalizeWorkspace = (team: { id?: number | string; name?: string }): WorkspaceOption => {
const name = team.name?.trim() || DEFAULT_WORKSPACE_NAME
return {
id: String(team.id ?? `workspace-${Date.now()}`),
name,
icon: getWorkspaceIcon(name),
}
}
const createDefaultWorkspace = async (token: string): Promise<WorkspaceOption | null> => {
const response = await fetch(`${API_BASE_URL}/teams`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: DEFAULT_WORKSPACE_NAME,
description: 'Default workspace',
is_public: false,
}),
})
if (!response.ok) {
return null
}
const data = await response.json()
if (!data?.team) {
return null
}
return normalizeWorkspace(data.team)
}
const loadWorkspaces = async () => {
const token = getAuthToken()
if (!token) {
const fallbackWorkspace = {
id: 'local-default',
name: DEFAULT_WORKSPACE_NAME,
icon: IconFileText,
}
setWorkspaces([fallbackWorkspace])
setSelectedWorkspaceId(fallbackWorkspace.id)
persistSelectedWorkspace(fallbackWorkspace)
return
}
try {
const response = await fetch(`${API_BASE_URL}/teams`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
let mappedWorkspaces: WorkspaceOption[] = []
if (response.ok) {
const data = await response.json()
const teams = Array.isArray(data?.teams) ? data.teams : []
mappedWorkspaces = teams.map(normalizeWorkspace)
}
if (mappedWorkspaces.length === 0) {
const created = await createDefaultWorkspace(token)
if (created) {
mappedWorkspaces = [created]
}
}
if (mappedWorkspaces.length === 0) {
mappedWorkspaces = [
{
id: 'local-default',
name: DEFAULT_WORKSPACE_NAME,
icon: IconFileText,
},
]
}
setWorkspaces(mappedWorkspaces)
const persistedWorkspaceId = localStorage.getItem('trackeep_workspace_id') || ''
const initialSelection =
mappedWorkspaces.find((workspace) => workspace.id === persistedWorkspaceId) || mappedWorkspaces[0]
setSelectedWorkspaceId(initialSelection.id)
persistSelectedWorkspace(initialSelection)
} catch (error) {
console.error('Failed to load workspaces:', error)
const fallbackWorkspace = {
id: 'local-default',
name: DEFAULT_WORKSPACE_NAME,
icon: IconFileText,
}
setWorkspaces([fallbackWorkspace])
setSelectedWorkspaceId(fallbackWorkspace.id)
persistSelectedWorkspace(fallbackWorkspace)
}
}
const handleCreateWorkspace = async () => {
const trimmed = workspaceName().trim()
const description = workspaceDescription().trim()
const isPublic = workspaceIsPublic()
if (!trimmed) {
setCreateWorkspaceError('Workspace name is required.')
return
}
setCreateWorkspaceError('')
setIsCreatingWorkspace(true)
const token = getAuthToken()
if (!token) {
const localWorkspace = {
id: `local-${Date.now()}`,
name: trimmed,
icon: getWorkspaceIcon(trimmed),
}
setWorkspaces((prev) => [localWorkspace, ...prev])
handleWorkspaceSelect(localWorkspace)
setIsCreateWorkspaceModalOpen(false)
resetCreateWorkspaceForm()
setIsCreatingWorkspace(false)
return
}
try {
const response = await fetch(`${API_BASE_URL}/teams`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: trimmed,
description,
is_public: isPublic,
}),
})
if (!response.ok) {
let message = `Failed to create workspace: ${response.status}`
try {
const data = await response.json()
message = data?.error || data?.message || message
} catch {
// Keep fallback message
}
throw new Error(message)
}
const data = await response.json()
const createdWorkspace = normalizeWorkspace(data.team)
setWorkspaces((prev) => [createdWorkspace, ...prev])
handleWorkspaceSelect(createdWorkspace)
setIsCreateWorkspaceModalOpen(false)
resetCreateWorkspaceForm()
} catch (error) {
console.error('Failed to create workspace:', error)
setCreateWorkspaceError(error instanceof Error ? error.message : 'Failed to create workspace.')
} finally {
setIsCreatingWorkspace(false)
}
}
// Close dropdown when clicking outside
onMount(() => {
void loadWorkspaces()
const handleClickOutside = (event: MouseEvent) => {
const target = event.target
if (!(target instanceof HTMLElement)) return
@@ -85,28 +318,34 @@ export function Sidebar(props: SidebarProps) {
}
}
document.addEventListener('click', handleClickOutside)
return () => document.removeEventListener('click', handleClickOutside)
onCleanup(() => document.removeEventListener('click', handleClickOutside))
})
return (
<>
{/* Mobile Close Button - Above sidebar */}
<Show when={props.isOpen}>
<button
onClick={props.onClose}
class="fixed top-4 right-4 z-50 md:hidden 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"
>
<IconX class="size-4" />
</button>
</Show>
<div class={`fixed inset-y-0 left-0 z-50 w-280px border-r border-r-border flex-shrink-0 bg-card transform transition-transform duration-300 ease-in-out md:relative md:translate-x-0 ${
props.isOpen ? 'translate-x-0' : '-translate-x-full'
}`}>
<div class="flex h-full">
<div
class={`fixed inset-y-0 left-0 z-50 border-r border-r-border bg-card transition-all duration-300 ease-in-out overflow-hidden md:relative md:inset-y-auto md:left-auto md:transform-none ${
props.isOpen ? 'w-[280px] translate-x-0' : 'w-[280px] -translate-x-full md:w-0 md:translate-x-0 md:pointer-events-none'
}`}
>
<div class="w-[280px] h-full flex">
<div class="h-full flex flex-col pb-6 flex-1 min-w-0">
<div class="px-4 pt-4">
<A
href="/app"
class="flex items-center gap-3 rounded-lg px-2 py-2 hover:bg-accent/40 transition-colors"
>
<img
src="/trackeep.svg"
alt="Trackeep Logo"
class="w-7 h-7 app-logo-mono"
/>
<span class="font-semibold tracking-tight text-foreground">Trackeep</span>
</A>
</div>
{/* Organization Selector */}
<div class="p-4 pb-0 min-w-0 max-w-full" id="workspace-selector">
<div class="p-4 pb-0 pt-3 min-w-0 max-w-full" id="workspace-selector">
<div role="group" class="w-full relative">
<button
type="button"
@@ -133,7 +372,7 @@ export function Sidebar(props: SidebarProps) {
<Show when={isWorkspaceDropdownOpen()}>
<div class="absolute top-full left-0 right-0 mt-1 bg-popover border border-border rounded-md shadow-lg z-50 max-h-60 overflow-auto">
<div class="p-1" role="listbox">
<For each={mockWorkspaces}>
<For each={workspaces()}>
{(workspace) => (
<button
type="button"
@@ -156,6 +395,7 @@ export function Sidebar(props: SidebarProps) {
<div class="border-t border-border mt-1 pt-1">
<button
type="button"
onClick={openCreateWorkspaceModal}
class="flex w-full items-center gap-2 px-3 py-2 text-sm rounded-sm hover:bg-accent/50 transition-colors focus:bg-accent/50 focus:outline-none text-muted-foreground"
>
<IconPlus class="size-4" />
@@ -262,9 +502,8 @@ export function Sidebar(props: SidebarProps) {
}}></div>
</A>
<button
onClick={() => {
// Handle logout logic here
localStorage.removeItem('auth_token')
onClick={async () => {
await logout()
window.location.href = '/login'
}}
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 w-full relative overflow-hidden hover:bg-destructive/10 hover:text-destructive dark:text-muted-foreground"
@@ -279,6 +518,87 @@ export function Sidebar(props: SidebarProps) {
</div>
</div>
</div>
<Show when={isCreateWorkspaceModalOpen()}>
<ModalPortal>
<>
<div
class="fixed inset-0 z-[90] bg-black/50"
onClick={closeCreateWorkspaceModal}
/>
<div class="fixed top-1/2 left-1/2 z-[100] w-full max-w-md -translate-x-1/2 -translate-y-1/2 px-4">
<div class="rounded-lg border border-border bg-card shadow-xl">
<div class="border-b border-border p-5">
<h3 class="text-lg font-semibold text-foreground">Create Workspace</h3>
<p class="mt-1 text-sm text-muted-foreground">Add a new workspace for your team or projects.</p>
</div>
<div
class="space-y-4 p-5"
>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground">
Name
</label>
<Input
type="text"
placeholder="Workspace name"
value={workspaceName()}
onInput={(event) => setWorkspaceName((event.currentTarget as HTMLInputElement).value)}
required
disabled={isCreatingWorkspace()}
/>
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium text-foreground" for="workspace-description">
Description
</label>
<textarea
id="workspace-description"
rows={3}
class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Optional description"
value={workspaceDescription()}
onInput={(event) => setWorkspaceDescription((event.currentTarget as HTMLTextAreaElement).value)}
disabled={isCreatingWorkspace()}
/>
</div>
<div class="flex items-center justify-between rounded-md border border-border bg-background px-3 py-2">
<div>
<p class="text-sm font-medium text-foreground">Public workspace</p>
<p class="text-xs text-muted-foreground">Allow all members to discover this workspace.</p>
</div>
<Switch
checked={workspaceIsPublic()}
onCheckedChange={setWorkspaceIsPublic}
disabled={isCreatingWorkspace()}
/>
</div>
<Show when={createWorkspaceError()}>
<p class="text-sm text-destructive">{createWorkspaceError()}</p>
</Show>
<div class="flex justify-end gap-2 pt-2">
<Button
variant="outline"
onClick={closeCreateWorkspaceModal}
disabled={isCreatingWorkspace()}
>
Cancel
</Button>
<Button onClick={() => void handleCreateWorkspace()} disabled={isCreatingWorkspace()}>
{isCreatingWorkspace() ? 'Creating...' : 'Create Workspace'}
</Button>
</div>
</div>
</div>
</div>
</>
</ModalPortal>
</Show>
</>
)
}
+115 -126
View File
@@ -11,6 +11,9 @@ import {
IconClock,
IconExternalLink
} from '@tabler/icons-solidjs';
import { getApiV1BaseUrl } from '@/lib/api-url';
const API_BASE_URL = getApiV1BaseUrl();
interface ActivityItem {
id: string;
@@ -27,6 +30,7 @@ interface ActivityItem {
language?: string;
tags?: string[];
};
displayTimestamp?: string;
}
interface ActivityFeedProps {
@@ -40,6 +44,21 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
const [filter, setFilter] = createSignal<'all' | 'trackeep' | 'github'>('all');
const [loading, setLoading] = createSignal(true);
const normalizeActivityType = (type: string): ActivityItem['type'] => {
if (type === 'bookmark' || type === 'task' || type === 'note' || type === 'file') {
return type;
}
return 'task';
};
const formatTimestamp = (value: string): string => {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return parsed.toISOString().split('T')[0];
};
const getActivityIcon = (type: string) => {
switch (type) {
case 'bookmark': return IconBookmark;
@@ -57,79 +76,37 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
const fetchActivities = async () => {
try {
setLoading(true);
// Import mock data for demo mode
const { getMockActivities } = await import('@/lib/mockData');
// Combine and format activities
const combinedActivities: ActivityItem[] = [];
// Add Trackeep activities from mock data
const mockActivities = getMockActivities();
const now = new Date();
mockActivities.forEach((activity, index) => {
// Create realistic timestamps
const timestamp = new Date(now.getTime() - (index * 3600000)); // Each activity 1 hour apart
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/dashboard/stats`, {
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (!response.ok) {
throw new Error(`Failed to fetch activities: ${response.status}`);
}
const data = await response.json();
const recentActivities: Array<{ id?: number; type?: string; title?: string; timestamp?: string }> = Array.isArray(data.recentActivity)
? data.recentActivity
: [];
recentActivities.forEach((activity, index) => {
combinedActivities.push({
id: activity.id,
type: activity.type as any,
title: activity.title,
description: `${activity.action} ${activity.type}`,
timestamp: timestamp.toISOString(),
source: 'trackeep' as const,
metadata: {
tags: activity.details?.tags ? Object.keys(activity.details.tags) : undefined
}
id: String(activity.id ?? `activity-${index}`),
type: normalizeActivityType(activity.type || ''),
title: activity.title || 'Activity',
description: activity.type || 'trackeep',
timestamp: new Date().toISOString(),
displayTimestamp: activity.timestamp || '',
source: 'trackeep',
});
});
// Add some GitHub-style activities
const githubActivities = [
{
id: 'github_1',
type: 'github_commit' as const,
title: 'Fixed responsive design issues',
description: 'Resolved mobile layout problems on dashboard',
timestamp: new Date(now.getTime() - 2 * 3600000).toISOString(),
source: 'github' as const,
metadata: {
repo: 'tdvorak/trackeep',
url: 'https://github.com/tdvorak/trackeep/commit/abc123',
branch: 'main',
language: 'Go'
}
},
{
id: 'github_2',
type: 'github_pr' as const,
title: 'Add AI chat integration',
description: 'Implement LongCat AI provider with model switching',
timestamp: new Date(now.getTime() - 5 * 3600000).toISOString(),
source: 'github' as const,
metadata: {
repo: 'tdvorak/trackeep',
url: 'https://github.com/tdvorak/trackeep/pull/42',
branch: 'feature/ai-chat',
language: 'TypeScript'
}
},
{
id: 'github_3',
type: 'github_star' as const,
title: 'trackeep gained new stars',
description: 'Repository reached 245 stars',
timestamp: new Date(now.getTime() - 8 * 3600000).toISOString(),
source: 'github' as const,
metadata: {
repo: 'tdvorak/trackeep',
url: 'https://github.com/tdvorak/trackeep'
}
}
];
combinedActivities.push(...githubActivities);
// Sort by timestamp (most recent first)
combinedActivities.sort((a, b) =>
@@ -149,6 +126,7 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
setActivities(limitedActivities);
} catch (error) {
console.error('Failed to fetch activities:', error);
setActivities([]);
} finally {
setLoading(false);
}
@@ -179,7 +157,10 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
{props.showFilter && (
<div class="flex gap-2">
<button
onClick={() => setFilter('all')}
onClick={() => {
setFilter('all');
fetchActivities();
}}
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
filter() === 'all'
? 'bg-[#262626] text-[#fafafa]'
@@ -189,7 +170,10 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
All
</button>
<button
onClick={() => setFilter('trackeep')}
onClick={() => {
setFilter('trackeep');
fetchActivities();
}}
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
filter() === 'trackeep'
? 'bg-[#262626] text-[#fafafa]'
@@ -199,7 +183,10 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
Trackeep
</button>
<button
onClick={() => setFilter('github')}
onClick={() => {
setFilter('github');
fetchActivities();
}}
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
filter() === 'github'
? 'bg-[#262626] text-[#fafafa]'
@@ -220,68 +207,70 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
)}
{/* Activity List */}
<div class="space-y-3 flex-1 min-h-0 overflow-y-auto max-h-96 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
<For each={activities()}>
{(activity) => {
const Icon = getActivityIcon(activity.type);
return (
<div class="flex items-center justify-between p-3 bg-card rounded-lg border hover:bg-muted/50 transition-colors">
<div class="flex items-center gap-3">
<div class="bg-primary/10 p-2 rounded-lg">
<Icon class="size-4 text-primary" />
</div>
<div class="flex-1">
<p class="text-sm text-foreground font-medium">
{activity.title}
</p>
<div class="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<span>{new Date(activity.timestamp).toISOString().split('T')[0]}</span>
<span></span>
<span class="text-primary">
{activity.source === 'github'
? (activity.metadata?.repo?.split('/').pop() || 'GitHub')
: 'trackeep'}
</span>
<span></span>
<span>
{activity.source === 'github'
? activity.type === 'github_commit'
? 'pushed'
: activity.type === 'github_pr'
? 'opened PR'
: activity.type === 'github_star'
? 'starred'
: activity.type === 'github_fork'
? 'forked'
: 'activity'
: activity.description || activity.type}
</span>
{activities().length > 0 && (
<div class="space-y-3 flex-1 min-h-0 overflow-y-auto max-h-96 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
<For each={activities()}>
{(activity) => {
const Icon = getActivityIcon(activity.type);
return (
<div class="flex items-center justify-between p-3 bg-card rounded-lg border hover:bg-muted/50 transition-colors">
<div class="flex items-center gap-3">
<div class="bg-primary/10 p-2 rounded-lg">
<Icon class="size-4 text-primary" />
</div>
<div class="flex-1">
<p class="text-sm text-foreground font-medium">
{activity.title}
</p>
<div class="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<span>{activity.displayTimestamp || formatTimestamp(activity.timestamp)}</span>
<span></span>
<span class="text-primary">
{activity.source === 'github'
? (activity.metadata?.repo?.split('/').pop() || 'GitHub')
: 'trackeep'}
</span>
<span></span>
<span>
{activity.source === 'github'
? activity.type === 'github_commit'
? 'pushed'
: activity.type === 'github_pr'
? 'opened PR'
: activity.type === 'github_star'
? 'starred'
: activity.type === 'github_fork'
? 'forked'
: 'activity'
: activity.description || activity.type}
</span>
</div>
</div>
</div>
{activity.metadata?.url && (
<a
href={activity.metadata.url}
target="_blank"
rel="noopener noreferrer"
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 ml-2"
onClick={(e) => e.stopPropagation()}
>
<IconExternalLink class="size-4 text-primary" />
</a>
)}
</div>
{activity.metadata?.url && (
<a
href={activity.metadata.url}
target="_blank"
rel="noopener noreferrer"
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 ml-2"
onClick={(e) => e.stopPropagation()}
>
<IconExternalLink class="size-4 text-primary" />
</a>
)}
</div>
);
}}
</For>
</div>
);
}}
</For>
</div>
)}
{/* Empty State */}
{!loading() && activities().length === 0 && (
<div class="text-center py-8">
<IconClock class="size-12 text-[#a3a3a3] mx-auto mb-4" />
<p class="text-[#a3a3a3]">No recent activity found</p>
<p class="text-[#a3a3a3]">No activity yet</p>
<p class="text-sm text-[#a3a3a3] mt-1">
{filter() === 'github' ? 'Connect your GitHub account to see activity' : 'Start using Trackeep to see your activity here'}
</p>
+79 -76
View File
@@ -2,6 +2,7 @@ import { createSignal, createEffect } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { TagPicker } from '@/components/ui/TagPicker';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX } from '@tabler/icons-solidjs';
interface BookmarkModalProps {
@@ -52,92 +53,94 @@ export const BookmarkModal = (props: BookmarkModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-[60] mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-[60]" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-[70] ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: min(500px, 90vw); max-height: min(80vh, 600px); overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-4 sm:p-6 border-b border-border">
<h3 class="text-lg font-semibold">Add New Bookmark</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-[70] ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: min(500px, 90vw); max-height: min(80vh, 600px); overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-4 sm:p-6 border-b border-border">
<h3 class="text-lg font-semibold">Add New Bookmark</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-4 sm:p-6 space-y-4">
<div class="relative">
{/* Content */}
<div class="p-4 sm:p-6 space-y-4">
<div class="relative">
<Input
type="url"
placeholder="URL *"
value={newBookmark().url}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setNewBookmark(prev => ({ ...prev, url: target.value }));
}}
required
class="pr-12"
/>
{faviconPreview() && (
<div class="absolute right-3 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-muted rounded flex items-center justify-center overflow-hidden">
<img
src={faviconPreview()}
alt="Site favicon"
class="w-4 h-4 object-contain"
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
</div>
)}
</div>
<Input
type="url"
placeholder="URL *"
value={newBookmark().url}
type="text"
placeholder="Title (optional)"
value={newBookmark().title}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setNewBookmark(prev => ({ ...prev, url: target.value }));
if (target) setNewBookmark(prev => ({ ...prev, title: target.value }));
}}
required
class="pr-12"
/>
{faviconPreview() && (
<div class="absolute right-3 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-muted rounded flex items-center justify-center overflow-hidden">
<img
src={faviconPreview()}
alt="Site favicon"
class="w-4 h-4 object-contain"
onError={(e) => { e.currentTarget.style.display = 'none'; }}
/>
</div>
)}
</div>
<Input
type="text"
placeholder="Title (optional)"
value={newBookmark().title}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setNewBookmark(prev => ({ ...prev, title: target.value }));
}}
/>
<Input
type="text"
placeholder="Description (optional)"
value={newBookmark().description}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setNewBookmark(prev => ({ ...prev, description: target.value }));
}}
/>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground">Tags</label>
<TagPicker
availableTags={availableTags()}
selectedTags={tags()}
onTagsChange={(next) => setTags(next)}
placeholder="Add tags..."
allowNew={true}
<Input
type="text"
placeholder="Description (optional)"
value={newBookmark().description}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setNewBookmark(prev => ({ ...prev, description: target.value }));
}}
/>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground">Tags</label>
<TagPicker
availableTags={availableTags()}
selectedTags={tags()}
onTagsChange={(next) => setTags(next)}
placeholder="Add tags..."
allowNew={true}
/>
</div>
</div>
</div>
{/* Footer */}
<div class="flex flex-col sm:flex-row justify-end gap-2 p-4 sm:p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!newBookmark().url.trim()}>
Save Bookmark
</Button>
{/* Footer */}
<div class="flex flex-col sm:flex-row justify-end gap-2 p-4 sm:p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!newBookmark().url.trim()}>
Save Bookmark
</Button>
</div>
</div>
</div>
</>
</>
</ModalPortal>
);
};
@@ -386,10 +386,6 @@
inset: -5px;
}
.-translate-y-1\/2 {
transform: translateY(-50%);
}
/* Z-index utilities */
.z-50 {
z-index: 50;
+38 -35
View File
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/Button';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX, IconAlertTriangle } from '@tabler/icons-solidjs';
interface ConfirmModalProps {
@@ -45,45 +46,47 @@ export const ConfirmModal = (props: ConfirmModalProps) => {
};
return (
<>
{/* Backdrop */}
{isOpen && (
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 400px; max-width: 90vw;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<div class="flex items-center gap-3">
{getIcon()}
<h3 class="text-lg font-semibold">{title}</h3>
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 400px; max-width: 90vw;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<div class="flex items-center gap-3">
{getIcon()}
<h3 class="text-lg font-semibold">{title}</h3>
</div>
<button
onClick={onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
<button
onClick={onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-6">
<p class="text-muted-foreground">{message}</p>
</div>
{/* Content */}
<div class="p-6">
<p class="text-muted-foreground">{message}</p>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={onClose}>
{cancelText}
</Button>
<Button variant={getConfirmButtonVariant()} onClick={onConfirm}>
{confirmText}
</Button>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={onClose}>
{cancelText}
</Button>
<Button variant={getConfirmButtonVariant()} onClick={onConfirm}>
{confirmText}
</Button>
</div>
</div>
</div>
</>
</>
</ModalPortal>
);
};
@@ -2,6 +2,7 @@ import { createSignal, onMount } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { TagPicker } from '@/components/ui/TagPicker';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX } from '@tabler/icons-solidjs';
interface Bookmark {
@@ -71,79 +72,81 @@ export const EditBookmarkModal = (props: EditBookmarkModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 500px; max-width: 90vw;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">Edit Bookmark</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 500px; max-width: 90vw;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">Edit Bookmark</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="url"
placeholder="URL *"
value={editBookmark().url}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setEditBookmark(prev => ({ ...prev, url: target.value }));
}}
required
/>
<Input
type="text"
placeholder="Title"
value={editBookmark().title}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setEditBookmark(prev => ({ ...prev, title: target.value }));
}}
/>
<Input
type="text"
placeholder="Description"
value={editBookmark().description}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setEditBookmark(prev => ({ ...prev, description: target.value }));
}}
/>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground">Tags</label>
<TagPicker
availableTags={availableTags()}
selectedTags={tags()}
onTagsChange={(next) => setTags(next)}
placeholder="Add tags..."
allowNew={true}
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="url"
placeholder="URL *"
value={editBookmark().url}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setEditBookmark(prev => ({ ...prev, url: target.value }));
}}
required
/>
<Input
type="text"
placeholder="Title"
value={editBookmark().title}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setEditBookmark(prev => ({ ...prev, title: target.value }));
}}
/>
<Input
type="text"
placeholder="Description"
value={editBookmark().description}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setEditBookmark(prev => ({ ...prev, description: target.value }));
}}
/>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground">Tags</label>
<TagPicker
availableTags={availableTags()}
selectedTags={tags()}
onTagsChange={(next) => setTags(next)}
placeholder="Add tags..."
allowNew={true}
/>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!editBookmark().url.trim()}>
Save Changes
</Button>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!editBookmark().url.trim()}>
Save Changes
</Button>
</div>
</div>
</>
</>
</ModalPortal>
);
};
+30 -31
View File
@@ -1,6 +1,8 @@
import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX, IconDownload, IconExternalLink, IconEye, IconFile, IconCode, IconFileText, IconAlertTriangle, IconMusic, IconFileDescription, IconChartBar, IconChartLine } from '@tabler/icons-solidjs';
import { isDemoMode } from '@/lib/demo-mode';
interface FilePreviewModalProps {
isOpen: boolean;
@@ -168,12 +170,7 @@ export const FilePreviewModal = (props: FilePreviewModalProps) => {
};
const handleDownload = () => {
// Check if we're in demo mode
const isDemoMode = localStorage.getItem('demoMode') === 'true' ||
document.title.includes('Demo Mode') ||
window.location.search.includes('demo=true');
if (isDemoMode) {
if (isDemoMode()) {
// Simulate download in demo mode
alert(`Download simulated for: ${props.file.name}\n\nIn production, this would download the actual file.`);
return;
@@ -190,31 +187,32 @@ export const FilePreviewModal = (props: FilePreviewModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 900px; max-width: 95vw; max-height: 85vh;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<div class="flex items-center gap-3 flex-1 min-w-0">
<h3 class="text-lg font-semibold truncate">{props.file?.name}</h3>
<span class="text-sm text-muted-foreground flex-shrink-0">
{props.file?.size ? formatFileSize(props.file.size) : 'Unknown size'}
</span>
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 900px; max-width: 95vw; max-height: 85vh;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<div class="flex items-center gap-3 flex-1 min-w-0">
<h3 class="text-lg font-semibold truncate">{props.file?.name}</h3>
<span class="text-sm text-muted-foreground flex-shrink-0">
{props.file?.size ? formatFileSize(props.file.size) : 'Unknown size'}
</span>
</div>
<button
onClick={props.onClose}
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 flex-shrink-0"
>
<IconX class="size-4" />
</button>
</div>
<button
onClick={props.onClose}
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 flex-shrink-0"
>
<IconX class="size-4" />
</button>
</div>
{/* Preview Area */}
<div class="p-6" style="height: 500px;">
@@ -251,7 +249,8 @@ export const FilePreviewModal = (props: FilePreviewModalProps) => {
</Button>
</div>
</div>
</div>
</>
</div>
</>
</ModalPortal>
);
};
+24 -11
View File
@@ -1,5 +1,6 @@
import { createSignal, For, Show } from 'solid-js';
import { cn } from '@/lib/utils';
import { ModalPortal } from './ModalPortal';
import './FileUpload.css';
export interface FileUploadProps {
@@ -191,17 +192,26 @@ export const FileUpload = (props: FileUploadProps) => {
props.onClose?.();
};
if (!props.isOpen) {
return null;
}
return (
<div
class={cn(
"relative w-full rounded-20 bg-bg-white-0 focus:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 max-w-[440px] shadow-custom-md",
props.class
)}
role="dialog"
aria-labelledby="file-upload-title"
aria-describedby="file-upload-description"
data-state={props.isOpen ? 'open' : 'closed'}
>
<ModalPortal>
<>
<div class="fixed inset-0 z-[80] bg-black/50" onClick={handleClose} />
<div class="fixed top-1/2 left-1/2 z-[90] w-[min(440px,90vw)] max-h-[85vh] -translate-x-1/2 -translate-y-1/2 overflow-y-auto">
<div
class={cn(
"relative w-full rounded-20 bg-bg-white-0 focus:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 max-w-[440px] shadow-custom-md",
props.class
)}
role="dialog"
aria-labelledby="file-upload-title"
aria-describedby="file-upload-description"
data-state="open"
onClick={(event) => event.stopPropagation()}
>
{/* Header */}
<div class="relative flex items-start gap-3.5 py-4 pl-5 pr-14 before:absolute before:inset-x-0 before:bottom-0 before:border-b before:border-stroke-soft-200">
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-white-0 ring-1 ring-inset ring-stroke-soft-200">
@@ -366,6 +376,9 @@ export const FileUpload = (props: FileUploadProps) => {
</div>
</div>
</div>
</div>
</div>
</div>
</>
</ModalPortal>
);
};
+13 -10
View File
@@ -2,6 +2,7 @@ import { createSignal, For, Show, onMount, onCleanup } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Card } from '@/components/ui/Card';
import { ModalPortal } from '@/components/ui/ModalPortal';
import {
IconX,
IconUpload,
@@ -153,15 +154,16 @@ export const FileUploadModal = (props: FileUploadModalProps) => {
};
return (
<Show when={props.isOpen}>
<div
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 mt-0"
onClick={props.onClose}
>
<div
class="bg-card rounded-lg border border-border p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4 my-4"
onClick={(e) => e.stopPropagation()}
<ModalPortal>
<Show when={props.isOpen}>
<div
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={props.onClose}
>
<div
class="bg-card rounded-lg border border-border p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4 my-4"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold">Upload File</h2>
@@ -382,8 +384,9 @@ export const FileUploadModal = (props: FileUploadModalProps) => {
Upload
</Button>
</div>
</div>
</div>
</div>
</Show>
</Show>
</ModalPortal>
);
};
+123 -231
View File
@@ -51,141 +51,20 @@ export const GitHubActivity = (props: GitHubActivityProps) => {
longestStreak: 0
});
onMount(() => {
// Always show rich mock data for demonstration
generateMockData();
return;
// Original real data loading logic (commented out for demo)
/*
if (isDemoMode()) {
// In demo mode, always show rich mock data
generateMockData();
return;
}
loadRealData().catch((error) => {
console.error('Failed to load GitHub activity analytics, falling back to mock data:', error);
generateMockData();
});
*/
});
const generateMockData = () => {
const activityData: ActivityData[] = [];
const today = new Date();
const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
let currentStreak = 0;
let longestStreak = 0;
let tempStreak = 0;
let totalContributions = 0;
// Generate more realistic activity patterns
for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
const dayOfWeek = d.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const monthsAgo = Math.floor((today.getTime() - d.getTime()) / (30 * 24 * 60 * 60 * 1000));
// More realistic patterns:
// - Higher activity in recent months
// - Lower activity on weekends
// - Some bursts of activity followed by quiet periods
let baseProbability = 0.3; // 30% chance of some activity
// Increase activity for more recent months
if (monthsAgo < 3) baseProbability = 0.7; // Last 3 months: 70% chance
else if (monthsAgo < 6) baseProbability = 0.5; // 3-6 months ago: 50% chance
else baseProbability = 0.3; // 6+ months ago: 30% chance
// Reduce activity on weekends
if (isWeekend) baseProbability *= 0.6;
// Add some randomness and bursts
const hasActivity = Math.random() < baseProbability;
let count = 0;
if (hasActivity) {
// Generate contribution count with some bursts
if (Math.random() < 0.1) {
// 10% chance of high activity burst
count = Math.floor(Math.random() * 15) + 10;
} else {
// Normal activity
count = Math.floor(Math.random() * 8) + 1;
}
}
const level = count === 0 ? 0 : Math.min(5, Math.ceil(count / 2));
activityData.push({
date: new Date(d).toISOString().split('T')[0],
count,
level
});
if (count > 0) {
tempStreak++;
if (d.toDateString() === today.toDateString()) {
currentStreak = tempStreak;
}
} else {
longestStreak = Math.max(longestStreak, tempStreak);
tempStreak = 0;
}
totalContributions += count;
}
const defaultEvents: ActivityEvent[] = [
{
type: 'commit',
title: 'feat: Add advanced color scheme management',
date: '2024-01-28',
link: '/app/activity',
repo: 'trackeep',
action: 'pushed'
},
{
type: 'pull_request',
title: 'Enhance admin settings with toggle buttons',
date: '2024-01-27',
link: '/app/admin',
repo: 'trackeep',
action: 'opened'
},
{
type: 'merge',
title: 'Merge branch: feature/ai-chat-enhancements',
date: '2024-01-26',
link: '/app/chat',
repo: 'trackeep',
action: 'merged'
},
{
type: 'bookmark',
title: 'Added bookmark: Advanced React Patterns',
date: '2024-01-25',
link: '/app/bookmarks'
},
{
type: 'project',
title: 'Updated project: Trackeep Dashboard',
date: '2024-01-24',
link: '/app/projects'
}
];
setActivities(activityData);
setRecentEvents(props.customEvents || defaultEvents);
const setEmptyData = () => {
setActivities([]);
setRecentEvents(props.customEvents || []);
setStats({
totalContributions,
currentStreak,
longestStreak: Math.max(longestStreak, tempStreak)
totalContributions: 0,
currentStreak: 0,
longestStreak: 0
});
};
onMount(() => {
setEmptyData();
});
const getMonthLabels = () => {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const today = new Date();
@@ -325,75 +204,84 @@ export const GitHubActivity = (props: GitHubActivityProps) => {
</h3>
</div>
{/* Month labels - Show all months with responsive spacing */}
<div class="flex justify-between mb-3 px-6 sm:px-8 text-xs sm:text-sm font-medium overflow-x-auto">
<div class="flex gap-2 sm:gap-3 min-w-max">
{getMonthLabels().map((month) => (
<span class="text-foreground/80 hover:text-foreground transition-colors cursor-default whitespace-nowrap">
{month}
</span>
))}
</div>
</div>
{/* Contribution grid - Responsive and prevents overflow */}
<div class="overflow-hidden w-full">
<div class="flex gap-1 min-w-0">
{/* Day labels */}
<div class="flex flex-col gap-1 pr-2 flex-shrink-0">
{['Mon', 'Wed', 'Fri'].map((day) => (
<div class="h-3 flex items-center justify-end">
<span class="text-xs text-foreground/70 hover:text-foreground transition-colors cursor-default font-medium">
{day}
</span>
</div>
<Show
when={activities().length > 0}
fallback={
<div class="h-44 border border-dashed border-border rounded-lg flex items-center justify-center">
<p class="text-sm text-muted-foreground">No GitHub contribution data yet.</p>
</div>
}
>
{/* Month labels - Show all months with responsive spacing */}
<div class="flex justify-between mb-3 px-6 sm:px-8 text-xs sm:text-sm font-medium overflow-x-auto">
<div class="flex gap-2 sm:gap-3 min-w-max">
{getMonthLabels().map((month) => (
<span class="text-foreground/80 hover:text-foreground transition-colors cursor-default whitespace-nowrap">
{month}
</span>
))}
</div>
</div>
{/* Weekly columns - Responsive with proper overflow handling */}
<div class="flex gap-1 overflow-x-auto overflow-y-hidden min-w-0 pb-2">
{Array.from({ length: 53 }, (_, weekIndex) => (
<div class="flex flex-col gap-1 flex-shrink-0">
{Array.from({ length: 7 }, (_, dayIndex) => {
const activityIndex = weekIndex * 7 + dayIndex;
const activity = activities()[activityIndex];
{/* Contribution grid - Responsive and prevents overflow */}
<div class="overflow-hidden w-full">
<div class="flex gap-1 min-w-0">
{/* Day labels */}
<div class="flex flex-col gap-1 pr-2 flex-shrink-0">
{['Mon', 'Wed', 'Fri'].map((day) => (
<div class="h-3 flex items-center justify-end">
<span class="text-xs text-foreground/70 hover:text-foreground transition-colors cursor-default font-medium">
{day}
</span>
</div>
))}
</div>
{/* Weekly columns - Responsive with proper overflow handling */}
<div class="flex gap-1 overflow-x-auto overflow-y-hidden min-w-0 pb-2">
{Array.from({ length: 53 }, (_, weekIndex) => (
<div class="flex flex-col gap-1 flex-shrink-0">
{Array.from({ length: 7 }, (_, dayIndex) => {
const activityIndex = weekIndex * 7 + dayIndex;
const activity = activities()[activityIndex];
if (!activity) {
return (
<div
class="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-sm flex-shrink-0 transition-all"
style={`background-color: ${getActivityColor(0)}`}
></div>
);
}
if (!activity) {
return (
<div
class="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-sm flex-shrink-0 transition-all"
style={`background-color: ${getActivityColor(0)}`}
class="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-sm hover:ring-1 hover:ring-primary cursor-pointer transition-all flex-shrink-0 hover:scale-110"
style={`background-color: ${getActivityColor(activity.level)}`}
title={`${activity.date}: ${activity.count} contributions`}
></div>
);
}
return (
<div
class="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-sm hover:ring-1 hover:ring-primary cursor-pointer transition-all flex-shrink-0 hover:scale-110"
style={`background-color: ${getActivityColor(activity.level)}`}
title={`${activity.date}: ${activity.count} contributions`}
></div>
);
})}
</div>
))}
})}
</div>
))}
</div>
</div>
</div>
</div>
{/* Legend */}
<div class="flex items-center justify-between mt-4">
<span class="text-xs text-muted-foreground">Less</span>
<div class="flex gap-1">
{[0, 1, 2, 3, 4].map((level) => (
<div
class="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-sm"
style={`background-color: ${getActivityColor(level)}`}
></div>
))}
{/* Legend */}
<div class="flex items-center justify-between mt-4">
<span class="text-xs text-muted-foreground">Less</span>
<div class="flex gap-1">
{[0, 1, 2, 3, 4].map((level) => (
<div
class="w-2.5 h-2.5 sm:w-3 sm:h-3 rounded-sm"
style={`background-color: ${getActivityColor(level)}`}
></div>
))}
</div>
<span class="text-xs text-muted-foreground">More</span>
</div>
<span class="text-xs text-muted-foreground">More</span>
</div>
</Show>
</Card>
</Show>
@@ -407,52 +295,56 @@ export const GitHubActivity = (props: GitHubActivityProps) => {
<span>Active</span>
</div>
</div>
<div class="space-y-3 max-h-64 overflow-y-auto">
<For each={recentEvents()}>
{(event) => (
<div class="flex items-center justify-between p-3 bg-card rounded-lg border hover:bg-muted/50 transition-colors">
<div class="flex items-center gap-3">
<div class="bg-primary/10 p-2 rounded-lg">
{getEventIcon(event.type)}
</div>
<div class="flex-1">
<p class="text-sm text-foreground font-medium">{event.title}</p>
<div class="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<span>{event.date}</span>
{event.repo && (
<>
<span></span>
<span class="text-primary">{event.repo}</span>
</>
)}
{event.action && (
<>
<span></span>
<span>{event.action}</span>
</>
)}
<Show
when={recentEvents().length > 0}
fallback={<p class="text-sm text-muted-foreground">No GitHub events yet.</p>}
>
<div class="space-y-3 max-h-64 overflow-y-auto">
<For each={recentEvents()}>
{(event) => (
<div class="flex items-center justify-between p-3 bg-card rounded-lg border hover:bg-muted/50 transition-colors">
<div class="flex items-center gap-3">
<div class="bg-primary/10 p-2 rounded-lg">
{getEventIcon(event.type)}
</div>
<div class="flex-1">
<p class="text-sm text-foreground font-medium">{event.title}</p>
<div class="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<span>{event.date}</span>
{event.repo && (
<>
<span></span>
<span class="text-primary">{event.repo}</span>
</>
)}
{event.action && (
<>
<span></span>
<span>{event.action}</span>
</>
)}
</div>
</div>
</div>
{event.link && (
<Button
variant="ghost"
size="sm"
onClick={() => {
if (event.link) {
window.location.href = event.link;
}
}}
class="hover:bg-primary/10 transition-colors"
>
<IconExternalLink class="size-4" />
</Button>
)}
</div>
{event.link && (
<Button
variant="ghost"
size="sm"
onClick={() => {
// Navigate to the link in the same tab
if (event.link) {
window.location.href = event.link;
}
}}
class="hover:bg-primary/10 transition-colors"
>
<IconExternalLink class="size-4" />
</Button>
)}
</div>
)}
</For>
</div>
)}
</For>
</div>
</Show>
</Card>
</Show>
</div>
@@ -1,6 +1,7 @@
import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX } from '@tabler/icons-solidjs';
interface LearningPathFormData {
@@ -100,8 +101,9 @@ export const LearningPathModal = (props: LearningPathModalProps) => {
if (!props.isOpen) return null;
return (
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 mt-0">
<div class="bg-[#1a1a1a] rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4 my-4">
<ModalPortal>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-[#1a1a1a] rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4 my-4">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-[#404040]">
<h2 class="text-xl font-semibold text-[#fafafa]">
@@ -264,7 +266,8 @@ export const LearningPathModal = (props: LearningPathModalProps) => {
</Button>
</div>
</form>
</div>
</div>
</div>
</ModalPortal>
);
};
@@ -1,6 +1,7 @@
import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX, IconClock, IconUsers, IconStar, IconBook, IconVideo, IconFileText, IconCode, IconCheck } from '@tabler/icons-solidjs';
interface LearningPath {
@@ -82,8 +83,9 @@ export const LearningPathPreviewModal = (props: LearningPathPreviewModalProps) =
if (!props.isOpen || !props.learningPath) return null;
return (
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 mt-0">
<div class="bg-[#1a1a1a] rounded-lg w-full max-w-4xl max-h-[90vh] overflow-y-auto mx-4 my-4">
<ModalPortal>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-[#1a1a1a] rounded-lg w-full max-w-4xl max-h-[90vh] overflow-y-auto mx-4 my-4">
{/* Header */}
<div class="relative">
{/* Thumbnail */}
@@ -241,7 +243,8 @@ export const LearningPathPreviewModal = (props: LearningPathPreviewModalProps) =
</div>
)}
</div>
</div>
</div>
</div>
</ModalPortal>
);
};
+68 -65
View File
@@ -1,6 +1,7 @@
import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX } from '@tabler/icons-solidjs';
interface Member {
@@ -55,74 +56,76 @@ export const MemberModal = (props: MemberModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 500px; max-width: 90vw;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">
{props.isEdit ? 'Edit Member' : 'Add New Member'}
</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="text"
placeholder="Member name *"
value={memberData().name}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setMemberData(prev => ({ ...prev, name: target.value }));
}}
required
/>
<Input
type="email"
placeholder="Email address *"
value={memberData().email}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setMemberData(prev => ({ ...prev, email: target.value }));
}}
required
/>
<div class="space-y-2">
<label class="text-sm font-medium text-foreground">Role</label>
<select
value={memberData().role}
onChange={(e) => setMemberData(prev => ({ ...prev, role: e.target.value as 'Admin' | 'Member' }))}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring"
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 500px; max-width: 90vw;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">
{props.isEdit ? 'Edit Member' : 'Add New Member'}
</h3>
<button
onClick={props.onClose}
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"
>
<option value="Member">Member</option>
<option value="Admin">Admin</option>
</select>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="text"
placeholder="Member name *"
value={memberData().name}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setMemberData(prev => ({ ...prev, name: target.value }));
}}
required
/>
<Input
type="email"
placeholder="Email address *"
value={memberData().email}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setMemberData(prev => ({ ...prev, email: target.value }));
}}
required
/>
<div class="space-y-2">
<label class="text-sm font-medium text-foreground">Role</label>
<select
value={memberData().role}
onChange={(e) => setMemberData(prev => ({ ...prev, role: e.target.value as 'Admin' | 'Member' }))}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring"
>
<option value="Member">Member</option>
<option value="Admin">Admin</option>
</select>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!memberData().name.trim() || !memberData().email.trim()}>
{props.isEdit ? 'Save Changes' : 'Add Member'}
</Button>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!memberData().name.trim() || !memberData().email.trim()}>
{props.isEdit ? 'Save Changes' : 'Add Member'}
</Button>
</div>
</div>
</>
</>
</ModalPortal>
);
};
@@ -0,0 +1,10 @@
import type { JSX } from 'solid-js'
import { Portal } from 'solid-js/web'
interface ModalPortalProps {
children: JSX.Element
}
export const ModalPortal = (props: ModalPortalProps) => {
return <Portal mount={document.body}>{props.children}</Portal>
}
+67 -64
View File
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { RichTextEditor } from '@/components/ui/RichTextEditor';
import { TagPicker } from '@/components/ui/TagPicker';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX, IconTag } from '@tabler/icons-solidjs';
interface NoteModalProps {
@@ -34,74 +35,76 @@ export const NoteModal = (props: NoteModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 600px; max-width: 90vw; max-height: 80vh; overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">
{props.note ? 'Edit Note' : 'Add New Note'}
</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="text"
placeholder="Note title"
value={noteData().title}
onInput={(e: any) => setNoteData(prev => ({ ...prev, title: e.target.value }))}
required
/>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<IconTag class="size-4" />
Content
</label>
<RichTextEditor
value={noteData().content}
onChange={(content) => setNoteData(prev => ({ ...prev, content }))}
placeholder="Write your note here..."
mode="markdown"
/>
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 600px; max-width: 90vw; max-height: 80vh; overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">
{props.note ? 'Edit Note' : 'Add New Note'}
</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground">Tags</label>
<TagPicker
availableTags={availableTags()}
selectedTags={noteData().tags}
onTagsChange={(tags) => setNoteData(prev => ({ ...prev, tags }))}
placeholder="Add tags..."
allowNew={true}
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="text"
placeholder="Note title"
value={noteData().title}
onInput={(e: any) => setNoteData(prev => ({ ...prev, title: e.target.value }))}
required
/>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<IconTag class="size-4" />
Content
</label>
<RichTextEditor
value={noteData().content}
onChange={(content) => setNoteData(prev => ({ ...prev, content }))}
placeholder="Write your note here..."
mode="markdown"
/>
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-muted-foreground">Tags</label>
<TagPicker
availableTags={availableTags()}
selectedTags={noteData().tags}
onTagsChange={(tags) => setNoteData(prev => ({ ...prev, tags }))}
placeholder="Add tags..."
allowNew={true}
/>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!noteData().title.trim()}>
{props.note ? 'Update Note' : 'Save Note'}
</Button>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!noteData().title.trim()}>
{props.note ? 'Update Note' : 'Save Note'}
</Button>
</div>
</div>
</>
</>
</ModalPortal>
);
};
@@ -11,12 +11,13 @@ interface SearchTagFilterBarProps {
selectedTag: string;
onTagChange: (value: string) => void;
onReset: () => void;
allOptionLabel?: string;
}
export const SearchTagFilterBar = (props: SearchTagFilterBarProps) => {
return (
<div class="flex flex-col sm:flex-row gap-4 mb-6">
<div class="flex flex-1 gap-2">
<div class="mb-6 space-y-3">
<div class="grid grid-cols-1 sm:grid-cols-[17fr_3fr] gap-3 items-stretch sm:items-center">
<Input
type="text"
placeholder={props.searchPlaceholder}
@@ -25,14 +26,14 @@ export const SearchTagFilterBar = (props: SearchTagFilterBarProps) => {
const target = e.currentTarget as HTMLInputElement;
if (target) props.onSearchChange(target.value);
}}
class="flex-1"
class="w-full min-w-0"
/>
<select
value={props.selectedTag}
onChange={(e) => props.onTagChange(e.target.value)}
class="flex h-10 w-full sm:w-48 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring"
class="flex h-10 w-full min-w-0 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring"
>
<option value="">All Tags</option>
<option value="">{props.allOptionLabel || 'All Tags'}</option>
<For each={props.tagOptions}>
{(tag) => <option value={tag}>{tag}</option>}
</For>
+72 -69
View File
@@ -2,6 +2,7 @@ import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { DatePicker } from '@/components/ui/DatePicker';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX } from '@tabler/icons-solidjs';
interface Task {
@@ -78,79 +79,81 @@ export const TaskModal = (props: TaskModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-[60]" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 500px; max-width: 90vw;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">
{props.isEdit ? 'Edit Task' : 'Add New Task'}
</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="text"
placeholder="Task title *"
value={taskData().title}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setTaskData(prev => ({ ...prev, title: target.value }));
}}
required
/>
<Input
type="text"
placeholder="Description (optional)"
value={taskData().description}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setTaskData(prev => ({ ...prev, description: target.value }));
}}
/>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<select
value={taskData().priority}
onChange={(e) => setTaskData(prev => ({ ...prev, priority: e.target.value as any }))}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring"
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-[70] w-full max-w-lg mx-4 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`}>
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">
{props.isEdit ? 'Edit Task' : 'Add New Task'}
</h3>
<button
onClick={props.onClose}
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"
>
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<DatePicker
value={dueDate()}
onChange={(date) => setDueDate(date || undefined)}
placeholder="Due date (optional)"
class="w-full"
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-6 space-y-4">
<Input
type="text"
placeholder="Task title *"
value={taskData().title}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setTaskData(prev => ({ ...prev, title: target.value }));
}}
required
/>
<Input
type="text"
placeholder="Description (optional)"
value={taskData().description}
onInput={(e) => {
const target = e.currentTarget as HTMLInputElement;
if (target) setTaskData(prev => ({ ...prev, description: target.value }));
}}
/>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<select
value={taskData().priority}
onChange={(e) => setTaskData(prev => ({ ...prev, priority: e.target.value as any }))}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring"
>
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<DatePicker
value={dueDate()}
onChange={(date) => setDueDate(date || undefined)}
placeholder="Due date (optional)"
class="w-full"
/>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!taskData().title.trim()}>
{props.isEdit ? 'Save Changes' : 'Add Task'}
</Button>
</div>
</div>
{/* Footer */}
<div class="flex justify-end gap-2 p-6 border-t border-border">
<Button variant="outline" onClick={props.onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!taskData().title.trim()}>
{props.isEdit ? 'Save Changes' : 'Add Task'}
</Button>
</div>
</div>
</>
</>
</ModalPortal>
);
};
+7 -4
View File
@@ -7,6 +7,7 @@ import {
IconLoader2
} from '@tabler/icons-solidjs';
import { updateStore } from '../../stores/updateStore';
import { ModalPortal } from './ModalPortal';
interface UpdateCheckerProps {
class?: string;
@@ -115,9 +116,10 @@ export function UpdateChecker(props: UpdateCheckerProps) {
{/* Update Modal */}
<Show when={showUpdateModal() && updateStore.updateInfo()}>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 mt-0">
<div class="bg-card border border-border rounded-lg shadow-lg max-w-md w-full max-h-[80vh] overflow-auto">
<div class="p-6">
<ModalPortal>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-card border border-border rounded-lg shadow-lg max-w-md w-full max-h-[80vh] overflow-auto">
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<IconDownload class="size-6 text-blue-500" />
<h2 class="text-lg font-semibold">Update Available</h2>
@@ -243,9 +245,10 @@ export function UpdateChecker(props: UpdateCheckerProps) {
</button>
</Show>
</div>
</div>
</div>
</div>
</div>
</ModalPortal>
</Show>
</>
);
+30 -28
View File
@@ -1,6 +1,11 @@
import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX, IconUpload } from '@tabler/icons-solidjs';
import { isDemoMode } from '@/lib/demo-mode';
import { getApiV1BaseUrl } from '@/lib/api-url';
const API_BASE_URL = getApiV1BaseUrl();
interface UploadModalProps {
isOpen: boolean;
@@ -39,13 +44,8 @@ export const UploadModal = (props: UploadModalProps) => {
const files = uploadedFiles();
if (files.length === 0) return;
// Check if we're in demo mode
const isDemoMode = localStorage.getItem('demoMode') === 'true' ||
document.title.includes('Demo Mode') ||
window.location.search.includes('demo=true');
try {
if (isDemoMode) {
if (isDemoMode()) {
// Simulate upload in demo mode
console.log('Demo mode: Simulating upload for files:', files.map(f => f.name));
// Simulate upload delay
@@ -62,7 +62,7 @@ export const UploadModal = (props: UploadModalProps) => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/api/v1/files/upload`, {
const response = await fetch(`${API_BASE_URL}/files/upload`, {
method: 'POST',
body: formData
});
@@ -86,26 +86,27 @@ export const UploadModal = (props: UploadModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-[60] mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-[60]" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-[70] ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: min(600px, 90vw); max-height: min(80vh, 600px); overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">Import Documents</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-[70] ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: min(600px, 90vw); max-height: min(80vh, 600px); overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold">Import Documents</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-4 sm:p-6 space-y-4">
@@ -175,7 +176,8 @@ export const UploadModal = (props: UploadModalProps) => {
Upload {uploadedFiles().length} {uploadedFiles().length === 1 ? 'File' : 'Files'}
</Button>
</div>
</div>
</>
</div>
</>
</ModalPortal>
);
};
@@ -1,4 +1,4 @@
import { createSignal } from 'solid-js';
import { createEffect, createMemo, createSignal } from 'solid-js';
import {
IconUser,
IconSettings,
@@ -7,21 +7,69 @@ import {
IconChevronDown
} from '@tabler/icons-solidjs';
import { DropdownMenu, DropdownMenuItem } from './DropdownMenu';
import { useAuth } from '@/lib/auth';
import { getApiV1BaseUrl } from '@/lib/api-url';
interface UserProfile {
name: string;
email: string;
avatar?: string;
role: string;
joinDate: string;
interface DashboardStats {
totalBookmarks: number;
totalTasks: number;
}
const API_BASE_URL = getApiV1BaseUrl();
export const UserProfileDropdown = () => {
const [userProfile] = createSignal<UserProfile>({
name: 'Admin User',
email: '[email protected]',
role: 'Administrator',
joinDate: '2024-01-01'
const { logout, authState } = useAuth();
const [dashboardStats, setDashboardStats] = createSignal<DashboardStats>({
totalBookmarks: 0,
totalTasks: 0,
});
const displayName = createMemo(() => {
const user = authState.user;
if (!user) return 'User';
return user.full_name?.trim() || user.username?.trim() || user.email?.split('@')[0] || 'User';
});
const displayEmail = createMemo(() => authState.user?.email || '[email protected]');
const loadDashboardStats = async () => {
if (!authState.isAuthenticated) {
setDashboardStats({ totalBookmarks: 0, totalTasks: 0 });
return;
}
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
if (!token) {
setDashboardStats({ totalBookmarks: 0, totalTasks: 0 });
return;
}
try {
const response = await fetch(`${API_BASE_URL}/dashboard/stats`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
throw new Error(`Failed to load dashboard stats: ${response.status}`);
}
const data = await response.json();
setDashboardStats({
totalBookmarks: Number(data?.totalBookmarks || 0),
totalTasks: Number(data?.totalTasks || 0),
});
} catch (error) {
console.error('Failed to load user stats:', error);
setDashboardStats({ totalBookmarks: 0, totalTasks: 0 });
}
};
createEffect(() => {
if (authState.isAuthenticated) {
void loadDashboardStats();
return;
}
setDashboardStats({ totalBookmarks: 0, totalTasks: 0 });
});
const handleProfileClick = () => {
@@ -36,20 +84,22 @@ export const UserProfileDropdown = () => {
window.location.href = '/app/stats';
};
const handleLogout = () => {
const handleLogout = async () => {
if (confirm('Are you sure you want to logout?')) {
// In real app, this would clear auth tokens and redirect to login
await logout();
window.location.href = '/login';
}
};
const getInitials = (name: string) => {
return name
const initials = name
.split(' ')
.filter(Boolean)
.map(part => part.charAt(0))
.join('')
.toUpperCase()
.slice(0, 2);
return initials || 'U';
};
return (
@@ -57,7 +107,7 @@ export const UserProfileDropdown = () => {
trigger={
<button type="button" class="items-center justify-center rounded-md font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-9 px-3 py-1 text-base flex gap-2">
<div class="w-5 h-5 bg-primary text-primary-foreground rounded-full flex items-center justify-center text-xs font-medium">
{getInitials(userProfile().name)}
{getInitials(displayName())}
</div>
<IconChevronDown class="size-3 opacity-50" />
</button>
@@ -67,11 +117,11 @@ export const UserProfileDropdown = () => {
<div class="px-3 py-2 border-b border-border">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-primary text-primary-foreground rounded-full flex items-center justify-center text-sm font-medium">
{getInitials(userProfile().name)}
{getInitials(displayName())}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">{userProfile().name}</p>
<p class="text-xs text-muted-foreground truncate">{userProfile().email}</p>
<p class="text-sm font-medium truncate">{displayName()}</p>
<p class="text-xs text-muted-foreground truncate">{displayEmail()}</p>
</div>
</div>
</div>
@@ -80,11 +130,11 @@ export const UserProfileDropdown = () => {
<div class="px-3 py-2 border-b border-border">
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="text-center">
<p class="font-medium text-primary">156</p>
<p class="font-medium text-primary">{dashboardStats().totalBookmarks}</p>
<p class="text-muted-foreground">Bookmarks</p>
</div>
<div class="text-center">
<p class="font-medium text-primary">42</p>
<p class="font-medium text-primary">{dashboardStats().totalTasks}</p>
<p class="text-muted-foreground">Tasks</p>
</div>
</div>
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/Button';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX, IconExternalLink } from '@tabler/icons-solidjs';
interface VideoPreviewModalProps {
@@ -13,62 +14,64 @@ export const VideoPreviewModal = (props: VideoPreviewModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 900px; max-width: 90vw; max-height: 80vh;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold truncate pr-4">{props.video?.title}</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Video Player */}
<div class="p-6">
<div class="aspect-video bg-black rounded-lg overflow-hidden">
{props.video && (
<iframe
src={getEmbedUrl(props.video.video_id)}
title={props.video.title}
class="w-full h-full"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
)}
</div>
</div>
{/* Video Info */}
<div class="px-6 pb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h4 class="text-lg font-medium mb-1">{props.video?.title}</h4>
<p class="text-muted-foreground text-sm">
Channel: {props.video?.channel_name}
</p>
</div>
<Button
onClick={() => window.open(props.video?.url, '_blank')}
class="flex items-center gap-2"
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-50 ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: 900px; max-width: 90vw; max-height: 80vh;">
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<h3 class="text-lg font-semibold truncate pr-4">{props.video?.title}</h3>
<button
onClick={props.onClose}
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"
>
<IconExternalLink class="size-4" />
Open on YouTube
</Button>
<IconX class="size-4" />
</button>
</div>
{/* Video Player */}
<div class="p-6">
<div class="aspect-video bg-black rounded-lg overflow-hidden">
{props.video && (
<iframe
src={getEmbedUrl(props.video.video_id)}
title={props.video.title}
class="w-full h-full"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
)}
</div>
</div>
{/* Video Info */}
<div class="px-6 pb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h4 class="text-lg font-medium mb-1">{props.video?.title}</h4>
<p class="text-muted-foreground text-sm">
Channel: {props.video?.channel_name}
</p>
</div>
<Button
onClick={() => window.open(props.video?.url, '_blank')}
class="flex items-center gap-2"
>
<IconExternalLink class="size-4" />
Open on YouTube
</Button>
</div>
</div>
</div>
</div>
</>
</>
</ModalPortal>
);
};
+24 -21
View File
@@ -1,6 +1,7 @@
import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { IconX } from '@tabler/icons-solidjs';
interface VideoUploadModalProps {
@@ -46,26 +47,27 @@ export const VideoUploadModal = (props: VideoUploadModalProps) => {
};
return (
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-[60] mt-0" onClick={props.onClose} />
)}
<ModalPortal>
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-[60]" onClick={props.onClose} />
)}
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-[70] ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: min(500px, 90vw); max-height: min(80vh, 600px); overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-4 sm:p-6 border-b border-border">
<h3 class="text-lg font-semibold">Add YouTube Video</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Modal */}
<div class={`fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-xl transition-all duration-300 z-[70] ${
props.isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'
}`} style="width: min(500px, 90vw); max-height: min(80vh, 600px); overflow-y: auto;">
{/* Header */}
<div class="flex items-center justify-between p-4 sm:p-6 border-b border-border">
<h3 class="text-lg font-semibold">Add YouTube Video</h3>
<button
onClick={props.onClose}
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"
>
<IconX class="size-4" />
</button>
</div>
{/* Content */}
<div class="p-4 sm:p-6 space-y-4">
@@ -119,7 +121,8 @@ export const VideoUploadModal = (props: VideoUploadModalProps) => {
Add Video
</Button>
</div>
</div>
</>
</div>
</>
</ModalPortal>
);
};
+24 -21
View File
@@ -1,4 +1,5 @@
import { Button } from '@/components/ui/Button';
import { ModalPortal } from '@/components/ui/ModalPortal';
import { For, Show, createEffect } from 'solid-js';
import { IconX, IconEdit, IconPin, IconTrash, IconCopy, IconDownload, IconPaperclip } from '@tabler/icons-solidjs';
@@ -65,23 +66,24 @@ export const ViewNoteModal = (props: ViewNoteModalProps) => {
});
return (
<>
{/* Backdrop */}
<Show when={props.isOpen && props.note}>
<div
class="fixed inset-0 bg-black/60 z-50"
onClick={props.onClose}
/>
</Show>
<ModalPortal>
<>
{/* Backdrop */}
<Show when={props.isOpen && props.note}>
<div
class="fixed inset-0 bg-black/60 z-50"
onClick={props.onClose}
/>
</Show>
{/* Modal */}
<Show when={props.isOpen && props.note}>
<div
class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-2xl transition-all duration-300 z-50"
style="width: 800px; max-width: 90vw; max-height: 85vh; overflow-y: auto;"
>
{props.note && (
<>
{/* Modal */}
<Show when={props.isOpen && props.note}>
<div
class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg shadow-2xl transition-all duration-300 z-50"
style="width: 800px; max-width: 90vw; max-height: 85vh; overflow-y: auto;"
>
{props.note && (
<>
{/* Header */}
<div class="flex items-center justify-between p-6 border-b border-border">
<div class="flex items-center gap-3">
@@ -239,10 +241,11 @@ export const ViewNoteModal = (props: ViewNoteModalProps) => {
</div>
</div>
</div>
</>
)}
</div>
</Show>
</>
</>
)}
</div>
</Show>
</>
</ModalPortal>
);
};