creates application

This commit is contained in:
simon.franken
2026-02-16 10:15:27 +01:00
parent 791c661395
commit 7d678c1c4d
65 changed files with 10389 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { authApi } from '@/api/auth';
import type { User } from '@/types';
interface AuthContextType {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: () => void;
logout: () => Promise<void>;
refetchUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
const { data: user, isLoading } = useQuery({
queryKey: ['currentUser'],
queryFn: authApi.getCurrentUser,
staleTime: 5 * 60 * 1000, // 5 minutes
});
const login = useCallback(() => {
authApi.login();
}, []);
const logout = useCallback(async () => {
await authApi.logout();
queryClient.setQueryData(['currentUser'], null);
queryClient.clear();
}, [queryClient]);
const refetchUser = useCallback(async () => {
await queryClient.invalidateQueries({ queryKey: ['currentUser'] });
}, [queryClient]);
return (
<AuthContext.Provider
value={{
user: user ?? null,
isLoading,
isAuthenticated: !!user,
login,
logout,
refetchUser,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,136 @@
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { timerApi } from '@/api/timer';
import type { OngoingTimer, TimeEntry } from '@/types';
interface TimerContextType {
ongoingTimer: OngoingTimer | null;
isLoading: boolean;
elapsedSeconds: number;
startTimer: (projectId?: string) => Promise<void>;
updateTimerProject: (projectId?: string | null) => Promise<void>;
stopTimer: (projectId?: string) => Promise<TimeEntry | null>;
}
const TimerContext = createContext<TimerContextType | undefined>(undefined);
export function TimerProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
const [elapsedSeconds, setElapsedSeconds] = useState(0);
const [elapsedInterval, setElapsedInterval] = useState<NodeJS.Timeout | null>(null);
const { data: ongoingTimer, isLoading } = useQuery({
queryKey: ['ongoingTimer'],
queryFn: timerApi.getOngoing,
refetchInterval: 60000, // Refetch every minute to sync with server
});
// Calculate elapsed time
useEffect(() => {
if (ongoingTimer) {
const startTime = new Date(ongoingTimer.startTime).getTime();
const now = Date.now();
const initialElapsed = Math.floor((now - startTime) / 1000);
setElapsedSeconds(initialElapsed);
// Start interval to update elapsed time
const interval = setInterval(() => {
setElapsedSeconds(Math.floor((Date.now() - startTime) / 1000));
}, 1000);
setElapsedInterval(interval);
} else {
setElapsedSeconds(0);
if (elapsedInterval) {
clearInterval(elapsedInterval);
setElapsedInterval(null);
}
}
return () => {
if (elapsedInterval) {
clearInterval(elapsedInterval);
}
};
}, [ongoingTimer]);
// Start timer mutation
const startMutation = useMutation({
mutationFn: timerApi.start,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ongoingTimer'] });
},
});
// Update timer mutation
const updateMutation = useMutation({
mutationFn: timerApi.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ongoingTimer'] });
},
});
// Stop timer mutation
const stopMutation = useMutation({
mutationFn: timerApi.stop,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ongoingTimer'] });
queryClient.invalidateQueries({ queryKey: ['timeEntries'] });
},
});
const startTimer = useCallback(
async (projectId?: string) => {
await startMutation.mutateAsync(projectId);
},
[startMutation]
);
const updateTimerProject = useCallback(
async (projectId?: string | null) => {
await updateMutation.mutateAsync(projectId);
},
[updateMutation]
);
const stopTimer = useCallback(
async (projectId?: string): Promise<TimeEntry | null> => {
try {
const entry = await stopMutation.mutateAsync(projectId);
return entry;
} catch {
return null;
}
},
[stopMutation]
);
return (
<TimerContext.Provider
value={{
ongoingTimer: ongoingTimer ?? null,
isLoading,
elapsedSeconds,
startTimer,
updateTimerProject,
stopTimer,
}}
>
{children}
</TimerContext.Provider>
);
}
export function useTimer() {
const context = useContext(TimerContext);
if (context === undefined) {
throw new Error('useTimer must be used within a TimerProvider');
}
return context;
}