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 pages and routing to use new features

+488 -183
+4 -4
apps/client/src/layouts/AuthenticatedLayout.tsx
··· 1 + import { Button } from "@cv/ui"; 1 2 import { Outlet } from "react-router-dom"; 2 - import Navbar from "@/components/Navbar"; 3 + import { Navbar } from "@/components/Navbar"; 3 4 import { defaultNavLinks } from "@/components/navLinks"; 4 5 import { useToken } from "@/contexts/TokenProvider"; 5 6 import { useMeMinimalQuery } from "@/generated/graphql"; 6 - import Button from "@/ui/Button"; 7 7 8 - export default function AuthenticatedLayout() { 8 + export const AuthenticatedLayout = () => { 9 9 const { data, loading, error } = useMeMinimalQuery(); 10 10 const { clearToken } = useToken(); 11 11 ··· 50 50 </main> 51 51 </div> 52 52 ); 53 - } 53 + };
+173
apps/client/src/pages/CreateCVPage.tsx
··· 1 + import { Button, PageHeader, Placeholder, TextInput } from "@cv/ui"; 2 + import { useQueryClient } from "@tanstack/react-query"; 3 + import { useState } from "react"; 4 + import { useNavigate } from "react-router-dom"; 5 + import { useToast } from "@/contexts/ToastContext"; 6 + import { useCreateCvMutation, useCvTemplatesQuery } from "@/generated/graphql"; 7 + 8 + export const CreateCVPage = () => { 9 + const navigate = useNavigate(); 10 + const queryClient = useQueryClient(); 11 + const { data: templatesData, isLoading: templatesLoading } = 12 + useCvTemplatesQuery({}); 13 + const { mutateAsync: createCV, isPending: creating } = useCreateCvMutation(); 14 + const { showSuccess, showError } = useToast(); 15 + 16 + const [formData, setFormData] = useState({ 17 + title: "", 18 + templateId: "", 19 + isPublic: false, 20 + }); 21 + 22 + const templates = 23 + templatesData?.cvTemplates?.edges?.map((edge) => edge.node) || []; 24 + 25 + const handleSubmit = async (e: React.FormEvent) => { 26 + e.preventDefault(); 27 + 28 + const hasTitle = formData.title.length > 0; 29 + const hasTemplate = formData.templateId.length > 0; 30 + 31 + if (!(hasTitle && hasTemplate)) { 32 + showError("Validation Error", "Please fill in all required fields."); 33 + return; 34 + } 35 + 36 + try { 37 + const result = await createCV({ 38 + input: { 39 + title: formData.title, 40 + templateId: formData.templateId, 41 + }, 42 + }); 43 + 44 + // Invalidate CV list query to refresh the cache 45 + await queryClient.invalidateQueries({ 46 + queryKey: ["MyCVs"], 47 + exact: false, 48 + }); 49 + 50 + showSuccess("CV Created", "Your CV has been successfully created."); 51 + navigate(`/cvs/${result.createCV.id}/edit`); 52 + } catch (error) { 53 + console.error("Error creating CV:", error); 54 + showError( 55 + "Failed to Create CV", 56 + "There was an error creating the CV. Please try again.", 57 + ); 58 + } 59 + }; 60 + 61 + if (templatesLoading) { 62 + return ( 63 + <div className="space-y-6"> 64 + <PageHeader 65 + title="Create New CV" 66 + description="Choose a template and give your CV a title" 67 + /> 68 + <Placeholder variant="loading" message="Loading templates..." /> 69 + </div> 70 + ); 71 + } 72 + 73 + return ( 74 + <div className="space-y-6"> 75 + <PageHeader 76 + title="Create New CV" 77 + description="Choose a template and give your CV a title" 78 + /> 79 + 80 + <form onSubmit={handleSubmit} className="space-y-8"> 81 + <div className="bg-ctp-surface0 rounded-lg border border-ctp-surface1 p-6"> 82 + <h2 className="text-xl font-semibold text-ctp-text mb-4"> 83 + CV Details 84 + </h2> 85 + 86 + <TextInput 87 + label="CV Title" 88 + value={formData.title} 89 + onChange={(value) => 90 + setFormData((prev) => ({ ...prev, title: value })) 91 + } 92 + placeholder="e.g., Software Engineer CV, Marketing Manager Resume" 93 + required 94 + /> 95 + 96 + <div className="mt-4"> 97 + <label className="flex items-center gap-2 cursor-pointer"> 98 + <input 99 + type="checkbox" 100 + checked={formData.isPublic} 101 + onChange={(e) => 102 + setFormData((prev) => ({ 103 + ...prev, 104 + isPublic: e.target.checked, 105 + })) 106 + } 107 + className="w-4 h-4 rounded border-ctp-surface1 bg-ctp-base text-ctp-blue focus:ring-ctp-blue" 108 + /> 109 + <span className="text-sm text-ctp-text">Make this CV public</span> 110 + </label> 111 + </div> 112 + </div> 113 + 114 + <div className="bg-ctp-surface0 rounded-lg border border-ctp-surface1 p-6"> 115 + <h2 className="text-xl font-semibold text-ctp-text mb-4"> 116 + Choose a Template 117 + </h2> 118 + 119 + {templates.length === 0 ? ( 120 + <p className="text-ctp-subtext0">No templates available.</p> 121 + ) : ( 122 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> 123 + {templates.map((template) => ( 124 + <button 125 + key={template.id} 126 + type="button" 127 + onClick={() => 128 + setFormData((prev) => ({ 129 + ...prev, 130 + templateId: template.id, 131 + })) 132 + } 133 + className={`p-4 rounded-lg border-2 transition-all text-left ${ 134 + formData.templateId === template.id 135 + ? "border-ctp-blue bg-ctp-blue/10" 136 + : "border-ctp-surface1 hover:border-ctp-blue/50" 137 + }`} 138 + > 139 + <div className="flex items-start justify-between mb-2"> 140 + <h3 className="font-semibold text-ctp-text"> 141 + {template.name} 142 + </h3> 143 + </div> 144 + {template.description && ( 145 + <p className="text-sm text-ctp-subtext0 mb-2"> 146 + {template.description} 147 + </p> 148 + )} 149 + </button> 150 + ))} 151 + </div> 152 + )} 153 + </div> 154 + 155 + <div className="flex gap-3"> 156 + <Button 157 + type="submit" 158 + disabled={creating || !formData.title || !formData.templateId} 159 + > 160 + {creating ? "Creating..." : "Create CV"} 161 + </Button> 162 + <Button 163 + type="button" 164 + variant="ghost" 165 + onClick={() => navigate("/cvs")} 166 + > 167 + Cancel 168 + </Button> 169 + </div> 170 + </form> 171 + </div> 172 + ); 173 + };
+5 -10
apps/client/src/pages/CreateJobExperiencePage.tsx
··· 1 + import { PageHeader } from "@cv/ui"; 1 2 import { useNavigate } from "react-router-dom"; 2 3 import { JobExperienceCreationSelector } from "@/features/job-experience/components/JobExperienceCreationSelector"; 3 4 ··· 14 15 15 16 return ( 16 17 <div className="space-y-6"> 17 - <div className="flex items-center justify-between"> 18 - <div> 19 - <h1 className="text-2xl font-bold text-ctp-text"> 20 - Create Job Experience 21 - </h1> 22 - <p className="text-ctp-subtext0 mt-1"> 23 - Add a new job experience to your profile 24 - </p> 25 - </div> 26 - </div> 18 + <PageHeader 19 + title="Create Job Experience" 20 + description="Add a new job experience to your profile" 21 + /> 27 22 28 23 <JobExperienceCreationSelector 29 24 onSuccess={handleSuccess}
+13 -24
apps/client/src/pages/CreateVacancyPage.tsx
··· 1 + import { Button, PageHeader } from "@cv/ui"; 1 2 import { useNavigate } from "react-router-dom"; 2 3 import { VacancyCreationSelector } from "@/features/vacancies/components"; 3 - import Button from "@/ui/Button"; 4 4 5 5 export default function CreateVacancyPage() { 6 6 const navigate = useNavigate(); 7 7 8 - const handleSuccess = () => { 9 - navigate("/vacancies"); 10 - }; 11 - 12 - const handleCancel = () => { 8 + const next = () => { 13 9 navigate("/vacancies"); 14 10 }; 15 11 16 12 return ( 17 - <div className="container mx-auto py-8 px-4 max-w-4xl"> 18 - <div className="mb-6"> 19 - <Button 20 - variant="ghost" 21 - onClick={() => navigate("/vacancies")} 22 - className="mb-4" 23 - > 24 - ← Back to Vacancies 25 - </Button> 26 - <h1 className="text-3xl font-bold text-ctp-text">Create New Vacancy</h1> 27 - <p className="text-ctp-subtext0 mt-2"> 28 - Choose how you'd like to create your vacancy listing 29 - </p> 30 - </div> 13 + <div className="space-y-6"> 14 + <PageHeader 15 + title="Create New Vacancy" 16 + description="Choose how you'd like to create your vacancy listing" 17 + action={ 18 + <Button variant="ghost" onClick={next}> 19 + ← Back to Vacancies 20 + </Button> 21 + } 22 + /> 31 23 32 24 <div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-6 shadow-sm"> 33 - <VacancyCreationSelector 34 - onSuccess={handleSuccess} 35 - onCancel={handleCancel} 36 - /> 25 + <VacancyCreationSelector onSuccess={next} onCancel={next} /> 37 26 </div> 38 27 </div> 39 28 );
+99 -32
apps/client/src/pages/DashboardPage.tsx
··· 1 - import { useMeMinimalQuery } from "@/generated/graphql"; 2 - import Button from "@/ui/Button"; 1 + import { Button, FormattedDate, PageHeader, Placeholder } from "@cv/ui"; 2 + import { Link } from "react-router-dom"; 3 + import { 4 + useCvTemplatesQuery, 5 + useMeMinimalQuery, 6 + useMyCVsQuery, 7 + } from "@/generated/graphql"; 3 8 4 9 export default function DashboardPage() { 5 - const { data, loading, error } = useMeMinimalQuery(); 10 + const { 11 + data: userData, 12 + isLoading: userLoading, 13 + error: userError, 14 + } = useMeMinimalQuery(); 15 + const { data: cvsData, isLoading: cvsLoading } = useMyCVsQuery({ first: 5 }); 16 + const { data: templatesData, isLoading: templatesLoading } = 17 + useCvTemplatesQuery({ first: 10 }); 6 18 7 - if (loading) { 19 + if (userLoading || cvsLoading || templatesLoading) { 8 20 return ( 9 - <div className="flex min-h-screen items-center justify-center"> 10 - <div className="text-center"> 11 - <div className="mb-4 text-lg">Loading...</div> 12 - </div> 21 + <div className="space-y-6"> 22 + <PageHeader title="Dashboard" /> 23 + <Placeholder variant="loading" message="Loading..." /> 13 24 </div> 14 25 ); 15 26 } 16 27 17 - if (error) { 28 + if (userError) { 18 29 return ( 19 - <div className="flex min-h-screen items-center justify-center"> 20 - <div className="text-center"> 21 - <div className="mb-4 text-lg text-ctp-red"> 22 - Error loading user data 23 - </div> 24 - </div> 30 + <div className="space-y-6"> 31 + <PageHeader title="Dashboard" /> 32 + <Placeholder variant="error" message="Error loading user data" /> 25 33 </div> 26 34 ); 27 35 } 28 36 29 - const user = data?.me; 37 + const user = userData?.me; 38 + const cvs = cvsData?.me?.cvs?.edges || []; 39 + const totalCVs = cvsData?.me?.cvs?.totalCount || 0; 40 + const totalTemplates = templatesData?.cvTemplates?.totalCount || 0; 41 + 42 + const latestCV = cvs[0]?.node; 30 43 31 44 return ( 32 - <> 33 - <div className="mb-8"> 34 - <h1 className="text-3xl font-bold text-ctp-text">Dashboard</h1> 35 - <p className="mt-2 text-ctp-subtext0"> 36 - Welcome back, {user?.name}! Here's your overview. 37 - </p> 38 - </div> 45 + <div className="space-y-6"> 46 + <PageHeader 47 + title="Dashboard" 48 + description={`Welcome back, ${user?.name ?? "User"}! Here's your overview.`} 49 + /> 39 50 40 51 <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> 41 52 {/* Stats cards */} 42 53 <div className="rounded-lg bg-ctp-crust/40 p-6 shadow"> 43 54 <h3 className="text-lg font-medium text-ctp-text">CVs Created</h3> 44 - <p className="mt-2 text-3xl font-bold text-ctp-blue">0</p> 55 + <p className="mt-2 text-3xl font-bold text-ctp-blue">{totalCVs}</p> 45 56 <p className="mt-1 text-sm text-ctp-subtext0">Total CVs generated</p> 46 57 </div> 47 58 48 59 <div className="rounded-lg bg-ctp-crust/40 p-6 shadow"> 49 60 <h3 className="text-lg font-medium text-ctp-text">Templates</h3> 50 - <p className="mt-2 text-3xl font-bold text-ctp-green">3</p> 61 + <p className="mt-2 text-3xl font-bold text-ctp-green"> 62 + {totalTemplates} 63 + </p> 51 64 <p className="mt-1 text-sm text-ctp-subtext0">Available templates</p> 52 65 </div> 53 66 54 67 <div className="rounded-lg bg-ctp-crust/40 p-6 shadow"> 55 68 <h3 className="text-lg font-medium text-ctp-text">Last Updated</h3> 56 - <p className="mt-2 text-sm text-ctp-subtext0">Never</p> 57 - <p className="mt-1 text-sm text-ctp-subtext0">Your latest CV</p> 69 + {latestCV ? ( 70 + <> 71 + <p className="mt-2 text-sm text-ctp-subtext0"> 72 + <FormattedDate date={latestCV.updatedAt} /> 73 + </p> 74 + <p className="mt-1 text-sm text-ctp-text font-medium"> 75 + {latestCV.title} 76 + </p> 77 + </> 78 + ) : ( 79 + <p className="mt-2 text-sm text-ctp-subtext0">Never</p> 80 + )} 58 81 </div> 59 82 </div> 60 83 ··· 63 86 <h2 className="mb-4 text-xl font-semibold text-ctp-text"> 64 87 Quick Actions 65 88 </h2> 66 - <div className="flex space-x-4"> 67 - <Button>Create New CV</Button> 68 - <Button variant="outline">Browse Templates</Button> 69 - <Button variant="outline">Import Data</Button> 89 + <div className="flex flex-wrap gap-4"> 90 + <Link to="/cvs/create"> 91 + <Button>Create New CV</Button> 92 + </Link> 93 + <Link to="/cvs"> 94 + <Button variant="outline">Browse My CVs</Button> 95 + </Link> 96 + <Link to="/applications/apply"> 97 + <Button variant="outline">Apply to Vacancy</Button> 98 + </Link> 99 + <Link to="/applications"> 100 + <Button variant="outline">My Applications</Button> 101 + </Link> 70 102 </div> 71 103 </div> 72 - </> 104 + 105 + {/* Recent CVs */} 106 + {cvs.length > 0 && ( 107 + <div className="mt-8"> 108 + <h2 className="mb-4 text-xl font-semibold text-ctp-text"> 109 + Recent CVs 110 + </h2> 111 + <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> 112 + {cvs.slice(0, 6).map(({ node: cv }) => ( 113 + <Link 114 + key={cv.id} 115 + to={`/cvs/${cv.id}`} 116 + className="rounded-lg bg-ctp-crust/40 p-4 shadow transition-shadow hover:shadow-lg" 117 + > 118 + <h3 className="text-lg font-medium text-ctp-text"> 119 + {cv.title} 120 + </h3> 121 + <p className="mt-1 text-sm text-ctp-subtext0"> 122 + Template: {cv.template.name} 123 + </p> 124 + <p className="mt-2 text-xs text-ctp-subtext0"> 125 + Updated: <FormattedDate date={cv.updatedAt} /> 126 + </p> 127 + </Link> 128 + ))} 129 + </div> 130 + {cvs.length >= 5 && ( 131 + <div className="mt-4"> 132 + <Link to="/cvs"> 133 + <Button variant="outline">View All CVs</Button> 134 + </Link> 135 + </div> 136 + )} 137 + </div> 138 + )} 139 + </div> 73 140 ); 74 141 }
+56 -43
apps/client/src/pages/JobExperiencePage.tsx
··· 1 + import { Button, PageHeader, Placeholder } from "@cv/ui"; 1 2 import { useNavigate } from "react-router-dom"; 2 - import { ErrorDisplay } from "@/components/ErrorBoundary"; 3 3 import { useToast } from "@/contexts/ToastContext"; 4 - import { 5 - JobExperienceLoading, 6 - JobExperienceTable, 7 - } from "@/features/job-experience/components"; 4 + import { JobExperienceTable } from "@/features/job-experience/components"; 8 5 import type { MeJobExperienceQuery } from "@/generated/graphql"; 9 6 import { useMeJobExperienceQuery } from "@/generated/graphql"; 10 - import Button from "@/ui/Button"; 11 7 12 8 export default function JobExperiencePage() { 13 - const { data, loading, error, refetch } = useMeJobExperienceQuery(); 9 + const { 10 + data, 11 + isPending: loading, 12 + error, 13 + refetch, 14 + } = useMeJobExperienceQuery(); 14 15 const { showInfo } = useToast(); 15 16 const navigate = useNavigate(); 16 17 17 18 const handleEdit = ( 18 - experience: MeJobExperienceQuery["myEmploymentHistory"][0], 19 + experience: NonNullable< 20 + NonNullable<MeJobExperienceQuery["me"]>["experience"] 21 + >["edges"][number]["node"], 19 22 ) => { 20 23 // TODO: Implement edit functionality 21 24 showInfo("Edit Feature", "Edit functionality is coming soon!"); ··· 31 34 }; 32 35 33 36 if (loading) { 34 - return <JobExperienceLoading />; 37 + return ( 38 + <div className="space-y-6"> 39 + <PageHeader 40 + title="Job Experience" 41 + description="Manage your work experience and career history" 42 + /> 43 + <Placeholder variant="loading" message="Loading job experience..." /> 44 + </div> 45 + ); 35 46 } 36 47 37 48 if (error) { 38 49 return ( 39 - <ErrorDisplay 40 - error={error} 41 - title="Error Loading Job Experience" 42 - onRetry={() => window.location.reload()} 43 - /> 50 + <div className="space-y-6"> 51 + <PageHeader 52 + title="Job Experience" 53 + description="Manage your work experience and career history" 54 + /> 55 + <Placeholder variant="error" message="Error loading job experience" /> 56 + </div> 44 57 ); 45 58 } 46 59 47 - const jobExperiences = data?.myEmploymentHistory || []; 60 + const jobExperiences = 61 + data?.me?.experience?.edges?.map((edge) => edge.node) || []; 48 62 49 63 if (jobExperiences.length === 0) { 50 64 return ( 51 65 <div className="space-y-6"> 52 - <div className="flex items-center justify-between"> 53 - <div> 54 - <h1 className="text-2xl font-bold text-ctp-text">Job Experience</h1> 55 - <p className="text-ctp-subtext0 mt-1"> 56 - Manage your work experience and career history 57 - </p> 58 - </div> 66 + <PageHeader 67 + title="Job Experience" 68 + description="Manage your work experience and career history" 69 + action={ 70 + <Button onClick={() => navigate("/job-experience/create")}> 71 + Create Experience 72 + </Button> 73 + } 74 + /> 59 75 60 - <Button onClick={() => navigate("/job-experience/create")}> 76 + <Placeholder 77 + variant="empty" 78 + message="Create your first job experience to get started" 79 + > 80 + <Button 81 + onClick={() => navigate("/job-experience/create")} 82 + className="mt-4" 83 + > 61 84 Create Experience 62 85 </Button> 63 - </div> 64 - 65 - <div className="text-center py-8"> 66 - <div className="text-ctp-subtext0 mb-2">No job experiences found</div> 67 - <div className="text-sm text-ctp-subtext1"> 68 - Create your first job experience to get started 69 - </div> 70 - </div> 86 + </Placeholder> 71 87 </div> 72 88 ); 73 89 } 74 90 75 91 return ( 76 92 <div className="space-y-6"> 77 - <div className="flex items-center justify-between"> 78 - <div> 79 - <h1 className="text-2xl font-bold text-ctp-text">Job Experience</h1> 80 - <p className="text-ctp-subtext0 mt-1"> 81 - Manage your work experience and career history 82 - </p> 83 - </div> 84 - 85 - <Button onClick={() => navigate("/job-experience/create")}> 86 - Create Experience 87 - </Button> 88 - </div> 93 + <PageHeader 94 + title="Job Experience" 95 + description="Manage your work experience and career history" 96 + action={ 97 + <Button onClick={() => navigate("/job-experience/create")}> 98 + Create Experience 99 + </Button> 100 + } 101 + /> 89 102 90 103 <div> 91 104 <h2 className="text-lg font-semibold text-ctp-text mb-4">
+34 -24
apps/client/src/pages/OrganizationsPage.tsx
··· 1 - import { OrganizationMembersTable } from "@/features/organizations/components"; 2 - import { useMeWithOrganizationsQuery } from "@/generated/graphql"; 1 + import { PageHeader, Placeholder } from "@cv/ui"; 2 + import { CollapsibleOrganizationTable } from "@/features/organizations/components"; 3 + import { useMeOrganizationsQuery } from "@/generated/graphql"; 3 4 4 5 export default function OrganizationsPage() { 5 - const { data, loading, error } = useMeWithOrganizationsQuery(); 6 + const { data, isLoading, error } = useMeOrganizationsQuery(); 6 7 7 - if (loading) { 8 + if (isLoading) { 8 9 return ( 9 - <div> 10 - <h1 className="mb-6 text-2xl font-bold text-ctp-text">Organizations</h1> 11 - <div className="text-center text-ctp-subtext0"> 12 - Loading organizations... 13 - </div> 10 + <div className="space-y-6"> 11 + <PageHeader 12 + title="Organizations" 13 + description="Manage your organization memberships" 14 + /> 15 + <Placeholder variant="loading" message="Loading organizations..." /> 14 16 </div> 15 17 ); 16 18 } 17 19 18 20 if (error) { 19 21 return ( 20 - <div> 21 - <h1 className="mb-6 text-2xl font-bold text-ctp-text">Organizations</h1> 22 - <div className="text-center text-ctp-red"> 23 - Error loading organizations 24 - </div> 22 + <div className="space-y-6"> 23 + <PageHeader 24 + title="Organizations" 25 + description="Manage your organization memberships" 26 + /> 27 + <Placeholder variant="error" message="Error loading organizations" /> 25 28 </div> 26 29 ); 27 30 } ··· 30 33 31 34 if (organizations.length === 0) { 32 35 return ( 33 - <div> 34 - <h1 className="mb-6 text-2xl font-bold text-ctp-text">Organizations</h1> 35 - <div className="text-center text-ctp-subtext0"> 36 - <div className="mb-4">No organizations found</div> 37 - <p>You're not a member of any organizations yet.</p> 38 - </div> 36 + <div className="space-y-6"> 37 + <PageHeader 38 + title="Organizations" 39 + description="Manage your organization memberships" 40 + /> 41 + <Placeholder 42 + variant="empty" 43 + message="You're not a member of any organizations yet." 44 + /> 39 45 </div> 40 46 ); 41 47 } 42 48 43 49 return ( 44 - <div> 45 - <h1 className="mb-6 text-2xl font-bold text-ctp-text">Organizations</h1> 50 + <div className="space-y-6"> 51 + <PageHeader 52 + title="Organizations" 53 + description="Manage your organization memberships" 54 + /> 46 55 <div className="space-y-6"> 47 - {organizations.map((organization) => ( 48 - <OrganizationMembersTable 56 + {organizations.map((organization, index) => ( 57 + <CollapsibleOrganizationTable 49 58 key={organization.id} 50 59 organization={organization} 60 + defaultExpanded={index === 0} 51 61 /> 52 62 ))} 53 63 </div>
+18 -19
apps/client/src/pages/ProfilePage.tsx
··· 1 + import { Button, PageHeader, Placeholder, TextInput } from "@cv/ui"; 1 2 import { useState } from "react"; 2 3 import { useMeMinimalQuery } from "@/generated/graphql"; 3 - import Button from "@/ui/Button"; 4 - import TextInput from "@/ui/TextInput"; 5 4 6 5 export default function ProfilePage() { 7 6 const { data, loading, error } = useMeMinimalQuery(); ··· 32 31 33 32 if (loading) { 34 33 return ( 35 - <div className="flex min-h-screen items-center justify-center"> 36 - <div className="text-center"> 37 - <div className="mb-4 text-lg">Loading...</div> 38 - </div> 34 + <div className="space-y-6"> 35 + <PageHeader 36 + title="Profile" 37 + description="Manage your account information and preferences" 38 + /> 39 + <Placeholder variant="loading" message="Loading..." /> 39 40 </div> 40 41 ); 41 42 } 42 43 43 44 if (error) { 44 45 return ( 45 - <div className="flex min-h-screen items-center justify-center"> 46 - <div className="text-center"> 47 - <div className="mb-4 text-lg text-ctp-red"> 48 - Error loading user data 49 - </div> 50 - </div> 46 + <div className="space-y-6"> 47 + <PageHeader 48 + title="Profile" 49 + description="Manage your account information and preferences" 50 + /> 51 + <Placeholder variant="error" message="Error loading user data" /> 51 52 </div> 52 53 ); 53 54 } ··· 55 56 const user = data?.me; 56 57 57 58 return ( 58 - <div className="max-w-4xl"> 59 - <div className="mb-8"> 60 - <h1 className="text-3xl font-bold text-ctp-text">Profile</h1> 61 - <p className="mt-2 text-ctp-subtext0"> 62 - Manage your account information and preferences. 63 - </p> 64 - </div> 59 + <div className="space-y-6"> 60 + <PageHeader 61 + title="Profile" 62 + description="Manage your account information and preferences" 63 + /> 65 64 66 65 <div className="rounded-lg bg-ctp-crust/40 p-6 shadow"> 67 66 <div className="mb-6 flex items-center justify-between">
+64 -25
apps/client/src/pages/VacanciesPage.tsx
··· 1 + import { Button, PageHeader, Placeholder } from "@cv/ui"; 1 2 import { useNavigate } from "react-router-dom"; 2 3 import { useToast } from "@/contexts/ToastContext"; 3 4 import { VacancyList } from "@/features/vacancies/components"; 4 5 import { useMyVacanciesQuery } from "@/generated/graphql"; 5 - import Button from "@/ui/Button"; 6 6 7 7 export default function VacanciesPage() { 8 - const { data, loading, error, refetch } = useMyVacanciesQuery(); 8 + const { data, isLoading, error, refetch } = useMyVacanciesQuery(); 9 9 const { showError } = useToast(); 10 10 const navigate = useNavigate(); 11 11 ··· 21 21 } 22 22 }; 23 23 24 - if (loading) { 24 + if (isLoading) { 25 25 return ( 26 - <div className="flex items-center justify-center py-8"> 27 - <div className="text-ctp-subtext0">Loading vacancies...</div> 26 + <div className="space-y-6"> 27 + <PageHeader 28 + title="Job Vacancies" 29 + description="Manage your job vacancies and track applications" 30 + /> 31 + <Placeholder variant="loading" message="Loading vacancies..." /> 28 32 </div> 29 33 ); 30 34 } 31 35 32 36 if (error) { 33 37 return ( 34 - <div className="text-center py-8"> 35 - <div className="text-ctp-red mb-2">Error loading vacancies</div> 36 - <div className="text-sm text-ctp-subtext0">{error.message}</div> 37 - <Button onClick={() => refetch()} className="mt-4"> 38 - Try Again 39 - </Button> 38 + <div className="space-y-6"> 39 + <PageHeader 40 + title="Job Vacancies" 41 + description="Manage your job vacancies and track applications" 42 + /> 43 + <Placeholder 44 + variant="error" 45 + message={(error as Error)?.message || "Error loading vacancies"} 46 + > 47 + <Button onClick={() => refetch()} className="mt-4"> 48 + Try Again 49 + </Button> 50 + </Placeholder> 40 51 </div> 41 52 ); 42 53 } 43 54 44 - const vacancies = data?.myVacancies || []; 55 + const vacancies = ( 56 + data?.me?.vacancies?.edges?.map((edge) => edge.node) || [] 57 + ).filter((v) => v.company?.id && v.company?.name); 45 58 46 59 return ( 47 60 <div className="space-y-6"> 48 - <div className="flex items-center justify-between"> 49 - <div> 50 - <h1 className="text-2xl font-bold text-ctp-text">Job Vacancies</h1> 51 - <p className="text-ctp-subtext0 mt-1"> 52 - Manage your job vacancies and track applications 53 - </p> 54 - </div> 55 - 56 - <Button onClick={() => navigate("/vacancies/create")}> 57 - Create Vacancy 58 - </Button> 59 - </div> 61 + <PageHeader 62 + title="Job Vacancies" 63 + description="Manage your job vacancies and track applications" 64 + action={ 65 + <Button onClick={() => navigate("/vacancies/create")}> 66 + Create Vacancy 67 + </Button> 68 + } 69 + /> 60 70 61 71 <div> 62 72 <h2 className="text-lg font-semibold text-ctp-text mb-4"> 63 73 Your Vacancies ({vacancies.length}) 64 74 </h2> 65 - <VacancyList vacancies={vacancies} onDelete={handleDelete} /> 75 + <VacancyList 76 + vacancies={vacancies 77 + .map((v) => { 78 + const company = v.company; 79 + const hasCompany = company?.id && company?.name; 80 + if (!hasCompany) { 81 + return null; 82 + } 83 + return { 84 + id: v.id, 85 + title: v.title, 86 + company: { 87 + id: company.id, 88 + name: company.name, 89 + }, 90 + description: v.description, 91 + requirements: v.requirements, 92 + location: v.location, 93 + minSalary: v.minSalary, 94 + maxSalary: v.maxSalary, 95 + jobType: null, 96 + applicationUrl: v.applicationUrl, 97 + deadline: v.deadline, 98 + isActive: v.isActive, 99 + createdAt: v.createdAt, 100 + }; 101 + }) 102 + .filter((v): v is NonNullable<typeof v> => v !== null)} 103 + onDelete={handleDelete} 104 + /> 66 105 </div> 67 106 </div> 68 107 );
+22 -2
apps/client/src/router/AppRouter.tsx
··· 1 1 import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; 2 2 import { useToken } from "@/contexts/TokenProvider"; 3 3 import { LoginForm, RegisterForm } from "@/features/auth/components"; 4 - import AuthenticatedLayout from "@/layouts/AuthenticatedLayout"; 4 + import { AuthenticatedLayout } from "@/layouts/AuthenticatedLayout"; 5 + import ApplicationFlowPage from "@/pages/ApplicationFlowPage"; 6 + import ApplicationsPage from "@/pages/ApplicationsPage"; 7 + import { CreateCVPage } from "@/pages/CreateCVPage"; 8 + import CreateEducationPage from "@/pages/CreateEducationPage"; 5 9 import CreateJobExperiencePage from "@/pages/CreateJobExperiencePage"; 6 10 import CreateVacancyPage from "@/pages/CreateVacancyPage"; 11 + import { CVsPage } from "@/pages/CVsPage"; 12 + import { CVViewPage } from "@/pages/CVViewPage"; 7 13 import DashboardPage from "@/pages/DashboardPage"; 14 + import EducationPage from "@/pages/EducationPage"; 8 15 import JobExperiencePage from "@/pages/JobExperiencePage"; 9 16 import OrganizationsPage from "@/pages/OrganizationsPage"; 10 17 import ProfilePage from "@/pages/ProfilePage"; ··· 24 31 } 25 32 26 33 return ( 27 - <BrowserRouter> 34 + <BrowserRouter 35 + future={{ 36 + v7_startTransition: true, 37 + v7_relativeSplatPath: true, 38 + }} 39 + > 28 40 <Routes> 29 41 {/* Public routes */} 30 42 <Route ··· 59 71 path="job-experience/create" 60 72 element={<CreateJobExperiencePage />} 61 73 /> 74 + <Route path="education" element={<EducationPage />} /> 75 + <Route path="education/create" element={<CreateEducationPage />} /> 62 76 <Route path="organizations" element={<OrganizationsPage />} /> 63 77 <Route path="vacancies" element={<VacanciesPage />} /> 64 78 <Route path="vacancies/create" element={<CreateVacancyPage />} /> 79 + <Route path="cvs" element={<CVsPage />} /> 80 + <Route path="cvs/create" element={<CreateCVPage />} /> 81 + <Route path="cvs/:id" element={<CVViewPage />} /> 82 + <Route path="cvs/:id/edit" element={<CVViewPage />} /> 83 + <Route path="applications" element={<ApplicationsPage />} /> 84 + <Route path="applications/apply" element={<ApplicationFlowPage />} /> 65 85 </Route> 66 86 67 87 {/* Catch all */}