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): merge providers into contexts and update imports

+243 -21
+115
apps/client/src/contexts/ConfirmationModalContext.tsx
··· 1 + import { 2 + createContext, 3 + type ReactNode, 4 + useCallback, 5 + useContext, 6 + useState, 7 + } from "react"; 8 + import ConfirmationModal from "@/components/ConfirmationModal"; 9 + 10 + interface ConfirmationModalState { 11 + isOpen: boolean; 12 + title: string; 13 + message: string; 14 + confirmText: string; 15 + cancelText: string; 16 + variant: "danger" | "warning" | "info"; 17 + onConfirm: () => void; 18 + onCancel?: () => void; 19 + } 20 + 21 + interface ConfirmationModalContextType { 22 + showConfirmation: ( 23 + config: Omit<ConfirmationModalState, "isOpen">, 24 + ) => Promise<boolean>; 25 + hideConfirmation: () => void; 26 + } 27 + 28 + const ConfirmationModalContext = createContext< 29 + ConfirmationModalContextType | undefined 30 + >(undefined); 31 + 32 + interface ConfirmationModalProviderProps { 33 + children: ReactNode; 34 + } 35 + 36 + export function ConfirmationModalProvider({ 37 + children, 38 + }: ConfirmationModalProviderProps) { 39 + const [state, setState] = useState<ConfirmationModalState>({ 40 + isOpen: false, 41 + title: "", 42 + message: "", 43 + confirmText: "Confirm", 44 + cancelText: "Cancel", 45 + variant: "danger", 46 + onConfirm: () => {}, 47 + }); 48 + 49 + const [resolvePromise, setResolvePromise] = useState< 50 + ((value: boolean) => void) | null 51 + >(null); 52 + 53 + const showConfirmation = useCallback( 54 + (config: Omit<ConfirmationModalState, "isOpen">): Promise<boolean> => { 55 + return new Promise<boolean>((resolve) => { 56 + setResolvePromise(() => resolve); 57 + setState({ 58 + ...config, 59 + isOpen: true, 60 + }); 61 + }); 62 + }, 63 + [], 64 + ); 65 + 66 + const hideConfirmation = useCallback(() => { 67 + setState((prev) => ({ ...prev, isOpen: false })); 68 + if (resolvePromise) { 69 + resolvePromise(false); 70 + setResolvePromise(null); 71 + } 72 + }, [resolvePromise]); 73 + 74 + const handleConfirm = useCallback(() => { 75 + state.onConfirm(); 76 + setState((prev) => ({ ...prev, isOpen: false })); 77 + if (resolvePromise) { 78 + resolvePromise(true); 79 + setResolvePromise(null); 80 + } 81 + }, [state.onConfirm, resolvePromise, state]); 82 + 83 + const handleCancel = useCallback(() => { 84 + state.onCancel?.(); 85 + hideConfirmation(); 86 + }, [state.onCancel, hideConfirmation, state]); 87 + 88 + return ( 89 + <ConfirmationModalContext.Provider 90 + value={{ showConfirmation, hideConfirmation }} 91 + > 92 + {children} 93 + <ConfirmationModal 94 + isOpen={state.isOpen} 95 + onClose={handleCancel} 96 + onConfirm={handleConfirm} 97 + title={state.title} 98 + message={state.message} 99 + confirmText={state.confirmText} 100 + cancelText={state.cancelText} 101 + variant={state.variant} 102 + /> 103 + </ConfirmationModalContext.Provider> 104 + ); 105 + } 106 + 107 + export function useConfirmationModal(): ConfirmationModalContextType { 108 + const context = useContext(ConfirmationModalContext); 109 + if (context === undefined) { 110 + throw new Error( 111 + "useConfirmationModal must be used within a ConfirmationModalProvider", 112 + ); 113 + } 114 + return context; 115 + }
+4 -4
apps/client/src/contexts/ToastContext.tsx
··· 49 49 }; 50 50 51 51 const showSuccess = (title: string, message?: string) => { 52 - showToast({ title, message, level: "success" }); 52 + showToast({ title, message: message ?? "", level: "success" }); 53 53 }; 54 54 55 55 const showWarning = (title: string, message?: string) => { 56 - showToast({ title, message, level: "warning" }); 56 + showToast({ title, message: message ?? "", level: "warning" }); 57 57 }; 58 58 59 59 const showInfo = (title: string, message?: string) => { 60 - showToast({ title, message, level: "info" }); 60 + showToast({ title, message: message ?? "", level: "info" }); 61 61 }; 62 62 63 63 const showError = (title: string, message?: string) => { 64 - showToast({ title, message, level: "error" }); 64 + showToast({ title, message: message ?? "", level: "error" }); 65 65 }; 66 66 67 67 const removeToast = (id: string) => {
+91
apps/client/src/contexts/TokenProvider.tsx
··· 1 + import { useApolloClient } from "@apollo/client"; 2 + import { raise } from "@cv/utils"; 3 + import { 4 + createContext, 5 + type ReactNode, 6 + useContext, 7 + useEffect, 8 + useState, 9 + } from "react"; 10 + import { AUTH_TOKEN_KEY } from "@/constants/auth"; 11 + import { useMeMinimalQuery } from "@/generated/graphql"; 12 + 13 + interface TokenContextType { 14 + token: string | null; 15 + setToken: (token: string) => void; 16 + clearToken: () => void; 17 + isAuthenticated: boolean; 18 + isLoading: boolean; 19 + } 20 + 21 + const TokenContext = createContext<TokenContextType | null>(null); 22 + 23 + export function TokenProvider({ children }: { children: ReactNode }) { 24 + const [token, setTokenState] = useState<string | null>(null); 25 + const [isAuthenticated, setIsAuthenticated] = useState(false); 26 + const [isLoading, setIsLoading] = useState(true); 27 + const apolloClient = useApolloClient(); 28 + 29 + useEffect(() => { 30 + const storedToken = localStorage.getItem(AUTH_TOKEN_KEY); 31 + setTokenState(storedToken); 32 + }, []); 33 + 34 + const { 35 + data: meData, 36 + loading: meLoading, 37 + error: meError, 38 + } = useMeMinimalQuery({ skip: !token }); 39 + 40 + useEffect(() => { 41 + if (!token) { 42 + setIsLoading(false); 43 + setIsAuthenticated(false); 44 + return; 45 + } 46 + 47 + if (meLoading) { 48 + setIsLoading(true); 49 + return; 50 + } 51 + 52 + if (meData) { 53 + setIsAuthenticated(true); 54 + setIsLoading(false); 55 + return; 56 + } 57 + 58 + if (meError) { 59 + setIsAuthenticated(false); 60 + setIsLoading(false); 61 + localStorage.removeItem(AUTH_TOKEN_KEY); 62 + setTokenState(null); 63 + } 64 + }, [token, meData, meLoading, meError]); 65 + 66 + const setToken = (newToken: string) => { 67 + localStorage.setItem(AUTH_TOKEN_KEY, newToken); 68 + setTokenState(newToken); 69 + setIsLoading(true); 70 + }; 71 + 72 + const clearToken = () => { 73 + localStorage.removeItem(AUTH_TOKEN_KEY); 74 + setTokenState(null); 75 + setIsAuthenticated(false); 76 + apolloClient.clearStore(); 77 + }; 78 + 79 + return ( 80 + <TokenContext.Provider 81 + value={{ token, setToken, clearToken, isAuthenticated, isLoading }} 82 + > 83 + {children} 84 + </TokenContext.Provider> 85 + ); 86 + } 87 + 88 + export function useToken() { 89 + const context = useContext(TokenContext); 90 + return context ?? raise("useToken must be used within a TokenProvider"); 91 + }
+1 -1
apps/client/src/hooks/useAuth.ts
··· 1 1 import { raise } from "@cv/utils"; 2 + import { useToken } from "@/contexts/TokenProvider"; 2 3 import { useLoginMutation, useRegisterMutation } from "@/generated/graphql"; 3 - import { useToken } from "@/providers/TokenProvider"; 4 4 import { handleAuthSuccess } from "@/utils/auth"; 5 5 6 6 export function useLogin() {
+9 -5
apps/client/src/layouts/AuthenticatedLayout.tsx
··· 1 1 import { Outlet } from "react-router-dom"; 2 - import { useMeMinimalQuery } from "@/generated/graphql"; 3 - import { useToken } from "@/providers/TokenProvider"; 4 - import Button from "@/ui/Button"; 5 2 import Navbar from "@/components/Navbar"; 6 3 import { defaultNavLinks } from "@/components/navLinks"; 4 + import { useToken } from "@/contexts/TokenProvider"; 5 + import { useMeMinimalQuery } from "@/generated/graphql"; 6 + import Button from "@/ui/Button"; 7 7 8 8 export default function AuthenticatedLayout() { 9 9 const { data, loading, error } = useMeMinimalQuery(); ··· 38 38 39 39 return ( 40 40 <div className="min-h-screen bg-ctp-base"> 41 - <Navbar user={{ name: data?.me?.name ?? "" }} onLogout={handleLogout} links={defaultNavLinks} /> 42 - 41 + <Navbar 42 + user={{ name: data?.me?.name ?? "" }} 43 + onLogout={handleLogout} 44 + links={defaultNavLinks} 45 + /> 46 + 43 47 {/* Main content */} 44 48 <main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"> 45 49 <Outlet />
+5 -2
apps/client/src/main.tsx
··· 2 2 import React from "react"; 3 3 import { createRoot } from "react-dom/client"; 4 4 import App from "./App"; 5 + import { ConfirmationModalProvider } from "./contexts/ConfirmationModalContext"; 5 6 import { ToastProvider } from "./contexts/ToastContext"; 7 + import { TokenProvider } from "./contexts/TokenProvider"; 6 8 import { apolloClient } from "./lib/apollo-client"; 7 - import { TokenProvider } from "./providers/TokenProvider"; 8 9 import "./index.css"; 9 10 10 11 const container = document.getElementById("app"); ··· 16 17 <ApolloProvider client={apolloClient}> 17 18 <TokenProvider> 18 19 <ToastProvider> 19 - <App /> 20 + <ConfirmationModalProvider> 21 + <App /> 22 + </ConfirmationModalProvider> 20 23 </ToastProvider> 21 24 </TokenProvider> 22 25 </ApolloProvider>
+18 -9
apps/client/src/router/AppRouter.tsx
··· 1 1 import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; 2 - import LoginForm from "@/features/auth/LoginForm"; 3 - import RegisterForm from "@/features/auth/RegisterForm"; 2 + import { useToken } from "@/contexts/TokenProvider"; 3 + import { LoginForm, RegisterForm } from "@/features/auth/components"; 4 4 import AuthenticatedLayout from "@/layouts/AuthenticatedLayout"; 5 + import CreateJobExperiencePage from "@/pages/CreateJobExperiencePage"; 6 + import CreateVacancyPage from "@/pages/CreateVacancyPage"; 5 7 import DashboardPage from "@/pages/DashboardPage"; 6 8 import JobExperiencePage from "@/pages/JobExperiencePage"; 7 9 import OrganizationsPage from "@/pages/OrganizationsPage"; 8 10 import ProfilePage from "@/pages/ProfilePage"; 9 - import { useToken } from "@/providers/TokenProvider"; 11 + import VacanciesPage from "@/pages/VacanciesPage"; 10 12 11 13 export default function AppRouter() { 12 14 const { isAuthenticated, isLoading } = useToken(); ··· 27 29 {/* Public routes */} 28 30 <Route 29 31 path="/auth/login" 30 - element={isAuthenticated ? <Navigate to="/" replace /> : <LoginForm />} 32 + element={ 33 + isAuthenticated ? <Navigate to="/" replace /> : <LoginForm /> 34 + } 31 35 /> 32 36 <Route 33 37 path="/auth/register" 34 - element={isAuthenticated ? <Navigate to="/" replace /> : <RegisterForm />} 38 + element={ 39 + isAuthenticated ? <Navigate to="/" replace /> : <RegisterForm /> 40 + } 35 41 /> 36 - <Route 37 - path="/auth" 38 - element={<Navigate to="/auth/login" replace />} 39 - /> 42 + <Route path="/auth" element={<Navigate to="/auth/login" replace />} /> 40 43 41 44 {/* Protected routes with nested layout */} 42 45 <Route ··· 52 55 <Route index element={<DashboardPage />} /> 53 56 <Route path="profile" element={<ProfilePage />} /> 54 57 <Route path="job-experience" element={<JobExperiencePage />} /> 58 + <Route 59 + path="job-experience/create" 60 + element={<CreateJobExperiencePage />} 61 + /> 55 62 <Route path="organizations" element={<OrganizationsPage />} /> 63 + <Route path="vacancies" element={<VacanciesPage />} /> 64 + <Route path="vacancies/create" element={<CreateVacancyPage />} /> 56 65 </Route> 57 66 58 67 {/* Catch all */}