Skip to content

Commit f737139

Browse files
committed
Feat: adds project page implementation
1 parent b6a8b3a commit f737139

File tree

6 files changed

+296
-21
lines changed

6 files changed

+296
-21
lines changed

src/app/projetos/page.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
import { PageFrame } from "@/components/PageFrame";
2+
import { ProjectsGrid } from "@/components/ProjectsGrid";
3+
import { getContent } from "@/utils/contentful";
4+
import { INavItem, IPageHeader, IProject } from "@/utils/interfaces";
5+
import { PROJECTS_QUERY } from "@/utils/queries";
6+
import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
27

38
export const revalidate = 60;
49

510
export default async function Projetos() {
11+
const {
12+
projectCollection,
13+
pageHeaderCollection: headers,
14+
navItemsCollection: navItems,
15+
workingFieldsCollection: workingFields,
16+
}: {
17+
projectCollection: { items: IProject[] };
18+
navItemsCollection: { items: INavItem[] };
19+
pageHeaderCollection: { items: IPageHeader[] };
20+
workingFieldsCollection: { items: { name: string }[] };
21+
} = await getContent(PROJECTS_QUERY);
22+
23+
const { title, text } = headers.items[0];
24+
625
return (
726
<PageFrame>
8-
<p>oii</p>
27+
<div className="flex flex-col gap-3 py-4 px-4">
28+
<div
29+
className="flex flex-col gap-2"
30+
style={{ color: navItems.items[0].color }}
31+
>
32+
<h1 className="text-3xl font-bold">{title}</h1>
33+
<article>{documentToReactComponents(text.json)}</article>
34+
</div>
35+
<ProjectsGrid
36+
tags={workingFields.items}
37+
initProjects={projectCollection.items}
38+
/>
39+
</div>
940
</PageFrame>
1041
);
1142
}

src/components/ProfessorsGrid.tsx

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

33
import { IProfessor } from "@/utils/interfaces";
4+
import { getContent } from "@/utils/contentful";
5+
import { PROFESSOR_QUERY } from "@/utils/queries";
46
import React, { useState } from "react";
57
import Professor from "./Professor";
68
import { FilterBar } from "./FilterBar";
@@ -12,26 +14,33 @@ export const ProfessorGrid: React.FC<{
1214
initProfessors: IProfessor[];
1315
}> = ({ tags = [], initProfessors = [] }) => {
1416
const [selectedTags, setSelectedTags] = useState<string[]>([]);
17+
const [professors, setProfessors] = useState<IProfessor[]>(initProfessors);
1518
const [currentPage, setCurrentPage] = useState(0);
19+
const [isLoading, setIsLoading] = useState(false);
1620

17-
const handleUpdate = (tagName: string) => {
18-
setSelectedTags((prev) =>
19-
prev.includes(tagName) ? prev.filter((t) => t !== tagName) : [...prev, tagName],
20-
);
21+
const handleUpdate = async (tagName: string) => {
22+
const next = selectedTags.includes(tagName)
23+
? selectedTags.filter((t) => t !== tagName)
24+
: [...selectedTags, tagName];
25+
setSelectedTags(next);
2126
setCurrentPage(0);
22-
};
2327

24-
const filtered =
25-
selectedTags.length === 0
26-
? initProfessors
27-
: initProfessors.filter((prof) =>
28-
selectedTags.some((tag) =>
29-
prof.workingFieldsCollection.items.some((f) => f.name === tag),
30-
),
31-
);
28+
if (next.length === 0) {
29+
setProfessors(initProfessors);
30+
} else {
31+
setIsLoading(true);
32+
const data = await getContent<{ docentesCollection: { items: IProfessor[] } }>(
33+
PROFESSOR_QUERY,
34+
{ workingField: next },
35+
);
36+
setProfessors(data.docentesCollection.items);
37+
await new Promise((resolve) => setTimeout(resolve, 300));
38+
setIsLoading(false);
39+
}
40+
};
3241

33-
const totalPages = Math.ceil(filtered.length / PAGE_SIZE);
34-
const professors = filtered.slice(currentPage * PAGE_SIZE, (currentPage + 1) * PAGE_SIZE);
42+
const totalPages = Math.ceil(professors.length / PAGE_SIZE);
43+
const paginated = professors.slice(currentPage * PAGE_SIZE, (currentPage + 1) * PAGE_SIZE);
3544

3645
return (
3746
<div className="container">
@@ -41,13 +50,15 @@ export const ProfessorGrid: React.FC<{
4150
onTagSelect={handleUpdate}
4251
/>
4352
<div className="my-6 h-px w-full bg-gray-200" aria-hidden="true" />
44-
{professors.length === 0 ? (
53+
{isLoading ? (
54+
<p className="py-20 text-center text-gray-500">Carregando...</p>
55+
) : professors.length === 0 ? (
4556
<p className="py-20 text-center text-gray-500">
4657
Nenhum professor foi encontrado para os filtros selecionados.
4758
</p>
4859
) : (
4960
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
50-
{professors.map((prof) => (
61+
{paginated.map((prof) => (
5162
<Professor key={prof.name} professor={prof} />
5263
))}
5364
</div>

src/components/ProjectCard.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { IProject } from "@/utils/interfaces";
2+
import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
3+
import { ExternalLink } from "lucide-react";
4+
5+
const getYear = (dateStr: string) => new Date(dateStr).getFullYear();
6+
7+
export const ProjectCard = ({ project }: { project: IProject }) => {
8+
const endLabel = project.endDate ? getYear(project.endDate) : "Atual";
9+
10+
return (
11+
<div className="py-6 border-b border-gray-200 hover:bg-gray-100 rounded-lg p-4 transition duration-300 slast:border-b-0">
12+
<div className="flex items-center justify-between gap-4">
13+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
14+
{project.link ? (
15+
<a
16+
href={project.link}
17+
target="_blank"
18+
rel="noreferrer"
19+
className="font-bold text-gray-900 hover:underline flex items-center gap-1"
20+
>
21+
{project.name}
22+
<ExternalLink size={14} className="shrink-0" />
23+
</a>
24+
) : (
25+
<span className="font-bold text-gray-900">{project.name}</span>
26+
)}
27+
{project.leader && (
28+
<span className="text-gray-600 text-sm">
29+
Coordenado por: <strong>{project.leader.name}</strong>
30+
</span>
31+
)}
32+
</div>
33+
<span className="text-sm text-gray-500 shrink-0">
34+
{getYear(project.initDate)} - {endLabel}
35+
</span>
36+
</div>
37+
38+
<div className="mt-2 text-sm text-gray-700">
39+
{documentToReactComponents(project.description.json)}
40+
</div>
41+
42+
<div className="mt-3 flex flex-wrap items-center justify-between gap-3">
43+
<div className="flex flex-wrap gap-2">
44+
{project.actionFieldsCollection.items.map((field) => (
45+
<span
46+
key={field.name}
47+
className="inline-block rounded-md bg-gray-200 px-3 py-1 text-sm text-gray-700"
48+
>
49+
#{field.name}
50+
</span>
51+
))}
52+
</div>
53+
{(project.graduates != null || project.underGraduates != null) && (
54+
<span className="text-sm text-gray-600 shrink-0">
55+
{project.graduates != null && <><strong>{project.graduates}</strong> Graduados</>}
56+
{project.graduates != null && project.underGraduates != null && ", "}
57+
{project.underGraduates != null && <><strong>{project.underGraduates}</strong> Alunos</>}
58+
</span>
59+
)}
60+
</div>
61+
</div>
62+
);
63+
};

src/components/ProjectsGrid.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use client";
2+
3+
import { IProject } from "@/utils/interfaces";
4+
import { getContent } from "@/utils/contentful";
5+
import { PROJECT_QUERY } from "@/utils/queries";
6+
import React, { useState } from "react";
7+
import { ProjectCard } from "./ProjectCard";
8+
import { FilterBar } from "./FilterBar";
9+
10+
const PAGE_SIZE = 9;
11+
12+
export const ProjectsGrid: React.FC<{
13+
tags: { name: string }[];
14+
initProjects: IProject[];
15+
}> = ({ tags = [], initProjects = [] }) => {
16+
const [selectedTags, setSelectedTags] = useState<string[]>([]);
17+
const [projects, setProjects] = useState<IProject[]>(initProjects);
18+
const [currentPage, setCurrentPage] = useState(0);
19+
const [isLoading, setIsLoading] = useState(false);
20+
21+
const handleUpdate = async (tagName: string) => {
22+
const next = selectedTags.includes(tagName)
23+
? selectedTags.filter((t) => t !== tagName)
24+
: [...selectedTags, tagName];
25+
setSelectedTags(next);
26+
setCurrentPage(0);
27+
28+
if (next.length === 0) {
29+
setProjects(initProjects);
30+
} else {
31+
setIsLoading(true);
32+
const data = await getContent<{ projectCollection: { items: IProject[] } }>(
33+
PROJECT_QUERY,
34+
{ actionField: next },
35+
);
36+
setProjects(data.projectCollection.items);
37+
await new Promise((resolve) => setTimeout(resolve, 300));
38+
setIsLoading(false);
39+
}
40+
};
41+
42+
const totalPages = Math.ceil(projects.length / PAGE_SIZE);
43+
const paginated = projects.slice(currentPage * PAGE_SIZE, (currentPage + 1) * PAGE_SIZE);
44+
45+
return (
46+
<div>
47+
<FilterBar
48+
tags={tags}
49+
selectedTags={selectedTags}
50+
onTagSelect={handleUpdate}
51+
/>
52+
<div className="my-6 h-px w-full bg-gray-200" aria-hidden="true" />
53+
{isLoading ? (
54+
<p className="py-20 text-center text-gray-500">Carregando...</p>
55+
) : paginated.length === 0 ? (
56+
<p className="py-20 text-center text-gray-500">
57+
Nenhum projeto foi encontrado para os filtros selecionados.
58+
</p>
59+
) : (
60+
<div>
61+
{paginated.map((proj) => (
62+
<ProjectCard key={proj.name} project={proj} />
63+
))}
64+
</div>
65+
)}
66+
{totalPages > 1 && (
67+
<div className="mt-8 flex justify-between text-sm text-gray-600">
68+
<button
69+
onClick={() => setCurrentPage((p) => p - 1)}
70+
disabled={currentPage === 0}
71+
className="flex items-center gap-1 disabled:opacity-30 hover:text-gray-900 cursor-pointer transition-colors"
72+
>
73+
← Anterior
74+
</button>
75+
<button
76+
onClick={() => setCurrentPage((p) => p + 1)}
77+
disabled={currentPage >= totalPages - 1}
78+
className="flex items-center gap-1 disabled:opacity-30 hover:text-gray-900 cursor-pointer transition-colors"
79+
>
80+
Próximo →
81+
</button>
82+
</div>
83+
)}
84+
</div>
85+
);
86+
};

src/utils/interfaces.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ export interface IWorkingField {
3434
name: string;
3535
}
3636

37+
export interface IProject {
38+
name: string;
39+
link?: string;
40+
leader: { name: string };
41+
description: { json: any };
42+
actionFieldsCollection: { items: IWorkingField[] };
43+
graduates: number;
44+
underGraduates: number;
45+
initDate: string;
46+
endDate?: string;
47+
}
48+
3749
export interface IProfessor {
3850
name: string;
3951
role: string;

src/utils/queries.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,54 @@ export const PROFESSORS_QUERY = `
9797
}
9898
`;
9999

100+
export const PROJECTS_QUERY = `
101+
query {
102+
projectCollection {
103+
items {
104+
name
105+
link
106+
leader {
107+
name
108+
}
109+
description {
110+
json
111+
}
112+
actionFieldsCollection {
113+
items {
114+
name
115+
}
116+
}
117+
graduates
118+
underGraduates
119+
initDate
120+
endDate
121+
}
122+
}
123+
pageHeaderCollection(where: { id: "projetos" }) {
124+
items {
125+
title
126+
text {
127+
json
128+
}
129+
}
130+
}
131+
navItemsCollection(where: { id: "projetos" }) {
132+
items {
133+
color
134+
}
135+
}
136+
workingFieldsCollection {
137+
items {
138+
name
139+
}
140+
}
141+
}
142+
`;
143+
100144
export const PROFESSOR_QUERY = `
101145
query GetProfessors($workingField: [String]) {
102-
docentesCollection(where: {
103-
workingFields: { name_in: $workingField }
146+
docentesCollection(where: {
147+
workingFields: { name_in: $workingField }
104148
}) {
105149
items {
106150
name
@@ -119,7 +163,35 @@ export const PROFESSOR_QUERY = `
119163
width
120164
height
121165
}
122-
}
166+
}
167+
}
168+
}
169+
`;
170+
171+
export const PROJECT_QUERY = `
172+
query GetProjects($actionField: [String]) {
173+
projectCollection(where: {
174+
actionFields: { name_in: $actionField }
175+
}) {
176+
items {
177+
name
178+
link
179+
leader {
180+
name
181+
}
182+
description {
183+
json
184+
}
185+
actionFieldsCollection {
186+
items {
187+
name
188+
}
189+
}
190+
graduates
191+
underGraduates
192+
initDate
193+
endDate
194+
}
123195
}
124196
}
125197
`;

0 commit comments

Comments
 (0)