because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(client): update client components and hooks

+131 -63
+6 -4
apps/client/src/components/ConfirmationModal.tsx
··· 31 31 /** 32 32 * Reusable confirmation modal component 33 33 */ 34 - export default function ConfirmationModal({ 34 + export const ConfirmationModal = ({ 35 35 isOpen, 36 36 onClose, 37 37 onConfirm, ··· 40 40 confirmText = "Confirm", 41 41 cancelText = "Cancel", 42 42 variant = "danger", 43 - }: ConfirmationModalProps) { 44 - if (!isOpen) return null; 43 + }: ConfirmationModalProps) => { 44 + if (!isOpen) { 45 + return null; 46 + } 45 47 46 48 const getIcon = () => { 47 49 switch (variant) { ··· 106 108 </div> 107 109 </Fragment> 108 110 ); 109 - } 111 + };
+1 -2
apps/client/src/components/ErrorBoundary.tsx
··· 1 + import { Button, ErrorIcon } from "@cv/ui"; 1 2 import type React from "react"; 2 - import { ErrorIcon } from "@/components/icons"; 3 - import Button from "@/ui/Button"; 4 3 5 4 interface ErrorBoundaryProps { 6 5 error?: Error | null;
+25 -16
apps/client/src/components/Navbar.tsx
··· 1 - import { Link } from "react-router-dom"; 2 - import Button from "@/ui/Button"; 1 + import { Link, useLocation } from "react-router-dom"; 2 + import { cn } from "@/utils/cn"; 3 3 import type { NavLink } from "./navLinks"; 4 + import { UserProfileDrawer } from "./UserProfileDrawer"; 4 5 5 6 type NavbarProps = { 6 7 user: { ··· 10 11 links: NavLink[]; 11 12 }; 12 13 13 - export default function Navbar({ user, onLogout, links }: NavbarProps) { 14 + export const Navbar = ({ user, onLogout, links }: NavbarProps) => { 15 + const location = useLocation(); 16 + 17 + const isActive = (path: string) => { 18 + return ( 19 + location.pathname === path || location.pathname.startsWith(`${path}/`) 20 + ); 21 + }; 22 + 14 23 return ( 15 24 <nav className="border-b border-ctp-surface0 bg-ctp-crust/40"> 16 25 <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> 17 - <div className="flex h-16 items-center justify-between"> 18 - <div className="flex items-center space-x-8"> 26 + <div className="flex min-h-16 items-center justify-between py-2"> 27 + <div className="flex items-center space-x-4 sm:space-x-8"> 19 28 <Link to="/" className="text-xl font-bold text-ctp-text"> 20 29 CV Generator 21 30 </Link> 22 - <div className="flex space-x-4"> 31 + <div className="hidden sm:flex space-x-4"> 23 32 {links.map((link) => ( 24 33 <Link 25 34 key={link.to} 26 35 to={link.to} 27 - className="rounded-md px-3 py-2 text-sm font-medium text-ctp-text hover:bg-ctp-surface0" 36 + className={cn( 37 + "rounded-md px-3 py-2 text-sm font-medium transition-colors", 38 + isActive(link.to) 39 + ? "bg-ctp-surface0 text-ctp-blue" 40 + : "text-ctp-text hover:bg-ctp-surface0", 41 + )} 28 42 > 29 43 {link.label} 30 44 </Link> 31 45 ))} 32 46 </div> 33 47 </div> 34 - <div className="flex items-center space-x-4"> 35 - <span className="text-sm text-ctp-subtext0"> 36 - Welcome, {user.name} 37 - </span> 38 - <Button variant="ghost" onClick={onLogout}> 39 - Logout 40 - </Button> 41 - </div> 48 + 49 + {/* User Profile Drawer */} 50 + <UserProfileDrawer user={user} onLogout={onLogout} /> 42 51 </div> 43 52 </div> 44 53 </nav> 45 54 ); 46 - } 55 + };
+2
apps/client/src/components/navLinks.ts
··· 6 6 export const defaultNavLinks: NavLink[] = [ 7 7 { to: "/", label: "Dashboard" }, 8 8 { to: "/job-experience", label: "Job Experience" }, 9 + { to: "/education", label: "Education" }, 10 + { to: "/cvs", label: "My CVs" }, 9 11 { to: "/organizations", label: "Organizations" }, 10 12 { to: "/vacancies", label: "Vacancies" }, 11 13 { to: "/profile", label: "Profile" },
+68 -24
apps/client/src/hooks/useAuth.ts
··· 1 - import { raise } from "@cv/utils"; 2 1 import { useToken } from "@/contexts/TokenProvider"; 3 2 import { useLoginMutation, useRegisterMutation } from "@/generated/graphql"; 4 - import { handleAuthSuccess } from "@/utils/auth"; 3 + 4 + const useAuthHandler = () => { 5 + const { setTokens } = useToken(); 6 + 7 + const handleAuthSuccess = (tokens: { 8 + access_token: string; 9 + refresh_token: string; 10 + expires_at: string; 11 + }) => { 12 + setTokens({ 13 + accessToken: tokens.access_token, 14 + refreshToken: tokens.refresh_token, 15 + expiresAt: tokens.expires_at, 16 + }); 17 + }; 18 + 19 + return { handleAuthSuccess }; 20 + }; 5 21 6 22 export function useLogin() { 7 - const { setToken } = useToken(); 23 + const { handleAuthSuccess } = useAuthHandler(); 8 24 9 - return useLoginMutation({ 10 - onCompleted: ({ login }) => { 11 - const { access_token, user } = login; 12 - if (access_token && user) { 13 - setToken(access_token); 14 - handleAuthSuccess(user); 15 - } else { 16 - raise("Login response missing required fields"); 17 - } 18 - }, 25 + const mutation = useLoginMutation({ 26 + onSuccess: ({ login }) => handleAuthSuccess(login), 19 27 }); 28 + 29 + if (mutation.error) { 30 + const graphqlError = mutation.error as { 31 + response?: { 32 + errors?: Array<{ message?: string }>; 33 + }; 34 + }; 35 + 36 + const errorMessage = 37 + graphqlError?.response?.errors?.[0]?.message || 38 + "Login failed. Please check your credentials."; 39 + 40 + // Create a new error with the user-friendly message 41 + const userFriendlyError = new Error(errorMessage); 42 + 43 + return { 44 + ...mutation, 45 + error: userFriendlyError, 46 + }; 47 + } 48 + 49 + return mutation; 20 50 } 21 51 22 52 export function useRegister() { 23 - const { setToken } = useToken(); 53 + const { handleAuthSuccess } = useAuthHandler(); 24 54 25 - return useRegisterMutation({ 26 - onCompleted: ({ register }) => { 27 - const { access_token, user } = register; 28 - if (access_token && user) { 29 - setToken(access_token); 30 - handleAuthSuccess(user); 31 - } else { 32 - raise("Register response missing required fields"); 33 - } 34 - }, 55 + const mutation = useRegisterMutation({ 56 + onSuccess: ({ register }) => handleAuthSuccess(register), 35 57 }); 58 + 59 + if (mutation.error) { 60 + const graphqlError = mutation.error as { 61 + response?: { 62 + errors?: Array<{ message?: string }>; 63 + }; 64 + }; 65 + 66 + const errorMessage = 67 + graphqlError?.response?.errors?.[0]?.message || 68 + "Registration failed. Please try again."; 69 + 70 + // Create a new error with the user-friendly message 71 + const userFriendlyError = new Error(errorMessage); 72 + 73 + return { 74 + ...mutation, 75 + error: userFriendlyError, 76 + }; 77 + } 78 + 79 + return mutation; 36 80 }
+29 -17
apps/client/src/hooks/useServerHealth.ts
··· 14 14 const lastErrorToastRef = useRef<Date | null>(null); 15 15 const wasOfflineRef = useRef(false); 16 16 17 - // Use Apollo's generated hook with polling 18 - const healthQuery = useHealthQuery({ 19 - fetchPolicy: "network-only", 20 - errorPolicy: "ignore", 21 - pollInterval: 30000, // Poll every 30 seconds 22 - notifyOnNetworkStatusChange: true, 23 - }); 17 + // Use React Query's generated hook with aggressive polling and no caching 18 + const healthQuery = useHealthQuery( 19 + {}, 20 + { 21 + refetchInterval: 5000, // Poll every 5 seconds for better responsiveness 22 + staleTime: 0, // Always consider data stale for network-only behavior 23 + gcTime: 0, // Don't cache results at all 24 + refetchOnWindowFocus: true, // Refetch when window regains focus 25 + refetchOnReconnect: true, // Refetch when network reconnects 26 + retry: false, // Don't retry failed requests immediately 27 + retryOnMount: false, // Don't retry on component mount 28 + }, 29 + ); 24 30 25 31 // Handle toast notifications only 26 32 useEffect(() => { ··· 39 45 if (shouldShowError) { 40 46 lastErrorToastRef.current = now; 41 47 const serverUrl = 42 - import.meta.env["VITE_SERVER_URL"] || "http://localhost:3000"; 48 + import.meta.env.VITE_SERVER_URL || "http://localhost:3000"; 43 49 showError( 44 50 "Server Unreachable", 45 51 `Cannot connect to ${serverUrl}. Check your connection and try again.`, ··· 51 57 // Show success toast only when recovering from offline state 52 58 if (wasOfflineRef.current) { 53 59 const serverUrl = 54 - import.meta.env["VITE_SERVER_URL"] || "http://localhost:3000"; 60 + import.meta.env.VITE_SERVER_URL || "http://localhost:3000"; 55 61 showSuccess( 56 62 "Server Connected", 57 63 `Successfully connected to ${serverUrl}`, ··· 63 69 } 64 70 }, [healthQuery, showError, showSuccess]); 65 71 66 - // Compute derived state from Apollo's data 67 - const status: ServerHealthStatus = 68 - healthQuery.loading && !healthQuery.data 72 + // Compute derived state from React Query's data 73 + // Aggressive offline detection with no caching: 74 + // - If we have fresh data, we're online 75 + // - If we're pending for the first time, we're checking 76 + // - If we have an error, we're immediately offline 77 + // - If we were online but now have stale data, go offline 78 + const status: ServerHealthStatus = healthQuery.error 79 + ? "offline" 80 + : healthQuery.isPending && !healthQuery.data 69 81 ? "checking" 70 - : healthQuery.error && !healthQuery.data 71 - ? "offline" 72 - : "online"; 82 + : healthQuery.data 83 + ? "online" 84 + : "offline"; 73 85 74 86 return { 75 87 // Derived state for convenience ··· 82 94 serverTime: healthQuery.data?.health?.timestamp ?? null, 83 95 serverTimezone: healthQuery.data?.health?.timezone ?? null, 84 96 serverUptime: healthQuery.data?.health?.uptime ?? null, 85 - // Expose Apollo's native properties for advanced usage 86 - ...healthQuery, 97 + // Manual refetch function 98 + refetch: healthQuery.refetch, 87 99 }; 88 100 }