From e7ef3f51385e90ee85e540ce514f59d7636640cb Mon Sep 17 00:00:00 2001 From: Eric McGarry Date: Thu, 16 Jul 2026 14:42:29 -0400 Subject: [PATCH 1/2] feat: public listing detail page supports external listings 6367 --- api/src/services/external-listing.service.ts | 400 +++++++++--------- api/src/services/listing.service.ts | 2 +- shared-helpers/src/locales/ar.json | 1 + shared-helpers/src/locales/bn.json | 1 + shared-helpers/src/locales/es.json | 1 + shared-helpers/src/locales/fa.json | 1 + shared-helpers/src/locales/general.json | 1 + shared-helpers/src/locales/hy.json | 1 + shared-helpers/src/locales/ko.json | 1 + shared-helpers/src/locales/tl.json | 1 + shared-helpers/src/locales/vi.json | 1 + shared-helpers/src/locales/zh.json | 1 + .../listing/ListingViewSeedsHelpers.test.ts | 15 + .../listing/listing_sections/Apply.test.tsx | 39 ++ .../src/components/listing/GetApplication.tsx | 7 + .../src/components/listing/ListingView.tsx | 8 +- .../components/listing/ListingViewSeeds.tsx | 2 +- .../listing/ListingViewSeedsHelpers.tsx | 7 +- .../listing/listing_sections/Apply.tsx | 10 +- .../listing_sections/OtherFeatures.tsx | 6 +- .../applications/start/choose-language.tsx | 2 +- sites/public/src/pages/listing/[id].tsx | 17 +- .../public/src/pages/listing/[id]/[slug].tsx | 18 +- 23 files changed, 325 insertions(+), 218 deletions(-) diff --git a/api/src/services/external-listing.service.ts b/api/src/services/external-listing.service.ts index d43ebfefa51..1c94b89357c 100644 --- a/api/src/services/external-listing.service.ts +++ b/api/src/services/external-listing.service.ts @@ -57,7 +57,10 @@ export class ExternalListingService { } async ingest(ingestParams: IngestParams) { - const { externalURL, jurisdictionId, targetName } = ingestParams; + const { jurisdictionId, targetName } = ingestParams; + const externalURL = ingestParams.externalURL.endsWith('/') + ? ingestParams.externalURL.slice(0, -1) + : ingestParams.externalURL; // fetch the externalized data from the external system const { data } = await firstValueFrom( @@ -306,203 +309,210 @@ export class ExternalListingService { } } - await this.prisma.listings.create({ - data: { - accessibility: data.accessibility, - additionalApplicationSubmissionNotes: - data.additionalApplicationSubmissionNotes, - allowsCats: data.allowsCats, - allowsDogs: data.allowsDogs, - amenities: data.amenities, - applicationDueDate: data.applicationDueDate, - applicationFee: data.applicationFee, - applicationOpenDate: data.applicationOpenDate, - applicationOrganization: data.applicationOrganization, - buildingTotalUnits: data.buildingTotalUnits, - cocInfo: data.cocInfo, - commonDigitalApplication: data.commonDigitalApplication, - communityDisclaimerDescription: data.communityDisclaimerDescription, - communityDisclaimerTitle: data.communityDisclaimerTitle, - configurableRegion: data.configurableRegion, - contentUpdatedAt: data.contentUpdatedAt, - costsNotIncluded: data.costsNotIncluded, - creditHistory: data.creditHistory, - creditScreeningFee: data.creditScreeningFee, - criminalBackground: data.criminalBackground, - customMapPin: data.customMapPin, - depositHelperText: data.depositHelperText, - depositMax: data.depositMax, - depositMin: data.depositMin, - depositType: data.depositType, - depositValue: data.depositValue, - developer: data.developer, - digitalApplication: data.digitalApplication, - disableUnitsAccordion: data.disableUnitsAccordion, - displayWaitlistSize: data.displayWaitlistSize, - externalJurisdictionId: externalJurisdiction.id, - externalListingId: externalListing.id, - externalURL: externalURL, - hasHudEbllClearance: data.hasHudEbllClearance, - homeType: data.homeType, - householdSizeMax: data.householdSizeMax, - householdSizeMin: data.householdSizeMin, - includeCommunityDisclaimer: data.includeCommunityDisclaimer, - isVerified: data.isVerified, - isWaitlistOpen: data.isWaitlistOpen, - leasingAgentEmail: data.leasingAgentEmail, - leasingAgentName: data.leasingAgentName, - leasingAgentOfficeHours: data.leasingAgentOfficeHours, - leasingAgentPhone: data.leasingAgentPhone, - leasingAgentTitle: data.leasingAgentTitle, - listingFileNumber: data.listingFileNumber, - listingType: data.listingType, - lotteryOptIn: data.lotteryOptIn, - managementWebsite: data.managementWebsite, - marketingMonth: data.marketingMonth, - marketingSeason: data.marketingSeason, - marketingType: data.marketingType, - marketingYear: data.marketingYear, - name: data.name, - neighborhood: data.neighborhood, - paperApplication: data.paperApplication, - parkingFee: data.parkingFee, - petPolicy: data.petPolicy, - postmarkedApplicationsReceivedByDate: - data.postmarkedApplicationsReceivedByDate, - programRules: data.programRules, - publishedAt: data.publishedAt, - referralOpportunity: data.referralOpportunity, - rentalAssistance: data.rentalAssistance, - rentalHistory: data.rentalHistory, - reservedCommunityDescription: data.reservedCommunityDescription, - reservedCommunityMinAge: data.reservedCommunityMinAge, - reviewOrderType: data.reviewOrderType, - scheduledPublishAt: data.scheduledPublishAt, - scheduledApplicationOpenAt: data.scheduledApplicationOpenAt, - section8Acceptance: data.section8Acceptance, - servicesOffered: data.servicesOffered, - smokingPolicy: data.smokingPolicy, - specialNotes: data.specialNotes, - status: data.status, - unitAmenities: data.unitAmenities, - unitsAvailable: data.unitsAvailable, - waitlistCurrentSize: data.waitlistCurrentSize, - waitlistMaxSize: data.waitlistMaxSize, - waitlistOpenSpots: data.waitlistOpenSpots, - whatToExpect: data.whatToExpect, - whatToExpectAdditionalText: data.whatToExpectAdditionalText, - yearBuilt: data.yearBuilt, - - assets: Prisma.JsonNullValueInput.JsonNull, - jurisdictions: { - connect: { - id: jurisdictionId, + try { + await this.prisma.listings.create({ + data: { + accessibility: data.accessibility, + additionalApplicationSubmissionNotes: + data.additionalApplicationSubmissionNotes, + allowsCats: data.allowsCats, + allowsDogs: data.allowsDogs, + amenities: data.amenities, + applicationDueDate: data.applicationDueDate, + applicationFee: data.applicationFee, + applicationOpenDate: data.applicationOpenDate, + applicationOrganization: data.applicationOrganization, + buildingTotalUnits: data.buildingTotalUnits, + cocInfo: data.cocInfo, + commonDigitalApplication: data.commonDigitalApplication, + communityDisclaimerDescription: data.communityDisclaimerDescription, + communityDisclaimerTitle: data.communityDisclaimerTitle, + configurableRegion: data.configurableRegion, + contentUpdatedAt: data.contentUpdatedAt, + costsNotIncluded: data.costsNotIncluded, + creditHistory: data.creditHistory, + creditScreeningFee: data.creditScreeningFee, + criminalBackground: data.criminalBackground, + customMapPin: data.customMapPin, + depositHelperText: data.depositHelperText, + depositMax: data.depositMax, + depositMin: data.depositMin, + depositType: data.depositType, + depositValue: data.depositValue, + developer: data.developer, + digitalApplication: data.digitalApplication, + disableUnitsAccordion: data.disableUnitsAccordion, + displayWaitlistSize: data.displayWaitlistSize, + externalJurisdictionId: externalJurisdiction.id, + externalListingId: externalListing.id, + externalURL: externalURL, + hasHudEbllClearance: data.hasHudEbllClearance, + homeType: data.homeType, + householdSizeMax: data.householdSizeMax, + householdSizeMin: data.householdSizeMin, + includeCommunityDisclaimer: data.includeCommunityDisclaimer, + isVerified: data.isVerified, + isWaitlistOpen: data.isWaitlistOpen, + leasingAgentEmail: data.leasingAgentEmail, + leasingAgentName: data.leasingAgentName, + leasingAgentOfficeHours: data.leasingAgentOfficeHours, + leasingAgentPhone: data.leasingAgentPhone, + leasingAgentTitle: data.leasingAgentTitle, + listingFileNumber: data.listingFileNumber, + listingType: data.listingType, + lotteryOptIn: data.lotteryOptIn, + managementWebsite: data.managementWebsite, + marketingMonth: data.marketingMonth, + marketingSeason: data.marketingSeason, + marketingType: data.marketingType, + marketingYear: data.marketingYear, + name: data.name, + neighborhood: data.neighborhood, + paperApplication: data.paperApplication, + parkingFee: data.parkingFee, + petPolicy: data.petPolicy, + postmarkedApplicationsReceivedByDate: + data.postmarkedApplicationsReceivedByDate, + programRules: data.programRules, + publishedAt: data.publishedAt, + referralOpportunity: data.referralOpportunity, + rentalAssistance: data.rentalAssistance, + rentalHistory: data.rentalHistory, + reservedCommunityDescription: data.reservedCommunityDescription, + reservedCommunityMinAge: data.reservedCommunityMinAge, + reviewOrderType: data.reviewOrderType, + scheduledPublishAt: data.scheduledPublishAt, + scheduledApplicationOpenAt: data.scheduledApplicationOpenAt, + section8Acceptance: data.section8Acceptance, + servicesOffered: data.servicesOffered, + smokingPolicy: data.smokingPolicy, + specialNotes: data.specialNotes, + status: data.status, + unitAmenities: data.unitAmenities, + unitsAvailable: data.unitsAvailable, + waitlistCurrentSize: data.waitlistCurrentSize, + waitlistMaxSize: data.waitlistMaxSize, + waitlistOpenSpots: data.waitlistOpenSpots, + whatToExpect: data.whatToExpect, + whatToExpectAdditionalText: data.whatToExpectAdditionalText, + yearBuilt: data.yearBuilt, + + assets: Prisma.JsonNullValueInput.JsonNull, + jurisdictions: { + connect: { + id: jurisdictionId, + }, }, - }, - listingImages: data.listingImages - ? { - create: data.listingImages.map((image) => ({ - assets: { - create: { - fileId: image.assets.fileId, - label: image.assets.label, + listingImages: data.listingImages + ? { + create: data.listingImages.map((image) => ({ + assets: { + create: { + fileId: image.assets.fileId, + label: image.assets.label, + }, }, + ordinal: image.ordinal, + description: image.description, + })), + } + : undefined, + listingsBuildingAddress: data.listingsBuildingAddress + ? { + create: { + ...data.listingsBuildingAddress, + id: undefined, }, - ordinal: image.ordinal, - description: image.description, - })), - } - : undefined, - listingsBuildingAddress: data.listingsBuildingAddress - ? { - create: { - ...data.listingsBuildingAddress, - id: undefined, - }, - } - : undefined, - reservedCommunityTypes: reservedCommunityTypeId - ? { - connect: { - id: reservedCommunityTypeId, - }, - } - : undefined, - unitGroups: data.unitGroups - ? { - create: data.unitGroups.map((group) => ({ - bathroomMax: group.bathroomMax, - bathroomMin: group.bathroomMin, - floorMax: group.floorMax, - floorMin: group.floorMin, - maxOccupancy: group.maxOccupancy, - minOccupancy: group.minOccupancy, - openWaitlist: group.openWaitlist, - sqFeetMax: group.sqFeetMax, - sqFeetMin: group.sqFeetMin, - rentType: group.rentType, - flatRentValueFrom: group.flatRentValueFrom, - flatRentValueTo: group.flatRentValueTo, - monthlyRent: group.monthlyRent, - totalAvailable: group.totalAvailable, - totalCount: group.totalCount, - accessibilityPriorityType: group.accessibilityPriorityType, - unitGroupAmiLevels: undefined, - unitTypes: { - connect: group.unitTypes.map((type) => ({ - id: combinedUTs.find( - (unitType) => unitType.externalId === type.id, - )?.internalId, - })), + } + : undefined, + reservedCommunityTypes: reservedCommunityTypeId + ? { + connect: { + id: reservedCommunityTypeId, }, - })), - } - : undefined, - units: data.units - ? { - create: data.units.map((unit) => ({ - accessibilityPriorityType: unit.accessibilityPriorityType, - amiPercentage: unit.amiPercentage, - annualIncomeMax: unit.annualIncomeMax, - annualIncomeMin: unit.annualIncomeMin, - bmrProgramChart: unit.bmrProgramChart, - floor: unit.floor, - maxOccupancy: unit.maxOccupancy, - minOccupancy: unit.minOccupancy, - monthlyIncomeMin: unit.monthlyIncomeMin, - monthlyRent: unit.monthlyRent, - monthlyRentAsPercentOfIncome: unit.monthlyRentAsPercentOfIncome, - numBedrooms: unit.numBedrooms, - numBathrooms: unit.numBathrooms, - number: unit.number, - sqFeet: unit.sqFeet, - - amiChart: undefined, - unitAmiChartOverrides: undefined, - unitRentTypes: unit.unitRentTypes - ? { - connect: { - id: combinedURTs.find( - (unitRentType) => - unitRentType.externalId === unit.unitRentTypes.id, - )?.internalId, - }, - } - : undefined, - unitTypes: unit.unitTypes - ? { - connect: { - id: combinedUTs.find( - (unitType) => - unitType.externalId === unit.unitTypes.id, - )?.internalId, - }, - } - : undefined, - })), - } - : undefined, - }, - }); + } + : undefined, + unitGroups: data.unitGroups + ? { + create: data.unitGroups.map((group) => ({ + bathroomMax: group.bathroomMax, + bathroomMin: group.bathroomMin, + floorMax: group.floorMax, + floorMin: group.floorMin, + maxOccupancy: group.maxOccupancy, + minOccupancy: group.minOccupancy, + openWaitlist: group.openWaitlist, + sqFeetMax: group.sqFeetMax, + sqFeetMin: group.sqFeetMin, + rentType: group.rentType, + flatRentValueFrom: group.flatRentValueFrom, + flatRentValueTo: group.flatRentValueTo, + monthlyRent: group.monthlyRent, + totalAvailable: group.totalAvailable, + totalCount: group.totalCount, + accessibilityPriorityType: group.accessibilityPriorityType, + unitGroupAmiLevels: undefined, + unitTypes: { + connect: group.unitTypes.map((type) => ({ + id: combinedUTs.find( + (unitType) => unitType.externalId === type.id, + )?.internalId, + })), + }, + })), + } + : undefined, + units: data.units + ? { + create: data.units.map((unit) => ({ + accessibilityPriorityType: unit.accessibilityPriorityType, + amiPercentage: unit.amiPercentage, + annualIncomeMax: unit.annualIncomeMax, + annualIncomeMin: unit.annualIncomeMin, + bmrProgramChart: unit.bmrProgramChart, + floor: unit.floor, + maxOccupancy: unit.maxOccupancy, + minOccupancy: unit.minOccupancy, + monthlyIncomeMin: unit.monthlyIncomeMin, + monthlyRent: unit.monthlyRent, + monthlyRentAsPercentOfIncome: + unit.monthlyRentAsPercentOfIncome, + numBedrooms: unit.numBedrooms, + numBathrooms: unit.numBathrooms, + number: unit.number, + sqFeet: unit.sqFeet, + + amiChart: undefined, + unitAmiChartOverrides: undefined, + unitRentTypes: unit.unitRentTypes + ? { + connect: { + id: combinedURTs.find( + (unitRentType) => + unitRentType.externalId === unit.unitRentTypes.id, + )?.internalId, + }, + } + : undefined, + unitTypes: unit.unitTypes + ? { + connect: { + id: combinedUTs.find( + (unitType) => + unitType.externalId === unit.unitTypes.id, + )?.internalId, + }, + } + : undefined, + })), + } + : undefined, + }, + }); + } catch (error) { + console.error( + `Error creating external listing with id ${externalListing.id} and name ${externalListing.name}\n ${error}`, + ); + } } } diff --git a/api/src/services/listing.service.ts b/api/src/services/listing.service.ts index 2d1cce85f29..68837a11a3a 100644 --- a/api/src/services/listing.service.ts +++ b/api/src/services/listing.service.ts @@ -1304,7 +1304,7 @@ export class ListingService implements OnModuleInit { result = await this.translationService.translateListing(result, lang); } - if (result.unitGroups.length > 0) { + if (result.unitGroups?.length > 0) { addUnitGroupsSummarized(result); } else { await this.addUnitsSummarized(result); diff --git a/shared-helpers/src/locales/ar.json b/shared-helpers/src/locales/ar.json index aaa5ecb634d..0cc2ed09825 100644 --- a/shared-helpers/src/locales/ar.json +++ b/shared-helpers/src/locales/ar.json @@ -880,6 +880,7 @@ "listings.apply.applicationSeason": "يجب على السكان التقديم في", "listings.apply.applicationWillBeAvailableOn": "سيكون التطبيق متاحًا للتنزيل والاستلام في %{openDate}", "listings.apply.applyOnline": "تقديم الطلب عبر الإنترنت", + "listings.apply.applyOnlineMessage": "* ينقلك هذا الزر إلى موقع إلكتروني خارجي، حيث يمكنك إنشاء حساب والتقدم بطلب لهذا الإعلان.", "listings.apply.downloadApplication": "تنزيل التطبيق", "listings.apply.dropOffApplication": "طلب الاستلام", "listings.apply.dropOffApplicationOrMail": "تسليم الطلب أو إرساله عبر البريد الأمريكي", diff --git a/shared-helpers/src/locales/bn.json b/shared-helpers/src/locales/bn.json index afb6a0d4baa..8b287e717dc 100644 --- a/shared-helpers/src/locales/bn.json +++ b/shared-helpers/src/locales/bn.json @@ -880,6 +880,7 @@ "listings.apply.applicationSeason": "বাসিন্দাদের আবেদন করতে হবে", "listings.apply.applicationWillBeAvailableOn": "%{openDate} এ ডাউনলোড এবং পিক -আপের জন্য আবেদন পাওয়া যাবে", "listings.apply.applyOnline": "অনলাইনে আবেদন করুন", + "listings.apply.applyOnlineMessage": "* এই বোতামটি আপনাকে একটি বাহ্যিক ওয়েবসাইটে নিয়ে যাবে, যেখানে আপনি একটি অ্যাকাউন্ট তৈরি করে এই তালিকাভুক্তির জন্য আবেদন করতে পারবেন।", "listings.apply.downloadApplication": "অ্যাপ্লিকেশন ডাউনলোড করুন", "listings.apply.dropOffApplication": "আবেদনপত্র জমা দিন", "listings.apply.dropOffApplicationOrMail": "আবেদনপত্র জমা দিন অথবা মার্কিন ডাকযোগে পাঠান", diff --git a/shared-helpers/src/locales/es.json b/shared-helpers/src/locales/es.json index e78ebb7c093..e2cfd1ba89c 100644 --- a/shared-helpers/src/locales/es.json +++ b/shared-helpers/src/locales/es.json @@ -880,6 +880,7 @@ "listings.apply.applicationSeason": "Los residentes deben aplicar en", "listings.apply.applicationWillBeAvailableOn": "La solicitud podrá ser descargada y recogida el %{openDate}", "listings.apply.applyOnline": "Haga su solicitud por internet", + "listings.apply.applyOnlineMessage": "*Este botón te lleva a un sitio web externo donde puedes crear una cuenta y solicitar este listado.", "listings.apply.downloadApplication": "Descargar la solicitud", "listings.apply.dropOffApplication": "Lleve la solicitud", "listings.apply.dropOffApplicationOrMail": "Lleve la solicitud o envíela por correo a través del Servicio Postal de los EE.UU.", diff --git a/shared-helpers/src/locales/fa.json b/shared-helpers/src/locales/fa.json index 9bf9df72727..16ffb1e9070 100644 --- a/shared-helpers/src/locales/fa.json +++ b/shared-helpers/src/locales/fa.json @@ -877,6 +877,7 @@ "listings.apply.applicationSeason": "ساکنین باید درخواست دهند", "listings.apply.applicationWillBeAvailableOn": "برنامه برای دانلود و دریافت در %{openDate} در دسترس خواهد بود.", "listings.apply.applyOnline": "درخواست آنلاین", + "listings.apply.applyOnlineMessage": "* این دکمه شما را به یک وب‌سایت خارجی هدایت می‌کند که در آن می‌توانید یک حساب کاربری ایجاد کنید و برای این آگهی درخواست دهید.", "listings.apply.downloadApplication": "دانلود اپلیکیشن", "listings.apply.dropOffApplication": "درخواست ترخیص", "listings.apply.dropOffApplicationOrMail": "درخواست را تحویل دهید یا از طریق پست ایالات متحده ارسال کنید", diff --git a/shared-helpers/src/locales/general.json b/shared-helpers/src/locales/general.json index ed56b280b9c..8945608dc37 100644 --- a/shared-helpers/src/locales/general.json +++ b/shared-helpers/src/locales/general.json @@ -876,6 +876,7 @@ "listings.apply.applicationSeason": "Residents should apply in", "listings.apply.applicationWillBeAvailableOn": "Application will be available for download and pick up on %{openDate}", "listings.apply.applyOnline": "Apply online", + "listings.apply.applyOnlineMessage": "* This button brings you to an external website where you can create an account and apply for this listing.", "listings.apply.downloadApplication": "Download application", "listings.apply.dropOffApplication": "Drop off application", "listings.apply.dropOffApplicationOrMail": "Drop off application or send by US Mail", diff --git a/shared-helpers/src/locales/hy.json b/shared-helpers/src/locales/hy.json index 89726e7fc99..e6cbb954ce5 100644 --- a/shared-helpers/src/locales/hy.json +++ b/shared-helpers/src/locales/hy.json @@ -877,6 +877,7 @@ "listings.apply.applicationSeason": "Բնակիչները պետք է դիմեն", "listings.apply.applicationWillBeAvailableOn": "Դիմումը հասանելի կլինի ներբեռնման և վերցման համար %{openDate}-ին։", "listings.apply.applyOnline": "Դիմել առցանց", + "listings.apply.applyOnlineMessage": "* Այս կոճակը ձեզ կտանի արտաքին կայք, որտեղ կարող եք ստեղծել հաշիվ և դիմել այս ցուցակի համար։", "listings.apply.downloadApplication": "Ներբեռնել ծրագիրը", "listings.apply.dropOffApplication": "Դիմումի ներկայացում", "listings.apply.dropOffApplicationOrMail": "Դիմումը թողեք կամ ուղարկեք ԱՄՆ փոստով", diff --git a/shared-helpers/src/locales/ko.json b/shared-helpers/src/locales/ko.json index db7d0fe09cb..665a4cb2160 100644 --- a/shared-helpers/src/locales/ko.json +++ b/shared-helpers/src/locales/ko.json @@ -877,6 +877,7 @@ "listings.apply.applicationSeason": "주민들은 다음 주소로 신청해야 합니다.", "listings.apply.applicationWillBeAvailableOn": "신청서는 %{openDate}부터 다운로드 및 수령 가능합니다.", "listings.apply.applyOnline": "온라인으로 지원하세요", + "listings.apply.applyOnlineMessage": "* 이 버튼을 누르면 계정을 생성하고 해당 공고에 지원할 수 있는 외부 웹사이트로 이동합니다.", "listings.apply.downloadApplication": "애플리케이션 다운로드", "listings.apply.dropOffApplication": "신청서 제출", "listings.apply.dropOffApplicationOrMail": "신청서를 직접 제출하거나 우편으로 보내주세요.", diff --git a/shared-helpers/src/locales/tl.json b/shared-helpers/src/locales/tl.json index d0e18e105a7..8dcbbcd3729 100644 --- a/shared-helpers/src/locales/tl.json +++ b/shared-helpers/src/locales/tl.json @@ -880,6 +880,7 @@ "listings.apply.applicationSeason": "Dapat mag-apply ang mga residente sa", "listings.apply.applicationWillBeAvailableOn": "Magiging available ang application para i-download at kunin sa %{openDate}", "listings.apply.applyOnline": "Mag-apply online", + "listings.apply.applyOnlineMessage": "* Dinadala ka ng button na ito sa isang panlabas na website kung saan maaari kang lumikha ng isang account at mag-apply para sa listahang ito.", "listings.apply.downloadApplication": "I-download ang application", "listings.apply.dropOffApplication": "I-drop off ang application", "listings.apply.dropOffApplicationOrMail": "Ihulog ang application o ipadala sa US Mail", diff --git a/shared-helpers/src/locales/vi.json b/shared-helpers/src/locales/vi.json index 0e1003dad7a..6b39e818465 100644 --- a/shared-helpers/src/locales/vi.json +++ b/shared-helpers/src/locales/vi.json @@ -880,6 +880,7 @@ "listings.apply.applicationSeason": "Cư dân nên nộp đơn tại", "listings.apply.applicationWillBeAvailableOn": "Đơn ghi danh sẽ có sẵn để tải xuống và tới nhận vào %{openDate}", "listings.apply.applyOnline": "Nộp đơn trực tuyến", + "listings.apply.applyOnlineMessage": "* Nút này sẽ đưa bạn đến một trang web bên ngoài nơi bạn có thể tạo tài khoản và đăng ký danh sách này.", "listings.apply.downloadApplication": "Tải ứng dụng", "listings.apply.dropOffApplication": "Nộp đơn", "listings.apply.dropOffApplicationOrMail": "Nộp đơn hoặc gửi qua Bưu điện Hoa Kỳ", diff --git a/shared-helpers/src/locales/zh.json b/shared-helpers/src/locales/zh.json index c14be1d591a..4bd7a3d67b6 100644 --- a/shared-helpers/src/locales/zh.json +++ b/shared-helpers/src/locales/zh.json @@ -880,6 +880,7 @@ "listings.apply.applicationSeason": "居民应申请", "listings.apply.applicationWillBeAvailableOn": "申請表將可供下載,亦可在 %{openDate} 領取", "listings.apply.applyOnline": "在线申请", + "listings.apply.applyOnlineMessage": "* 此按钮将带您进入外部网站,您可以在其中创建帐户并申请此列表。", "listings.apply.downloadApplication": "下载应用程序", "listings.apply.dropOffApplication": "投递申请", "listings.apply.dropOffApplicationOrMail": "投递申请或通过美国邮政寄送", diff --git a/sites/public/__tests__/components/listing/ListingViewSeedsHelpers.test.ts b/sites/public/__tests__/components/listing/ListingViewSeedsHelpers.test.ts index b9d3f3aaf6f..cc8954d0a14 100644 --- a/sites/public/__tests__/components/listing/ListingViewSeedsHelpers.test.ts +++ b/sites/public/__tests__/components/listing/ListingViewSeedsHelpers.test.ts @@ -269,6 +269,21 @@ describe("ListingViewSeedsHelpers", () => { expect(result.isCommonApp).toBe(true) }) + it("should return online application URL from Internal method with external URL", () => { + const methods: ApplicationMethod[] = [ + { + type: ApplicationMethodsTypeEnum.Internal, + externalReference: "", + } as ApplicationMethod, + ] + + const result = getOnlineApplicationURL(methods, "123", false, "www.externalURL.com") + expect(result.url).toBe( + "www.externalURL.com/applications/start/choose-language?listingId=123" + ) + expect(result.isCommonApp).toBe(true) + }) + it("should include preview parameter when preview is true", () => { const methods: ApplicationMethod[] = [ { diff --git a/sites/public/__tests__/components/listing/listing_sections/Apply.test.tsx b/sites/public/__tests__/components/listing/listing_sections/Apply.test.tsx index 6e2ffe24dc5..3bf6219449b 100644 --- a/sites/public/__tests__/components/listing/listing_sections/Apply.test.tsx +++ b/sites/public/__tests__/components/listing/listing_sections/Apply.test.tsx @@ -145,6 +145,45 @@ describe("", () => { expect(screen.queryByText("Download application")).not.toBeInTheDocument() }) + it("shows apply online button for internal online application for external listing", () => { + render( + null} + /> + ) + expect(screen.getByRole("heading", { level: 2, name: "How to apply" })).toBeInTheDocument() + expect(screen.getByText("Apply online")).toBeInTheDocument() + expect( + screen.getByText( + "* This button brings you to an external website where you can create an account and apply for this listing." + ) + ).toBeInTheDocument() + expect(screen.getByRole("link", { name: "Apply online" })).toHaveAttribute( + "href", + `www.externalURL.com/applications/start/choose-language?listingId=${listing.id}` + ) + + expect(screen.queryByText("Download application")).not.toBeInTheDocument() + }) + it("shows redirected apply online button for internal online application with mandated accounts on while signed out", () => { process.env.showMandatedAccounts = "TRUE" render( diff --git a/sites/public/src/components/listing/GetApplication.tsx b/sites/public/src/components/listing/GetApplication.tsx index 5758aa2a559..5fe5f82c7d6 100644 --- a/sites/public/src/components/listing/GetApplication.tsx +++ b/sites/public/src/components/listing/GetApplication.tsx @@ -43,9 +43,12 @@ export interface ApplicationsProps { listingStatus?: string /** Whether or not to block submission of test application */ preview?: boolean + /** Whether or not to show apply online helper text */ + isExternal?: boolean strings?: { applicationsOpenInFuture?: string applyOnline?: string + applyOnlineMessage?: string downloadApplication?: string getAPaperApplication?: string getDirections?: string @@ -105,6 +108,10 @@ const GetApplication = (props: ApplicationsProps) => { {props.strings?.applyOnline ?? t("listings.apply.applyOnline")} )} +
+ {props.isExternal && + (props.strings?.applyOnlineMessage ?? t("listings.apply.applyOnlineMessage"))} +
)} diff --git a/sites/public/src/components/listing/ListingView.tsx b/sites/public/src/components/listing/ListingView.tsx index 3fca5b83098..e9711b89768 100644 --- a/sites/public/src/components/listing/ListingView.tsx +++ b/sites/public/src/components/listing/ListingView.tsx @@ -76,10 +76,10 @@ interface ListingProps { } const getUnhiddenMultiselectQuestions = ( - arrayToSeach: ListingMultiselectQuestion[], + arrayToSearch: ListingMultiselectQuestion[], section: MultiselectQuestionsApplicationSectionEnum ): ListingMultiselectQuestion[] => { - return arrayToSeach.filter( + return arrayToSearch.filter( (elem) => elem.multiselectQuestions.applicationSection === section && !elem.multiselectQuestions.hideFromListing @@ -376,7 +376,7 @@ export const ListingView = (props: ListingProps) => { let onlineApplicationURL let isCommonApp = false if (hasMethod(listing.applicationMethods, ApplicationMethodsTypeEnum.Internal)) { - onlineApplicationURL = `/applications/start/choose-language?listingId=${listing.id}` + onlineApplicationURL = `${listing.externalURL}/applications/start/choose-language?listingId=${listing.id}` onlineApplicationURL += `${props.preview ? "&preview=true" : ""}` isCommonApp = true } else if (hasMethod(listing.applicationMethods, ApplicationMethodsTypeEnum.ExternalLink)) { @@ -435,6 +435,7 @@ export const ListingView = (props: ListingProps) => { initialStateLoaded && !profile && onlineApplicationURLInfo.isCommonApp && + !listing.externalURL && //previewing applications should not require admin to login !props.preview @@ -462,6 +463,7 @@ export const ListingView = (props: ListingProps) => { listingName={listing.name} listingId={listing.id} listingStatus={listing.status} + isExternal={!listing.externalURL} /> {listing.status !== ListingsStatusEnum.closed && submissionAddressExists && ( {isFeatureFlagOn(jurisdiction, FeatureFlagEnum.enableNonRegulatedListings) && listing.listingType === EnumListingListingType.nonRegulated && ( - + )} ) diff --git a/sites/public/src/components/listing/ListingViewSeedsHelpers.tsx b/sites/public/src/components/listing/ListingViewSeedsHelpers.tsx index e022014b9e9..5bb87b5a04c 100644 --- a/sites/public/src/components/listing/ListingViewSeedsHelpers.tsx +++ b/sites/public/src/components/listing/ListingViewSeedsHelpers.tsx @@ -111,12 +111,15 @@ export const getPaperApplications = (applicationMethods: ApplicationMethod[]) => export const getOnlineApplicationURL = ( applicationMethods: ApplicationMethod[], listingId: string, - preview: boolean + preview: boolean, + externalURL = "" ) => { let onlineApplicationURL let isCommonApp = false if (hasMethod(applicationMethods, ApplicationMethodsTypeEnum.Internal)) { - onlineApplicationURL = `/applications/start/choose-language?listingId=${listingId}` + onlineApplicationURL = `${ + externalURL ? externalURL : "" + }/applications/start/choose-language?listingId=${listingId}` onlineApplicationURL += `${preview ? "&preview=true" : ""}` isCommonApp = true } else if (hasMethod(applicationMethods, ApplicationMethodsTypeEnum.ExternalLink)) { diff --git a/sites/public/src/components/listing/listing_sections/Apply.tsx b/sites/public/src/components/listing/listing_sections/Apply.tsx index aaede0d93ae..766083380f7 100644 --- a/sites/public/src/components/listing/listing_sections/Apply.tsx +++ b/sites/public/src/components/listing/listing_sections/Apply.tsx @@ -31,11 +31,11 @@ export const Apply = ({ listing, preview, setShowDownloadModal }: ApplyProps) => const { initialStateLoaded, profile } = useContext(AuthContext) const applicationsClosed = dayjs() > dayjs(listing.applicationDueDate) - const onlineApplicationURLInfo = getOnlineApplicationURL( listing.applicationMethods, listing.id, - preview + preview, + listing.externalURL ) const redirectIfLogInRequired = () => @@ -43,6 +43,7 @@ export const Apply = ({ listing, preview, setShowDownloadModal }: ApplyProps) => initialStateLoaded && !profile && onlineApplicationURLInfo.isCommonApp && + !listing.externalURL && //previewing applications should not require admin to login !preview @@ -148,7 +149,10 @@ export const Apply = ({ listing, preview, setShowDownloadModal }: ApplyProps) => {t("listings.apply.howToApply")} {hasPrimaryApplicationMethod && ( - <>{onlineApplicationUrl ? ApplyOnlineButton : DownloadApplicationButton} + <> + {onlineApplicationUrl ? ApplyOnlineButton : DownloadApplicationButton} + {listing.externalURL && t("listings.apply.applyOnlineMessage")} + )} {showSecondarySection && ( diff --git a/sites/public/src/components/listing/listing_sections/OtherFeatures.tsx b/sites/public/src/components/listing/listing_sections/OtherFeatures.tsx index 4b9fd7cb029..79cd2709c1e 100644 --- a/sites/public/src/components/listing/listing_sections/OtherFeatures.tsx +++ b/sites/public/src/components/listing/listing_sections/OtherFeatures.tsx @@ -3,17 +3,17 @@ import styles from "../ListingViewSeeds.module.scss" import { t } from "@bloom-housing/ui-components" type OtherFeaturesProps = { - hasEbllClearence: boolean + hasEbllClearance: boolean } -export const OtherFeatures = ({ hasEbllClearence }: OtherFeaturesProps) => { +export const OtherFeatures = ({ hasEbllClearance }: OtherFeaturesProps) => { return (
diff --git a/sites/public/src/pages/applications/start/choose-language.tsx b/sites/public/src/pages/applications/start/choose-language.tsx index 98ec36de661..7fcb4ba735c 100644 --- a/sites/public/src/pages/applications/start/choose-language.tsx +++ b/sites/public/src/pages/applications/start/choose-language.tsx @@ -142,7 +142,7 @@ const ApplicationChooseLanguage = () => { } }, [listing, router, toastyRef]) - const imageUrl = listing?.assets + const imageUrl = listing?.listingImages ? imageUrlFromListing(listing, parseInt(process.env.listingPhotoSize))[0] : "" diff --git a/sites/public/src/pages/listing/[id].tsx b/sites/public/src/pages/listing/[id].tsx index 9a1f4ab14b7..cb4303581c9 100644 --- a/sites/public/src/pages/listing/[id].tsx +++ b/sites/public/src/pages/listing/[id].tsx @@ -14,13 +14,16 @@ export async function getServerSideProps(context: { params: Record locale: string }) => { - let response + let response: AxiosResponse try { response = await axios.get(`${process.env.backendApiBase}/listings/${context.params.id}`, { headers: { @@ -112,6 +112,20 @@ export const getStaticProps: GetStaticProps = async (context: { } const jurisdiction = fetchJurisdictionByName() + if (response.data.externalURL) { + try { + response = await axios.get( + `${response.data.externalURL}/listings/external/${response.data.externalListingId}`, + { headers: { language: context.locale } } + ) + // re-setting the externalURL field with the publicUrl if we need to redirect to the external site + response.data.externalURL = response.data.jurisdiction.publicUrl + } catch (e) { + console.error(`Error fetching external listing data: ${e}`) + return { notFound: true } + } + } + return { props: { listing: response.data, jurisdiction: await jurisdiction }, revalidate: Number(process.env.cacheRevalidate), From 6683b91df56b25ef81a26fbb84a11c91c5a06f62 Mon Sep 17 00:00:00 2001 From: Eric McGarry Date: Tue, 21 Jul 2026 12:15:59 -0400 Subject: [PATCH 2/2] feat: add div with padding 6367 --- .../public/src/components/listing/listing_sections/Apply.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sites/public/src/components/listing/listing_sections/Apply.tsx b/sites/public/src/components/listing/listing_sections/Apply.tsx index 766083380f7..c8afd09c711 100644 --- a/sites/public/src/components/listing/listing_sections/Apply.tsx +++ b/sites/public/src/components/listing/listing_sections/Apply.tsx @@ -151,7 +151,9 @@ export const Apply = ({ listing, preview, setShowDownloadModal }: ApplyProps) => {hasPrimaryApplicationMethod && ( <> {onlineApplicationUrl ? ApplyOnlineButton : DownloadApplicationButton} - {listing.externalURL && t("listings.apply.applyOnlineMessage")} +
+ {listing.externalURL && t("listings.apply.applyOnlineMessage")} +
)}