Skip to content

Commit e8ef3cf

Browse files
authored
Merge pull request #73 from thughari/dev
career hub
2 parents 1c6c9e9 + da780f9 commit e8ef3cf

14 files changed

Lines changed: 722 additions & 270 deletions

File tree

backend/src/main/java/com/thughari/jobtrackerpro/controller/CareerResourceController.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ public ResponseEntity<CareerResourcePageResponse> getResources(
4040
@RequestParam(defaultValue = "20") int size,
4141
@RequestParam(required = false) String query,
4242
@RequestParam(required = false) String category,
43-
@RequestParam(required = false) String type
43+
@RequestParam(required = false) String type,
44+
@RequestParam(required = false) String location,
45+
@RequestParam(required = false) String listingType
4446
) {
45-
return ResponseEntity.ok(careerResourceService.getResourcePage(page, size, query, category, type, getAuthenticatedEmailOrNull()));
47+
return ResponseEntity.ok(careerResourceService.getResourcePage(page, size, query, category, type, location, listingType, getAuthenticatedEmailOrNull()));
4648
}
4749

4850

4951
@GetMapping("/categories")
50-
public ResponseEntity<List<String>> getCategories() {
51-
return ResponseEntity.ok(careerResourceService.getAllCategories());
52+
public ResponseEntity<List<String>> getCategories(@RequestParam(required = false) String listingType) {
53+
return ResponseEntity.ok(careerResourceService.getAllCategories(listingType));
5254
}
5355

5456
@PostMapping
@@ -75,10 +77,23 @@ public ResponseEntity<CareerResourceDTO> uploadResource(
7577
@RequestParam String title,
7678
@RequestParam String category,
7779
@RequestParam(required = false) String description,
80+
@RequestParam(required = false) String location,
81+
@RequestParam(required = false) String company,
82+
@RequestParam(required = false) String listingType,
83+
@RequestParam(required = false) String eventDate,
7884
@RequestParam MultipartFile file
7985
) {
8086
String email = getAuthenticatedEmail();
81-
return ResponseEntity.ok(careerResourceService.createResourceFromFile(email, title, category, description, file));
87+
CreateCareerResourceRequest request = new CreateCareerResourceRequest();
88+
request.setTitle(title);
89+
request.setCategory(category);
90+
request.setDescription(description);
91+
request.setLocation(location);
92+
request.setCompany(company);
93+
request.setListingType(listingType);
94+
request.setEventDate(eventDate);
95+
96+
return ResponseEntity.ok(careerResourceService.createResourceFromFile(email, request, file));
8297
}
8398

8499
@DeleteMapping("/{id}")

backend/src/main/java/com/thughari/jobtrackerpro/dto/CareerResourceDTO.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ public class CareerResourceDTO {
1818
private boolean ownedByCurrentUser;
1919
private String submittedByName;
2020
private LocalDateTime createdAt;
21+
private String location;
22+
private String company;
23+
private LocalDateTime eventDate;
24+
private String listingType;
2125
}

backend/src/main/java/com/thughari/jobtrackerpro/dto/CreateCareerResourceRequest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ public class CreateCareerResourceRequest {
88
private String url;
99
private String category;
1010
private String description;
11+
private String location;
12+
private String company;
13+
private String eventDate; // Use String for flexibility in parsing if needed, or LocalDateTime
14+
private String listingType;
1115
}

backend/src/main/java/com/thughari/jobtrackerpro/dto/UpdateCareerResourceRequest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ public class UpdateCareerResourceRequest {
88
private String url;
99
private String category;
1010
private String description;
11+
private String location;
12+
private String company;
13+
private String eventDate;
14+
private String listingType;
1115
}

backend/src/main/java/com/thughari/jobtrackerpro/entity/CareerResource.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ public class CareerResource {
5151
@Column(nullable = false)
5252
private LocalDateTime createdAt;
5353

54+
@Column(length = 100)
55+
private String location;
56+
57+
@Column(length = 100)
58+
private String company;
59+
60+
private LocalDateTime eventDate;
61+
62+
@Column(nullable = false, length = 20, columnDefinition = "varchar(20) default 'RESOURCE'")
63+
private String listingType = "RESOURCE";
64+
5465
@PrePersist
5566
public void prePersist() {
5667
if (createdAt == null) {

backend/src/main/java/com/thughari/jobtrackerpro/repo/CareerResourceRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@ public interface CareerResourceRepository extends JpaRepository<CareerResource,
2020
@Query("select distinct r.category from CareerResource r where r.category is not null and trim(r.category) <> '' order by r.category asc")
2121
List<String> findDistinctCategories();
2222

23+
@Query("select distinct r.category from CareerResource r where upper(r.listingType) = upper(:listingType) and r.category is not null and trim(r.category) <> '' order by r.category asc")
24+
List<String> findDistinctCategoriesByListingType(String listingType);
25+
2326
boolean existsByUrl(String url);
2427
}

backend/src/main/java/com/thughari/jobtrackerpro/service/CareerResourceService.java

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,22 @@ public CareerResourceService(CareerResourceRepository resourceRepository,
4444

4545
@Transactional(readOnly = true)
4646
@Cacheable(value = "resourcePages",
47-
key = "{#page, #size, #query ?: '', #category ?: '', #type ?: '', #viewerEmail ?: 'anon'}")
47+
key = "{#page, #size, #query ?: '', #category ?: '', #type ?: '', #location ?: '', #listingType ?: '', #viewerEmail ?: 'anon'}")
4848
public CareerResourcePageResponse getResourcePage(int page, int size, String query,
49-
String category, String type, String viewerEmail) {
49+
String category, String type,
50+
String location, String listingType,
51+
String viewerEmail) {
5052
int sanitizedPage = Math.max(0, page);
5153
int sanitizedSize = Math.max(1, Math.min(size, MAX_PAGE_SIZE));
5254

5355
String normalizedQuery = normalizeFilter(query);
5456
String normalizedCategory = normalizeFilter(category);
5557
String normalizedType = normalizeType(type);
58+
String normalizedLocation = normalizeFilter(location);
59+
String normalizedListingType = normalizeFilter(listingType);
5660

57-
var pageable = PageRequest.of(sanitizedPage, sanitizedSize, Sort.by(Sort.Order.asc("category"), Sort.Order.asc("title")));
58-
var resourcePage = resourceRepository.findAll(buildResourceFilter(normalizedQuery, normalizedCategory, normalizedType), pageable);
61+
var pageable = PageRequest.of(sanitizedPage, sanitizedSize, Sort.by(Sort.Order.desc("createdAt")));
62+
var resourcePage = resourceRepository.findAll(buildExploreFilter(normalizedQuery, normalizedCategory, normalizedType, normalizedLocation, normalizedListingType), pageable);
5963

6064
var content = resourcePage.getContent().stream()
6165
.map(resource -> toDTO(resource, viewerEmail))
@@ -68,9 +72,12 @@ public CareerResourcePageResponse getResourcePage(int page, int size, String que
6872
}
6973

7074
@Transactional(readOnly = true)
71-
@Cacheable(value = "resourceCategories")
72-
public List<String> getAllCategories() {
73-
return resourceRepository.findDistinctCategories();
75+
@Cacheable(value = "resourceCategories", key = "#listingType ?: 'ALL'")
76+
public List<String> getAllCategories(String listingType) {
77+
if (listingType == null || listingType.isBlank() || "ALL".equalsIgnoreCase(listingType)) {
78+
return resourceRepository.findDistinctCategories();
79+
}
80+
return resourceRepository.findDistinctCategoriesByListingType(listingType.trim().toUpperCase(Locale.ROOT));
7481
}
7582

7683
@Transactional(readOnly = true)
@@ -100,7 +107,7 @@ public CareerResourceDTO createResource(String email, CreateCareerResourceReques
100107
resource.setUrl(normalizedUrl);
101108
resource.setCategory(request.getCategory().trim());
102109
resource.setDescription(request.getDescription() == null ? null : request.getDescription().trim());
103-
resource.setResourceType("LINK");
110+
applyMetadata(resource, request.getLocation(), request.getCompany(), request.getEventDate(), request.getListingType());
104111
applySubmitter(resource, user);
105112

106113
return toDTO(resourceRepository.save(resource), email);
@@ -111,22 +118,22 @@ public CareerResourceDTO createResource(String email, CreateCareerResourceReques
111118
@CacheEvict(value = "resourceCategories", allEntries = true),
112119
@CacheEvict(value = "userResources", key = "#email")
113120
})
114-
public CareerResourceDTO createResourceFromFile(String email, String title, String category,
115-
String description, MultipartFile file) {
121+
public CareerResourceDTO createResourceFromFile(String email, CreateCareerResourceRequest request, MultipartFile file) {
116122
if (file == null || file.isEmpty()) throw new IllegalArgumentException("File required");
117-
validateCommonFields(title, category);
118-
123+
validateCommonFields(request.getTitle(), request.getCategory());
124+
119125
User user = getUser(email);
120126
String fileUrl = storageService.uploadResourceFile(file, user.getId().toString());
121127

122128
CareerResource resource = new CareerResource();
123-
resource.setTitle(title.trim());
124-
resource.setCategory(category.trim());
125-
resource.setDescription(description == null ? null : description.trim());
129+
resource.setTitle(request.getTitle().trim());
130+
resource.setCategory(request.getCategory().trim());
131+
resource.setDescription(request.getDescription() == null ? null : request.getDescription().trim());
126132
resource.setUrl(fileUrl);
127133
resource.setResourceType("FILE");
128134
resource.setOriginalFileName(file.getOriginalFilename());
129135
resource.setFileSizeBytes(file.getSize());
136+
applyMetadata(resource, request.getLocation(), request.getCompany(), request.getEventDate(), request.getListingType());
130137
applySubmitter(resource, user);
131138

132139
return toDTO(resourceRepository.save(resource), email);
@@ -173,10 +180,12 @@ public CareerResourceDTO updateResource(String email, UUID resourceId, UpdateCar
173180
resource.setUrl(normalizeUrl(request.getUrl()));
174181
}
175182

183+
applyMetadata(resource, request.getLocation(), request.getCompany(), request.getEventDate(), request.getListingType());
184+
176185
return toDTO(resourceRepository.save(resource), email);
177186
}
178187

179-
private Specification<CareerResource> buildResourceFilter(String query, String category, String type) {
188+
private Specification<CareerResource> buildExploreFilter(String query, String category, String type, String location, String listingType) {
180189
return (root, criteriaQuery, criteriaBuilder) -> {
181190
var predicates = new ArrayList<Predicate>();
182191
if (query != null) {
@@ -185,6 +194,8 @@ private Specification<CareerResource> buildResourceFilter(String query, String c
185194
criteriaBuilder.like(criteriaBuilder.lower(root.get("title")), likeQuery),
186195
criteriaBuilder.like(criteriaBuilder.lower(root.get("category")), likeQuery),
187196
criteriaBuilder.like(criteriaBuilder.lower(root.get("description")), likeQuery),
197+
criteriaBuilder.like(criteriaBuilder.lower(root.get("location")), likeQuery),
198+
criteriaBuilder.like(criteriaBuilder.lower(root.get("company")), likeQuery),
188199
criteriaBuilder.like(criteriaBuilder.lower(root.get("submittedByName")), likeQuery)
189200
));
190201
}
@@ -194,6 +205,12 @@ private Specification<CareerResource> buildResourceFilter(String query, String c
194205
if (type != null) {
195206
predicates.add(criteriaBuilder.equal(criteriaBuilder.upper(root.get("resourceType")), type));
196207
}
208+
if (location != null) {
209+
predicates.add(criteriaBuilder.equal(criteriaBuilder.lower(root.get("location")), location.toLowerCase(Locale.ROOT)));
210+
}
211+
if (listingType != null) {
212+
predicates.add(criteriaBuilder.equal(criteriaBuilder.upper(root.get("listingType")), listingType.toUpperCase(Locale.ROOT)));
213+
}
197214
return predicates.isEmpty() ? criteriaBuilder.conjunction() : criteriaBuilder.and(predicates.toArray(new Predicate[0]));
198215
};
199216
}
@@ -254,6 +271,24 @@ private CareerResourceDTO toDTO(CareerResource resource, String viewerEmail) {
254271
dto.setOwnedByCurrentUser(viewerEmail != null && resource.getSubmittedByEmail().equalsIgnoreCase(viewerEmail));
255272
dto.setSubmittedByName(resource.getSubmittedByName());
256273
dto.setCreatedAt(resource.getCreatedAt());
274+
dto.setLocation(resource.getLocation());
275+
dto.setCompany(resource.getCompany());
276+
dto.setEventDate(resource.getEventDate());
277+
dto.setListingType(resource.getListingType());
257278
return dto;
258279
}
280+
281+
private void applyMetadata(CareerResource resource, String location, String company, String eventDate, String listingType) {
282+
if (location != null) resource.setLocation(location.trim());
283+
if (company != null) resource.setCompany(company.trim());
284+
if (listingType != null) resource.setListingType(listingType.trim().toUpperCase(Locale.ROOT));
285+
286+
if (eventDate != null && !eventDate.isBlank()) {
287+
try {
288+
resource.setEventDate(java.time.LocalDateTime.parse(eventDate));
289+
} catch (Exception e) {
290+
// Ignore parsing errors for now or handle gracefully
291+
}
292+
}
293+
}
259294
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
spring.application.name=JobTrackerPro
2-
spring.profiles.active=${SPRING_PROFILES_ACTIVE:local}
2+
spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev}

backend/src/test/java/com/thughari/jobtrackerpro/service/CareerResourceServiceTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void getResourcePageSanitizesInput() {
4545
return new PageImpl<>(List.of(), pageable, 0);
4646
});
4747

48-
var response = service.getResourcePage(-1, 1000, " all ", " all ", "bad", null);
48+
var response = service.getResourcePage(-1, 1000, " all ", " all ", "bad", null, null, null);
4949

5050
assertEquals(0, response.getPage());
5151
assertEquals(50, response.getSize());
@@ -77,7 +77,12 @@ void createResourceFromFileUploadsAndStoresMetadata() {
7777
when(storageService.uploadResourceFile(multipartFile, user.getId().toString())).thenReturn("https://cdn/file.pdf");
7878
when(resourceRepository.save(any(CareerResource.class))).thenAnswer(inv -> inv.getArgument(0));
7979

80-
var dto = service.createResourceFromFile("u@example.com", " Title ", " Prep ", " Desc ", multipartFile);
80+
CreateCareerResourceRequest req = new CreateCareerResourceRequest();
81+
req.setTitle(" Title ");
82+
req.setCategory(" Prep ");
83+
req.setDescription(" Desc ");
84+
85+
var dto = service.createResourceFromFile("u@example.com", req, multipartFile);
8186

8287
assertEquals("FILE", dto.getResourceType());
8388
assertEquals("guide.pdf", dto.getOriginalFileName());

frontend/src/app/app.routes.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,56 +17,56 @@ import { TocComponent } from './components/toc/toc.component';
1717
import { VerifyComponent } from './components/auth/verify/verify.component';
1818

1919
export const routes: Routes = [
20-
{
21-
path: '',
20+
{
21+
path: '',
2222
component: LandingComponent,
2323
title: 'JobTrackerPro - Automate Your Job Hunt'
2424
},
25-
{
26-
path: 'about',
25+
{
26+
path: 'about',
2727
component: AboutComponent,
2828
title: 'About JobTrackerPro - Architecture & Tech Stack'
2929
},
30-
{
31-
path: 'privacy',
32-
component: PrivacyComponent,
30+
{
31+
path: 'privacy',
32+
component: PrivacyComponent,
3333
title: 'Privacy Policy'
3434
},
35-
{
36-
path: 'terms',
35+
{
36+
path: 'terms',
3737
component: TocComponent,
3838
title: 'Terms of Service - JobTrackerPro'
3939
},
40-
{
41-
path: 'resources',
40+
{
41+
path: 'resources',
4242
component: ResourcesComponent,
43-
title: 'Career Resources - JobTrackerPro'
43+
title: 'Explore Community Hub - JobTrackerPro'
4444
},
45-
{
46-
path: 'reset-password',
45+
{
46+
path: 'reset-password',
4747
component: ResetPasswordComponent
4848
},
49-
{
50-
path: 'login',
51-
component: LoginComponent,
52-
canActivate: [guestGuard]
49+
{
50+
path: 'login',
51+
component: LoginComponent,
52+
canActivate: [guestGuard]
5353
},
54-
{
55-
path: 'signup',
56-
component: SignupComponent,
57-
canActivate: [guestGuard]
54+
{
55+
path: 'signup',
56+
component: SignupComponent,
57+
canActivate: [guestGuard]
5858
},
59-
{
60-
path: 'login-success',
61-
component: LoginSuccessComponent,
62-
canActivate: [guestGuard]
59+
{
60+
path: 'login-success',
61+
component: LoginSuccessComponent,
62+
canActivate: [guestGuard]
6363
},
64-
{
65-
path: 'verify-email',
64+
{
65+
path: 'verify-email',
6666
component: VerifyComponent,
6767
title: 'Verify Account | JobTrackerPro'
6868
},
69-
69+
7070
{
7171
path: 'app',
7272
canActivate: [authGuard],

0 commit comments

Comments
 (0)