Skip to content

Commit b2e05ed

Browse files
authored
[Feature] Add advertisement click functionality to ads-java service (#112)
This pull request introduces changes to the ad management system, including backend enhancements for handling ad clicks, frontend updates for improved ad display and interactivity, and the addition of ad pages for "Cool Hats", "Discount Clothing", and "Nice Bags". Below is a summary of the most important changes: ### Backend Enhancements for Ad Management: * Added a new `clickUrl` field to the `Advertisement` entity and updated the `Advertisement` class to include a new constructor and getter/setter methods for this field. This allows ads to have specific URLs for redirection. [[1]](diffhunk://#diff-983021bedc2f12a394677367bd55d4d052dc282657421cccb78303d8a8ceebe8R12) [[2]](diffhunk://#diff-983021bedc2f12a394677367bd55d4d052dc282657421cccb78303d8a8ceebe8R21-R34) * Implemented a new `/click/{id}` endpoint in the `AdsJavaApplication` class to handle ad clicks. This endpoint logs the click, retrieves the ad by its ID, and redirects the user to the associated `clickUrl` or a default URL if none is set. * Enhanced the database initialization logic to include meaningful `clickUrl` values for ads and ensure existing ads are updated with the correct URLs. ### Frontend Updates for Ad Interactivity: * Updated the `Ad` component in `Ad.tsx` to include a `handleAdClick` function that redirects users to the backend click endpoint when an ad is clicked. The ad banner now includes a `cursor-pointer` style and a `title` attribute for better user experience. [[1]](diffhunk://#diff-a8bb7ae2ac5ae758f047db6aaf17c2826457375226c6387413b27a1a6f26e358R49-R56) [[2]](diffhunk://#diff-a8bb7ae2ac5ae758f047db6aaf17c2826457375226c6387413b27a1a6f26e358L64-R86) * Refactored the `Ad` component to use a proper TypeScript interface (`Advertisement`) for better type safety and clarity. * Added pages for "Cool Hats", "Discount Clothing", and "Nice Bags". ## How to test 1. Checkout this branch. 2. Run `docker compose -f docker-compose.dev.yml up -d` 3. Navigate to `http://localhost` and click on the ads to ensure all three ads work as expected
2 parents 8553c83 + 82e909b commit b2e05ed

File tree

8 files changed

+523
-23
lines changed

8 files changed

+523
-23
lines changed

.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,4 @@ DISABLE_SPRING=1
7373
STOREDOG_URL=http://service-proxy:80 # base url for storedog service (default: 'http://service-proxy:80')
7474
PUPPETEER_TIMEOUT=30000 # timeout for puppeteer (default: 30000)
7575

76-
SKIP_SESSION_CLOSE= # skip session close for puppeteer (default: ''). Note that the current puppeteer script doesn't make use of this environment variable but can easily be updated to do so
76+
SKIP_SESSION_CLOSE= # skip session close for puppeteer (default: ''). Note that the current puppeteer script doesn't make use of this environment variable but can easily be updated to do so

services/ads/java/src/main/java/adsjava/AdsJavaApplication.java

Lines changed: 113 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
import org.springframework.web.bind.annotation.RequestMapping;
66
import org.springframework.web.bind.annotation.RestController;
77
import org.springframework.web.bind.annotation.CrossOrigin;
8+
import org.springframework.web.bind.annotation.PathVariable;
89
import org.springframework.http.MediaType;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.ResponseEntity;
912
import org.springframework.util.StreamUtils;
1013
import java.io.*;
14+
import java.net.URI;
1115
import org.springframework.web.bind.annotation.ResponseBody;
1216
import org.apache.commons.io.IOUtils;
1317
import java.util.concurrent.ThreadLocalRandom;
1418
import java.util.HashMap;
19+
import java.util.Optional;
1520
import org.springframework.web.bind.annotation.RequestParam;
1621
import java.util.concurrent.TimeoutException;
1722
import org.springframework.web.bind.annotation.RequestHeader;
@@ -44,15 +49,67 @@ public String home() {
4449
value = "/banners/{id}",
4550
produces = MediaType.IMAGE_JPEG_VALUE
4651
)
47-
public @ResponseBody byte[] getImageWithMediaType() throws IOException {
48-
logger.info("/banners/{id} called");
49-
int randomNum = ThreadLocalRandom.current().nextInt(1, 3 + 1);
50-
String imagePath = "/static/ads/ad" + randomNum + ".jpg";
51-
InputStream in = getClass()
52-
.getResourceAsStream(imagePath);
52+
public @ResponseBody byte[] getImageWithMediaType(@PathVariable String id) throws IOException {
53+
logger.info("/banners/{} called", id);
54+
55+
// Map the image path to the correct static file
56+
String imagePath;
57+
switch (id) {
58+
case "1.jpg":
59+
imagePath = "/static/ads/ad1.jpg";
60+
break;
61+
case "2.jpg":
62+
imagePath = "/static/ads/ad2.jpg";
63+
break;
64+
case "3.jpg":
65+
imagePath = "/static/ads/ad3.jpg";
66+
break;
67+
default:
68+
// Fallback to random image if unknown
69+
int randomNum = ThreadLocalRandom.current().nextInt(1, 3 + 1);
70+
imagePath = "/static/ads/ad" + randomNum + ".jpg";
71+
logger.warn("Unknown image id: {}, using random image", id);
72+
}
73+
74+
InputStream in = getClass().getResourceAsStream(imagePath);
75+
if (in == null) {
76+
logger.error("Image not found: {}", imagePath);
77+
throw new IOException("Image not found: " + imagePath);
78+
}
5379
return IOUtils.toByteArray(in);
5480
}
5581

82+
@CrossOrigin(origins = {"*"})
83+
@RequestMapping("/click/{id}")
84+
public ResponseEntity<Void> handleAdClick(@PathVariable Long id) {
85+
logger.info("Ad click for id: " + id);
86+
87+
Optional<Advertisement> adOptional = advertisementRepository.findById(id);
88+
if (adOptional.isPresent()) {
89+
Advertisement ad = adOptional.get();
90+
String clickUrl = ad.getClickUrl();
91+
92+
// Log the click for analytics
93+
logger.info("Redirecting ad '{}' (id: {}) to: {}", ad.getName(), id, clickUrl);
94+
95+
if (clickUrl != null && !clickUrl.isEmpty()) {
96+
// Return a redirect response to the click URL
97+
return ResponseEntity.status(HttpStatus.FOUND)
98+
.location(URI.create(clickUrl))
99+
.build();
100+
} else {
101+
// Default redirect if no clickUrl is set
102+
logger.warn("No clickUrl set for ad id: " + id + ", redirecting to homepage");
103+
return ResponseEntity.status(HttpStatus.FOUND)
104+
.location(URI.create("/"))
105+
.build();
106+
}
107+
} else {
108+
logger.error("Ad not found for id: " + id);
109+
return ResponseEntity.notFound().build();
110+
}
111+
}
112+
56113
@CrossOrigin(origins = {"*"})
57114
@RequestMapping(
58115
value = "/ads",
@@ -95,9 +152,56 @@ public static void main(String[] args) {
95152
public CommandLineRunner initDb(AdvertisementRepository repository) {
96153
return args -> {
97154
if (repository.count() == 0) {
98-
repository.save(new Advertisement("Discount Clothing", "1.jpg"));
99-
repository.save(new Advertisement("Cool Hats", "2.jpg"));
100-
repository.save(new Advertisement("Nice Bags", "3.jpg"));
155+
// Create ads with meaningful click URLs that point to relevant frontend pages
156+
// Based on actual image content, the files are mislabeled
157+
// Image 1.jpg shows Discount Clothing content, 2.jpg shows Cool Hats content
158+
repository.save(new Advertisement("Discount Clothing", "1.jpg", "/discount-clothing"));
159+
repository.save(new Advertisement("Cool Hats", "2.jpg", "/cool-hats"));
160+
repository.save(new Advertisement("Nice Bags", "3.jpg", "/nice-bags"));
161+
logger.info("Initialized database with 3 advertisements with click URLs");
162+
} else {
163+
// Always update existing ads to ensure they have the correct click URLs
164+
List<Advertisement> existingAds = repository.findAll();
165+
boolean needsUpdate = false;
166+
167+
for (Advertisement ad : existingAds) {
168+
String oldClickUrl = ad.getClickUrl();
169+
switch (ad.getName()) {
170+
case "Discount Clothing":
171+
if (!"/discount-clothing".equals(oldClickUrl) || !"1.jpg".equals(ad.getPath())) {
172+
ad.setClickUrl("/discount-clothing");
173+
ad.setPath("1.jpg");
174+
needsUpdate = true;
175+
logger.info("Updated '{}' clickUrl from '{}' to '/discount-clothing' and path to '1.jpg'", ad.getName(), oldClickUrl);
176+
}
177+
break;
178+
case "Cool Hats":
179+
if (!"/cool-hats".equals(oldClickUrl) || !"2.jpg".equals(ad.getPath())) {
180+
ad.setClickUrl("/cool-hats");
181+
ad.setPath("2.jpg");
182+
needsUpdate = true;
183+
logger.info("Updated '{}' clickUrl from '{}' to '/cool-hats' and path to '2.jpg'", ad.getName(), oldClickUrl);
184+
}
185+
break;
186+
case "Nice Bags":
187+
if (!"/nice-bags".equals(oldClickUrl) || !"3.jpg".equals(ad.getPath())) {
188+
ad.setClickUrl("/nice-bags");
189+
ad.setPath("3.jpg");
190+
needsUpdate = true;
191+
logger.info("Updated '{}' clickUrl from '{}' to '/nice-bags' and path to '3.jpg'", ad.getName(), oldClickUrl);
192+
}
193+
break;
194+
default:
195+
logger.info("Unknown ad name: '{}', leaving clickUrl unchanged", ad.getName());
196+
}
197+
}
198+
199+
if (needsUpdate) {
200+
repository.saveAll(existingAds);
201+
logger.info("Successfully updated existing ads with correct click URLs");
202+
} else {
203+
logger.info("All ads already have correct click URLs, no update needed");
204+
}
101205
}
102206
};
103207
}

services/ads/java/src/main/java/adsjava/Advertisement.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class Advertisement {
99
private Long id;
1010
private String name;
1111
private String path;
12+
private String clickUrl;
1213

1314
public Advertisement() {}
1415

@@ -17,10 +18,18 @@ public Advertisement(String name, String path) {
1718
this.path = path;
1819
}
1920

21+
public Advertisement(String name, String path, String clickUrl) {
22+
this.name = name;
23+
this.path = path;
24+
this.clickUrl = clickUrl;
25+
}
26+
2027
public Long getId() { return id; }
2128
public void setId(Long id) { this.id = id; }
2229
public String getName() { return name; }
2330
public void setName(String name) { this.name = name; }
2431
public String getPath() { return path; }
2532
public void setPath(String path) { this.path = path; }
33+
public String getClickUrl() { return clickUrl; }
34+
public void setClickUrl(String clickUrl) { this.clickUrl = clickUrl; }
2635
}

services/backend/Gemfile.lock

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ GEM
249249
globalid (1.0.0)
250250
activesupport (>= 5.0)
251251
glyphicons (1.0.2)
252+
google-protobuf (4.31.1-aarch64-linux-gnu)
253+
bigdecimal
254+
rake (>= 13)
252255
google-protobuf (4.31.1-x86_64-linux-gnu)
253256
bigdecimal
254257
rake (>= 13)
@@ -301,7 +304,10 @@ GEM
301304
addressable (~> 2.7)
302305
letter_opener (1.7.0)
303306
launchy (~> 2.2)
307+
libdatadog (18.1.0.1.0-aarch64-linux)
304308
libdatadog (18.1.0.1.0-x86_64-linux)
309+
libddwaf (1.24.1.0.0-aarch64-linux)
310+
ffi (~> 1.0)
305311
libddwaf (1.24.1.0.0-x86_64-linux)
306312
ffi (~> 1.0)
307313
listen (3.7.1)
@@ -341,6 +347,8 @@ GEM
341347
net-smtp (0.5.1)
342348
net-protocol
343349
nio4r (2.5.8)
350+
nokogiri (1.13.3-aarch64-linux)
351+
racc (~> 1.4)
344352
nokogiri (1.13.3-x86_64-linux)
345353
racc (~> 1.4)
346354
octokit (4.22.0)
@@ -498,8 +506,6 @@ GEM
498506
sendgrid-ruby (~> 6.4)
499507
sendgrid-ruby (6.6.0)
500508
ruby_http_client (~> 3.4)
501-
sentry-raven (3.1.2)
502-
faraday (>= 1.0)
503509
sidekiq (6.4.0)
504510
connection_pool (>= 2.2.2)
505511
rack (~> 2.0)
@@ -683,6 +689,7 @@ GEM
683689
zeitwerk (2.5.3)
684690

685691
PLATFORMS
692+
aarch64-linux
686693
x86_64-linux
687694

688695
DEPENDENCIES
@@ -714,7 +721,6 @@ DEPENDENCIES
714721
sass-rails
715722
sassc!
716723
sendgrid-actionmailer
717-
sentry-raven
718724
sidekiq
719725
spree (>= 4.4.0)
720726
spree_auth_devise

services/frontend/components/common/Ad/Ad.tsx

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { useState, useEffect, useCallback } from 'react'
22
import { codeStash } from 'code-stash'
33
import config from '../../../featureFlags.config.json'
44

5-
export interface AdDataResults {
6-
data: object | null
5+
// Proper TypeScript interface for Advertisement object from Java service
6+
export interface Advertisement {
7+
id: number
8+
name: string
79
path: string
10+
clickUrl: string
811
}
12+
913
// Advertisement banner
1014
function Ad() {
11-
const [data, setData] = useState<AdDataResults | null>(null)
15+
const [data, setData] = useState<Advertisement | null>(null)
1216
const [isLoading, setLoading] = useState(false)
1317
const adsPath = process.env.NEXT_PUBLIC_ADS_ROUTE || `/services/ads`
1418

@@ -27,21 +31,45 @@ function Ad() {
2731

2832
try {
2933
console.log('ads path', adsPath)
30-
const res = await fetch(`${adsPath}/ads`, { headers })
34+
// Add cache-busting parameter to ensure fresh data
35+
const timestamp = Date.now()
36+
const res = await fetch(`${adsPath}/ads?t=${timestamp}`, { headers })
3137
if (!res.ok) {
3238
throw new Error('Error fetching ad')
3339
}
34-
const data = await res.json()
35-
console.log(data)
36-
const index = getRandomArbitrary(0, data.length)
37-
setData(data[index])
40+
const data: Advertisement[] = await res.json()
41+
console.log('Available ads:', data)
42+
// Sort ads by ID to ensure consistent ordering
43+
const sortedAds = data.sort((a, b) => a.id - b.id)
44+
// Use a deterministic selection based on time to show different ads
45+
// This ensures the visual ad matches the expected click behavior
46+
const now = new Date()
47+
const adIndex = Math.floor(now.getSeconds() / 5) % sortedAds.length // Change ad every 5 seconds
48+
const selectedAd = sortedAds[adIndex]
49+
console.log('Selected ad:', selectedAd)
50+
setData(selectedAd)
3851
setLoading(false)
3952
} catch (e) {
4053
console.error(e)
4154
setLoading(false)
4255
}
4356
}, [adsPath, getRandomArbitrary, setData, setLoading])
4457

58+
const handleAdClick = useCallback(() => {
59+
if (data?.id) {
60+
console.log('Ad clicked!', {
61+
adId: data.id,
62+
adName: data.name,
63+
clickUrl: data.clickUrl,
64+
imagePath: data.path,
65+
redirectUrl: `${adsPath}/click/${data.id}`
66+
})
67+
// Direct browser navigation to the click endpoint
68+
// The Java service will handle the redirect to the appropriate URL
69+
window.location.href = `${adsPath}/click/${data.id}`
70+
}
71+
}, [data, adsPath])
72+
4573
useEffect(() => {
4674
if (!data) fetchAd()
4775
}, [data, fetchAd])
@@ -61,9 +89,17 @@ function Ad() {
6189

6290
return (
6391
<div className="flex flex-row justify-center py-4 advertisement-wrapper">
64-
<picture className="advertisement-banner">
92+
<picture
93+
className="advertisement-banner cursor-pointer"
94+
onClick={handleAdClick}
95+
title={`Click to see ${data.name}`}
96+
>
6597
<source srcSet={`${adsPath}/banners/${data.path}`} type="image/webp" />
66-
<img src={`${adsPath}/banners/${data.path}`} alt="Landscape picture" />
98+
<img
99+
src={`${adsPath}/banners/${data.path}`}
100+
alt={data.name || "Advertisement"}
101+
className="cursor-pointer"
102+
/>
67103
</picture>
68104
</div>
69105
)

0 commit comments

Comments
 (0)