33import com .thughari .jobtrackerpro .dto .CareerResourceDTO ;
44import com .thughari .jobtrackerpro .dto .CareerResourcePageResponse ;
55import com .thughari .jobtrackerpro .dto .CreateCareerResourceRequest ;
6+ import com .thughari .jobtrackerpro .dto .UpdateCareerResourceRequest ;
67import com .thughari .jobtrackerpro .entity .CareerResource ;
78import com .thughari .jobtrackerpro .entity .User ;
89import com .thughari .jobtrackerpro .interfaces .StorageService ;
1314import org .springframework .cache .annotation .Cacheable ;
1415import org .springframework .data .domain .PageRequest ;
1516import org .springframework .data .domain .Sort ;
17+ import org .springframework .data .jpa .domain .Specification ;
1618import org .springframework .stereotype .Service ;
1719import org .springframework .transaction .annotation .Transactional ;
1820import org .springframework .web .multipart .MultipartFile ;
1921
22+ import jakarta .persistence .criteria .Predicate ;
23+
24+ import java .util .ArrayList ;
25+ import java .util .Locale ;
26+
2027@ Service
2128@ Transactional
2229public class CareerResourceService {
2330
2431 private static final int MAX_PAGE_SIZE = 50 ;
32+ private static final int DEMO_MIN_RESOURCE_COUNT = 60 ;
2533
2634 private final CareerResourceRepository resourceRepository ;
2735 private final UserRepository userRepository ;
@@ -36,13 +44,21 @@ public CareerResourceService(CareerResourceRepository resourceRepository,
3644 }
3745
3846 @ Transactional (readOnly = true )
39- @ Cacheable (value = "resourcePages" , key = "{#page, #size, #viewerEmail == null ? 'anon' : #viewerEmail}" )
40- public CareerResourcePageResponse getResourcePage (int page , int size , String viewerEmail ) {
47+ @ Cacheable (value = "resourcePages" , key = "{#page, #size, #query == null ? '' : #query, #category == null ? '' : #category, #type == null ? '' : #type, #viewerEmail == null ? 'anon' : #viewerEmail}" )
48+ public CareerResourcePageResponse getResourcePage (int page ,
49+ int size ,
50+ String query ,
51+ String category ,
52+ String type ,
53+ String viewerEmail ) {
4154 int sanitizedPage = Math .max (0 , page );
4255 int sanitizedSize = Math .max (1 , Math .min (size , MAX_PAGE_SIZE ));
56+ String normalizedQuery = normalizeFilter (query );
57+ String normalizedCategory = normalizeFilter (category );
58+ String normalizedType = normalizeType (type );
4359
4460 var pageable = PageRequest .of (sanitizedPage , sanitizedSize , Sort .by (Sort .Direction .DESC , "createdAt" ));
45- var resourcePage = resourceRepository .findAllByOrderByCreatedAtDesc ( pageable );
61+ var resourcePage = resourceRepository .findAll ( buildResourceFilter ( normalizedQuery , normalizedCategory , normalizedType ), pageable );
4662
4763 var content = resourcePage .getContent ()
4864 .stream ()
@@ -59,6 +75,62 @@ public CareerResourcePageResponse getResourcePage(int page, int size, String vie
5975 );
6076 }
6177
78+ private Specification <CareerResource > buildResourceFilter (String query , String category , String type ) {
79+ return (root , criteriaQuery , criteriaBuilder ) -> {
80+ var predicates = new ArrayList <Predicate >();
81+
82+ if (query != null ) {
83+ String likeQuery = "%" + query .toLowerCase (Locale .ROOT ) + "%" ;
84+ predicates .add (criteriaBuilder .or (
85+ criteriaBuilder .like (criteriaBuilder .lower (root .get ("title" )), likeQuery ),
86+ criteriaBuilder .like (criteriaBuilder .lower (root .get ("category" )), likeQuery ),
87+ criteriaBuilder .like (criteriaBuilder .lower (root .get ("description" )), likeQuery ),
88+ criteriaBuilder .like (criteriaBuilder .lower (root .get ("submittedByName" )), likeQuery )
89+ ));
90+ }
91+
92+ if (category != null ) {
93+ String likeCategory = "%" + category .toLowerCase (Locale .ROOT ) + "%" ;
94+ predicates .add (criteriaBuilder .like (criteriaBuilder .lower (root .get ("category" )), likeCategory ));
95+ }
96+
97+ if (type != null ) {
98+ predicates .add (criteriaBuilder .equal (criteriaBuilder .upper (root .get ("resourceType" )), type ));
99+ }
100+
101+ return predicates .isEmpty ()
102+ ? criteriaBuilder .conjunction ()
103+ : criteriaBuilder .and (predicates .toArray (new Predicate [0 ]));
104+ };
105+ }
106+
107+ private String normalizeFilter (String value ) {
108+ if (value == null ) {
109+ return null ;
110+ }
111+
112+ String trimmed = value .trim ();
113+ if (trimmed .isEmpty () || "all" .equalsIgnoreCase (trimmed )) {
114+ return null ;
115+ }
116+
117+ return trimmed ;
118+ }
119+
120+ private String normalizeType (String value ) {
121+ String normalized = normalizeFilter (value );
122+ if (normalized == null ) {
123+ return null ;
124+ }
125+
126+ String upper = normalized .toUpperCase (Locale .ROOT );
127+ if (!upper .equals ("LINK" ) && !upper .equals ("FILE" )) {
128+ return null ;
129+ }
130+
131+ return upper ;
132+ }
133+
62134 @ CacheEvict (value = "resourcePages" , allEntries = true )
63135 public CareerResourceDTO createResource (String email , CreateCareerResourceRequest request ) {
64136 String normalizedUrl = normalizeUrl (request .getUrl ());
@@ -120,17 +192,79 @@ public void deleteResource(String email, java.util.UUID resourceId) {
120192 resourceRepository .delete (resource );
121193 }
122194
123- @ PostConstruct
124- public void seedStarterResources () {
125- if (resourceRepository .count () > 0 ) {
126- return ;
195+ @ Transactional (readOnly = true )
196+ public java .util .List <CareerResourceDTO > getMyResources (String email ) {
197+ return resourceRepository .findAllBySubmittedByEmailOrderByCreatedAtDesc (email )
198+ .stream ()
199+ .map (resource -> toDTO (resource , email ))
200+ .toList ();
201+ }
202+
203+ @ CacheEvict (value = "resourcePages" , allEntries = true )
204+ public CareerResourceDTO updateResource (String email , java .util .UUID resourceId , UpdateCareerResourceRequest request ) {
205+ CareerResource resource = resourceRepository .findById (resourceId )
206+ .orElseThrow (() -> new IllegalArgumentException ("Resource not found" ));
207+
208+ if (!resource .getSubmittedByEmail ().equalsIgnoreCase (email )) {
209+ throw new IllegalArgumentException ("You can only edit resources you added" );
210+ }
211+
212+ validateCommonFields (request .getTitle (), request .getCategory ());
213+
214+ resource .setTitle (request .getTitle ().trim ());
215+ resource .setCategory (request .getCategory ().trim ());
216+ resource .setDescription (request .getDescription () == null ? null : request .getDescription ().trim ());
217+
218+ if ("LINK" .equalsIgnoreCase (resource .getResourceType ())) {
219+ String normalizedUrl = normalizeUrl (request .getUrl ());
220+ if (normalizedUrl == null || normalizedUrl .isBlank ()) {
221+ throw new IllegalArgumentException ("Valid URL is required for link resources" );
222+ }
223+ resource .setUrl (normalizedUrl );
127224 }
128225
226+ return toDTO (resourceRepository .save (resource ), email );
227+ }
228+
229+ @ PostConstruct
230+ public void seedStarterResources () {
129231 addSeedResource ("Career Preparation Notes" , "https://docs.google.com/document/d/1-25JrPUai6P7pjKk1g7mELpI0YUINS_NpjUk7lsMfyg/edit?tab=t.0#heading=h.kf8l3f8jftc2" , "Guides & Study Docs" );
130232 addSeedResource ("Main Career Resources Folder" , "https://drive.google.com/drive/folders/1ISp9GBv7ih1blEQOPYplD_idG1j0ibGq?usp=sharing" , "Drive Folders & File Packs" );
131233 addSeedResource ("DSA Folder" , "https://drive.google.com/drive/folders/1ei52Zc_cQe0rJK404M56BmEisUjnF6kN?usp=drive_link" , "Drive Folders & File Packs" );
132234 addSeedResource ("21 Days React Study Plan" , "https://thecodedose.notion.site/21-Days-React-Study-Plan-1988ff023cae48459bae8cb20cb75a67" , "Structured Learning" );
133235 addSeedResource ("Opportunity Tracker Sheet" , "https://docs.google.com/spreadsheets/d/1KBFiqJTaFY1164XtglKvn2vAofScCfGlkY-n54D2d14/edit?gid=584790886#gid=584790886" , "Trackers & Opportunity Sheets" );
236+
237+ seedDemoVolumeResources ();
238+ }
239+
240+ private void seedDemoVolumeResources () {
241+ long currentCount = resourceRepository .count ();
242+ if (currentCount >= DEMO_MIN_RESOURCE_COUNT ) {
243+ return ;
244+ }
245+
246+ String [][] templates = {
247+ {"React Interview Drill" , "https://example.com/resources/react-interview-drill" , "Interview Prep" },
248+ {"DSA Patterns Workbook" , "https://example.com/resources/dsa-patterns-workbook" , "DSA" },
249+ {"System Design Primer" , "https://example.com/resources/system-design-primer" , "System Design" },
250+ {"Resume Bullet Bank" , "https://example.com/resources/resume-bullet-bank" , "Resume" },
251+ {"Backend Fundamentals Roadmap" , "https://example.com/resources/backend-fundamentals-roadmap" , "Roadmaps" },
252+ {"Frontend Fundamentals Roadmap" , "https://example.com/resources/frontend-fundamentals-roadmap" , "Roadmaps" },
253+ {"Mock Interview Checklist" , "https://example.com/resources/mock-interview-checklist" , "Mock Interviews" },
254+ {"Job Board Tracker" , "https://example.com/resources/job-board-tracker" , "Job Boards" },
255+ {"Behavioral Question Matrix" , "https://example.com/resources/behavioral-question-matrix" , "Interview Prep" },
256+ {"Portfolio Project Ideas" , "https://example.com/resources/portfolio-project-ideas" , "Portfolio" }
257+ };
258+
259+ long resourcesToAdd = DEMO_MIN_RESOURCE_COUNT - currentCount ;
260+ for (int i = 0 ; i < resourcesToAdd ; i ++) {
261+ String [] template = templates [i % templates .length ];
262+ String title = template [0 ] + " #" + (i + 1 );
263+ String url = template [1 ] + "?v=" + (i + 1 );
264+ String category = template [2 ];
265+
266+ addSeedResource (title , url , category );
267+ }
134268 }
135269
136270 private void addSeedResource (String title , String url , String category ) {
0 commit comments