diff --git a/.env.template b/.env.template index af77ed45..a340174d 100644 --- a/.env.template +++ b/.env.template @@ -73,4 +73,4 @@ DISABLE_SPRING=1 STOREDOG_URL=http://service-proxy:80 # base url for storedog service (default: 'http://service-proxy:80') PUPPETEER_TIMEOUT=30000 # timeout for puppeteer (default: 30000) -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 +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 \ No newline at end of file diff --git a/services/ads/java/src/main/java/adsjava/AdsJavaApplication.java b/services/ads/java/src/main/java/adsjava/AdsJavaApplication.java index b375fc65..de4d3551 100644 --- a/services/ads/java/src/main/java/adsjava/AdsJavaApplication.java +++ b/services/ads/java/src/main/java/adsjava/AdsJavaApplication.java @@ -5,13 +5,18 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.util.StreamUtils; import java.io.*; +import java.net.URI; import org.springframework.web.bind.annotation.ResponseBody; import org.apache.commons.io.IOUtils; import java.util.concurrent.ThreadLocalRandom; import java.util.HashMap; +import java.util.Optional; import org.springframework.web.bind.annotation.RequestParam; import java.util.concurrent.TimeoutException; import org.springframework.web.bind.annotation.RequestHeader; @@ -44,15 +49,67 @@ public String home() { value = "/banners/{id}", produces = MediaType.IMAGE_JPEG_VALUE ) - public @ResponseBody byte[] getImageWithMediaType() throws IOException { - logger.info("/banners/{id} called"); - int randomNum = ThreadLocalRandom.current().nextInt(1, 3 + 1); - String imagePath = "/static/ads/ad" + randomNum + ".jpg"; - InputStream in = getClass() - .getResourceAsStream(imagePath); + public @ResponseBody byte[] getImageWithMediaType(@PathVariable String id) throws IOException { + logger.info("/banners/{} called", id); + + // Map the image path to the correct static file + String imagePath; + switch (id) { + case "1.jpg": + imagePath = "/static/ads/ad1.jpg"; + break; + case "2.jpg": + imagePath = "/static/ads/ad2.jpg"; + break; + case "3.jpg": + imagePath = "/static/ads/ad3.jpg"; + break; + default: + // Fallback to random image if unknown + int randomNum = ThreadLocalRandom.current().nextInt(1, 3 + 1); + imagePath = "/static/ads/ad" + randomNum + ".jpg"; + logger.warn("Unknown image id: {}, using random image", id); + } + + InputStream in = getClass().getResourceAsStream(imagePath); + if (in == null) { + logger.error("Image not found: {}", imagePath); + throw new IOException("Image not found: " + imagePath); + } return IOUtils.toByteArray(in); } + @CrossOrigin(origins = {"*"}) + @RequestMapping("/click/{id}") + public ResponseEntity handleAdClick(@PathVariable Long id) { + logger.info("Ad click for id: " + id); + + Optional adOptional = advertisementRepository.findById(id); + if (adOptional.isPresent()) { + Advertisement ad = adOptional.get(); + String clickUrl = ad.getClickUrl(); + + // Log the click for analytics + logger.info("Redirecting ad '{}' (id: {}) to: {}", ad.getName(), id, clickUrl); + + if (clickUrl != null && !clickUrl.isEmpty()) { + // Return a redirect response to the click URL + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(clickUrl)) + .build(); + } else { + // Default redirect if no clickUrl is set + logger.warn("No clickUrl set for ad id: " + id + ", redirecting to homepage"); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create("/")) + .build(); + } + } else { + logger.error("Ad not found for id: " + id); + return ResponseEntity.notFound().build(); + } + } + @CrossOrigin(origins = {"*"}) @RequestMapping( value = "/ads", @@ -95,9 +152,56 @@ public static void main(String[] args) { public CommandLineRunner initDb(AdvertisementRepository repository) { return args -> { if (repository.count() == 0) { - repository.save(new Advertisement("Discount Clothing", "1.jpg")); - repository.save(new Advertisement("Cool Hats", "2.jpg")); - repository.save(new Advertisement("Nice Bags", "3.jpg")); + // Create ads with meaningful click URLs that point to relevant frontend pages + // Based on actual image content, the files are mislabeled + // Image 1.jpg shows Discount Clothing content, 2.jpg shows Cool Hats content + repository.save(new Advertisement("Discount Clothing", "1.jpg", "/discount-clothing")); + repository.save(new Advertisement("Cool Hats", "2.jpg", "/cool-hats")); + repository.save(new Advertisement("Nice Bags", "3.jpg", "/nice-bags")); + logger.info("Initialized database with 3 advertisements with click URLs"); + } else { + // Always update existing ads to ensure they have the correct click URLs + List existingAds = repository.findAll(); + boolean needsUpdate = false; + + for (Advertisement ad : existingAds) { + String oldClickUrl = ad.getClickUrl(); + switch (ad.getName()) { + case "Discount Clothing": + if (!"/discount-clothing".equals(oldClickUrl) || !"1.jpg".equals(ad.getPath())) { + ad.setClickUrl("/discount-clothing"); + ad.setPath("1.jpg"); + needsUpdate = true; + logger.info("Updated '{}' clickUrl from '{}' to '/discount-clothing' and path to '1.jpg'", ad.getName(), oldClickUrl); + } + break; + case "Cool Hats": + if (!"/cool-hats".equals(oldClickUrl) || !"2.jpg".equals(ad.getPath())) { + ad.setClickUrl("/cool-hats"); + ad.setPath("2.jpg"); + needsUpdate = true; + logger.info("Updated '{}' clickUrl from '{}' to '/cool-hats' and path to '2.jpg'", ad.getName(), oldClickUrl); + } + break; + case "Nice Bags": + if (!"/nice-bags".equals(oldClickUrl) || !"3.jpg".equals(ad.getPath())) { + ad.setClickUrl("/nice-bags"); + ad.setPath("3.jpg"); + needsUpdate = true; + logger.info("Updated '{}' clickUrl from '{}' to '/nice-bags' and path to '3.jpg'", ad.getName(), oldClickUrl); + } + break; + default: + logger.info("Unknown ad name: '{}', leaving clickUrl unchanged", ad.getName()); + } + } + + if (needsUpdate) { + repository.saveAll(existingAds); + logger.info("Successfully updated existing ads with correct click URLs"); + } else { + logger.info("All ads already have correct click URLs, no update needed"); + } } }; } diff --git a/services/ads/java/src/main/java/adsjava/Advertisement.java b/services/ads/java/src/main/java/adsjava/Advertisement.java index edd00986..8a200818 100644 --- a/services/ads/java/src/main/java/adsjava/Advertisement.java +++ b/services/ads/java/src/main/java/adsjava/Advertisement.java @@ -9,6 +9,7 @@ public class Advertisement { private Long id; private String name; private String path; + private String clickUrl; public Advertisement() {} @@ -17,10 +18,18 @@ public Advertisement(String name, String path) { this.path = path; } + public Advertisement(String name, String path, String clickUrl) { + this.name = name; + this.path = path; + this.clickUrl = clickUrl; + } + public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } + public String getClickUrl() { return clickUrl; } + public void setClickUrl(String clickUrl) { this.clickUrl = clickUrl; } } \ No newline at end of file diff --git a/services/backend/Gemfile.lock b/services/backend/Gemfile.lock index 3822d239..4f6955f6 100644 --- a/services/backend/Gemfile.lock +++ b/services/backend/Gemfile.lock @@ -249,6 +249,9 @@ GEM globalid (1.0.0) activesupport (>= 5.0) glyphicons (1.0.2) + google-protobuf (4.31.1-aarch64-linux-gnu) + bigdecimal + rake (>= 13) google-protobuf (4.31.1-x86_64-linux-gnu) bigdecimal rake (>= 13) @@ -301,7 +304,10 @@ GEM addressable (~> 2.7) letter_opener (1.7.0) launchy (~> 2.2) + libdatadog (18.1.0.1.0-aarch64-linux) libdatadog (18.1.0.1.0-x86_64-linux) + libddwaf (1.24.1.0.0-aarch64-linux) + ffi (~> 1.0) libddwaf (1.24.1.0.0-x86_64-linux) ffi (~> 1.0) listen (3.7.1) @@ -341,6 +347,8 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.5.8) + nokogiri (1.13.3-aarch64-linux) + racc (~> 1.4) nokogiri (1.13.3-x86_64-linux) racc (~> 1.4) octokit (4.22.0) @@ -498,8 +506,6 @@ GEM sendgrid-ruby (~> 6.4) sendgrid-ruby (6.6.0) ruby_http_client (~> 3.4) - sentry-raven (3.1.2) - faraday (>= 1.0) sidekiq (6.4.0) connection_pool (>= 2.2.2) rack (~> 2.0) @@ -683,6 +689,7 @@ GEM zeitwerk (2.5.3) PLATFORMS + aarch64-linux x86_64-linux DEPENDENCIES @@ -714,7 +721,6 @@ DEPENDENCIES sass-rails sassc! sendgrid-actionmailer - sentry-raven sidekiq spree (>= 4.4.0) spree_auth_devise diff --git a/services/frontend/components/common/Ad/Ad.tsx b/services/frontend/components/common/Ad/Ad.tsx index fb153cc7..014c7c3a 100644 --- a/services/frontend/components/common/Ad/Ad.tsx +++ b/services/frontend/components/common/Ad/Ad.tsx @@ -2,13 +2,17 @@ import { useState, useEffect, useCallback } from 'react' import { codeStash } from 'code-stash' import config from '../../../featureFlags.config.json' -export interface AdDataResults { - data: object | null +// Proper TypeScript interface for Advertisement object from Java service +export interface Advertisement { + id: number + name: string path: string + clickUrl: string } + // Advertisement banner function Ad() { - const [data, setData] = useState(null) + const [data, setData] = useState(null) const [isLoading, setLoading] = useState(false) const adsPath = process.env.NEXT_PUBLIC_ADS_ROUTE || `/services/ads` @@ -27,14 +31,23 @@ function Ad() { try { console.log('ads path', adsPath) - const res = await fetch(`${adsPath}/ads`, { headers }) + // Add cache-busting parameter to ensure fresh data + const timestamp = Date.now() + const res = await fetch(`${adsPath}/ads?t=${timestamp}`, { headers }) if (!res.ok) { throw new Error('Error fetching ad') } - const data = await res.json() - console.log(data) - const index = getRandomArbitrary(0, data.length) - setData(data[index]) + const data: Advertisement[] = await res.json() + console.log('Available ads:', data) + // Sort ads by ID to ensure consistent ordering + const sortedAds = data.sort((a, b) => a.id - b.id) + // Use a deterministic selection based on time to show different ads + // This ensures the visual ad matches the expected click behavior + const now = new Date() + const adIndex = Math.floor(now.getSeconds() / 5) % sortedAds.length // Change ad every 5 seconds + const selectedAd = sortedAds[adIndex] + console.log('Selected ad:', selectedAd) + setData(selectedAd) setLoading(false) } catch (e) { console.error(e) @@ -42,6 +55,21 @@ function Ad() { } }, [adsPath, getRandomArbitrary, setData, setLoading]) + const handleAdClick = useCallback(() => { + if (data?.id) { + console.log('Ad clicked!', { + adId: data.id, + adName: data.name, + clickUrl: data.clickUrl, + imagePath: data.path, + redirectUrl: `${adsPath}/click/${data.id}` + }) + // Direct browser navigation to the click endpoint + // The Java service will handle the redirect to the appropriate URL + window.location.href = `${adsPath}/click/${data.id}` + } + }, [data, adsPath]) + useEffect(() => { if (!data) fetchAd() }, [data, fetchAd]) @@ -61,9 +89,17 @@ function Ad() { return (
- + - Landscape picture + {data.name
) diff --git a/services/frontend/pages/cool-hats.tsx b/services/frontend/pages/cool-hats.tsx new file mode 100644 index 00000000..f8e93453 --- /dev/null +++ b/services/frontend/pages/cool-hats.tsx @@ -0,0 +1,95 @@ +import { Layout } from '@components/common' +import { Container, Text } from '@components/ui' +import { SEO } from '@components/common' +import { GetServerSidePropsContext } from 'next' +import { Product } from '@customTypes/product' +import { ProductCard } from '@components/product' + +interface CoolHatsPageProps { + hatProducts: Product[] +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const baseUrl = process.env.NEXT_PUBLIC_FRONTEND_API_ROUTE + ? `${process.env.NEXT_PUBLIC_FRONTEND_API_ROUTE}/api` + : 'http://localhost:3000/api' + + try { + // Fetch all products and filter for hat-related items + const allProducts: Product[] = await fetch(`${baseUrl}/products`) + .then((res) => res.json()) + .catch(() => []) + + // Filter for hat-related products (you can adjust this logic) + const hatProducts = allProducts.filter(product => + product.name?.toLowerCase().includes('hat') || + product.name?.toLowerCase().includes('cap') || + product.name?.toLowerCase().includes('beanie') || + product.description?.toLowerCase().includes('hat') || + product.description?.toLowerCase().includes('headwear') + ) + + return { + props: { + hatProducts: hatProducts.slice(0, 12) // Limit to 12 products + } + } + } catch (error) { + console.error('Error fetching hat products:', error) + return { + props: { + hatProducts: [] + } + } + } +} + +export default function CoolHatsPage({ hatProducts }: CoolHatsPageProps) { + return ( + <> + + + + {/* Hero Section */} +
+

+ ๐Ÿงข Cool Hats Collection +

+

+ Welcome to our awesome hat collection! You clicked on the Cool Hats ad and landed here. + Discover headwear that's both stylish and functional. +

+
+ + + + {/* Hat Products Section */} +
+
๐Ÿงข
+

No Hat Products Found

+

+ We're still building our hat collection! Check back soon for awesome headwear. +

+
+ + {/* Call to Action */} +
+
+

Love Fashion? We've Got You Covered!

+

+ Join our hat enthusiasts community and be the first to know about new arrivals. +

+ + Explore All Products + +
+
+
+ + ) +} + +CoolHatsPage.Layout = Layout \ No newline at end of file diff --git a/services/frontend/pages/discount-clothing.tsx b/services/frontend/pages/discount-clothing.tsx new file mode 100644 index 00000000..4ca8625f --- /dev/null +++ b/services/frontend/pages/discount-clothing.tsx @@ -0,0 +1,131 @@ +import { Layout } from '@components/common' +import { Container, Text } from '@components/ui' +import { SEO } from '@components/common' +import { GetServerSidePropsContext } from 'next' +import { Product } from '@customTypes/product' +import { ProductCard } from '@components/product' + +interface DiscountClothingPageProps { + clothingProducts: Product[] +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const baseUrl = process.env.NEXT_PUBLIC_FRONTEND_API_ROUTE + ? `${process.env.NEXT_PUBLIC_FRONTEND_API_ROUTE}/api` + : 'http://localhost:3000/api' + + try { + // Fetch all products and filter for clothing-related items + const allProducts: Product[] = await fetch(`${baseUrl}/products`) + .then((res) => res.json()) + .catch(() => []) + + // Filter for clothing-related products + const clothingProducts = allProducts.filter(product => + product.name?.toLowerCase().includes('shirt') || + product.name?.toLowerCase().includes('dress') || + product.name?.toLowerCase().includes('pants') || + product.name?.toLowerCase().includes('jeans') || + product.name?.toLowerCase().includes('sweater') || + product.name?.toLowerCase().includes('jacket') || + product.name?.toLowerCase().includes('coat') || + product.name?.toLowerCase().includes('blouse') || + product.name?.toLowerCase().includes('skirt') || + product.name?.toLowerCase().includes('t-shirt') || + product.name?.toLowerCase().includes('hoodie') || + product.description?.toLowerCase().includes('shirt') || + product.description?.toLowerCase().includes('dress') || + product.description?.toLowerCase().includes('pants') || + product.description?.toLowerCase().includes('jeans') || + product.description?.toLowerCase().includes('sweater') || + product.description?.toLowerCase().includes('jacket') || + product.description?.toLowerCase().includes('coat') || + product.description?.toLowerCase().includes('blouse') || + product.description?.toLowerCase().includes('skirt') || + product.description?.toLowerCase().includes('t-shirt') || + product.description?.toLowerCase().includes('hoodie') + ) + + return { + props: { + clothingProducts: clothingProducts.slice(0, 12) // Limit to 12 products + } + } + } catch (error) { + console.error('Error fetching clothing products:', error) + return { + props: { + clothingProducts: [] + } + } + } +} + +export default function DiscountClothingPage({ clothingProducts }: DiscountClothingPageProps) { + return ( + <> + + + + {/* Hero Section */} +
+

+ ๐Ÿ‘• Discount Clothing Collection +

+

+ Welcome to our fabulous discount clothing collection! You clicked on the Discount Clothing ad and landed here. + Discover stylish fashion at unbeatable prices. +

+
+ + + + {/* Clothing Products Section */} + {clothingProducts.length > 0 ? ( + <> +

Featured Clothing Products

+
+ {clothingProducts.map((product) => ( + + ))} +
+ + ) : ( +
+
๐Ÿ‘•
+

No Clothing Products Found

+

+ We're still building our clothing collection! Check back soon for fabulous fashion items. +

+
+ )} + + {/* Call to Action */} +
+
+

Love Fashion? We've Got You Covered!

+

+ Join our fashion-forward community and be the first to know about new arrivals and exclusive discounts. +

+ + Explore All Products + +
+
+
+ + ) +} + +DiscountClothingPage.Layout = Layout \ No newline at end of file diff --git a/services/frontend/pages/nice-bags.tsx b/services/frontend/pages/nice-bags.tsx new file mode 100644 index 00000000..cb7fca65 --- /dev/null +++ b/services/frontend/pages/nice-bags.tsx @@ -0,0 +1,119 @@ +import { Layout } from '@components/common' +import { Container, Text } from '@components/ui' +import { SEO } from '@components/common' +import { GetServerSidePropsContext } from 'next' +import { Product } from '@customTypes/product' +import { ProductCard } from '@components/product' + +interface NiceBagsPageProps { + bagProducts: Product[] +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const baseUrl = process.env.NEXT_PUBLIC_FRONTEND_API_ROUTE + ? `${process.env.NEXT_PUBLIC_FRONTEND_API_ROUTE}/api` + : 'http://localhost:3000/api' + + try { + // Fetch all products and filter for bag-related items + const allProducts: Product[] = await fetch(`${baseUrl}/products`) + .then((res) => res.json()) + .catch(() => []) + + // Filter for bag-related products + const bagProducts = allProducts.filter(product => + product.name?.toLowerCase().includes('bag') || + product.name?.toLowerCase().includes('purse') || + product.name?.toLowerCase().includes('handbag') || + product.name?.toLowerCase().includes('backpack') || + product.name?.toLowerCase().includes('tote') || + product.name?.toLowerCase().includes('clutch') || + product.description?.toLowerCase().includes('bag') || + product.description?.toLowerCase().includes('purse') || + product.description?.toLowerCase().includes('handbag') || + product.description?.toLowerCase().includes('backpack') + ) + + return { + props: { + bagProducts: bagProducts.slice(0, 12) // Limit to 12 products + } + } + } catch (error) { + console.error('Error fetching bag products:', error) + return { + props: { + bagProducts: [] + } + } + } +} + +export default function NiceBagsPage({ bagProducts }: NiceBagsPageProps) { + return ( + <> + + + + {/* Hero Section */} +
+

+ ๐Ÿ‘œ Nice Bags Collection +

+

+ Welcome to our fabulous bag collection! You clicked on the Nice Bags ad and landed here. + Discover bags that are both stylish and practical for every occasion. +

+
+ + + + {/* Bag Products Section */} + {bagProducts.length > 0 ? ( + <> +

Featured Bag Products

+
+ {bagProducts.map((product) => ( + + ))} +
+ + ) : ( +
+
๐Ÿ‘œ
+

No Bag Products Found

+

+ We're still building our bag collection! Check back soon for fabulous accessories. +

+
+ )} + + {/* Call to Action */} +
+
+

Love Fashion? We've Got You Covered!

+

+ Join our fashion-forward community and be the first to know about new arrivals. +

+ + Explore All Products + +
+
+
+ + ) +} + +NiceBagsPage.Layout = Layout \ No newline at end of file