Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@
"riskCaption": "بناءً على مزيج تصنيفات السندات في حيازاتك.",
"activityTitle": "النشاط",
"activityNote": "كل إدخال → Stellar Expert",
"transactionHashLabel": "هاش المعاملة",
"riskScore": "Portfolio risk score",
"riskTierConservative": "Conservative",
"riskTierModerate": "Moderate",
Expand Down Expand Up @@ -394,11 +393,6 @@
"scoreGaugeAria": "{label}: {value} من {max}",
"onChainNote": "كل تحديث Oracle على السلسلة ↗",
"verifiedAgo": "تحقق قبل 2 ساعة ↗",
"pricingTitle": "سجل أسعار وعوائد السندات",
"priceLabel": "سجل الأسعار (% من القيمة الاسمية)",
"priceHistory": "سجل الأسعار",
"yieldLabel": "العائد حتى تاريخ الاستحقاق",
"yieldHistory": "سجل العائد",
"fundingTitle": "جدول التمويل",
"fundingGoalLabel": "{raised} م collects من هدف {goal}",
"contributionTitle": "مساهمة هذا المشروع",
Expand All @@ -407,6 +401,12 @@
"calcFormula": "العائد المتوقع = الاستثمار × (الائتمان + الأخضر) ÷ 200 — اقرأه في العقد ↗",
"investCta": "استثمر في المجمع الذي يمول هذا",
"investNote": "أنت تستثمر في المجمع المشترك، الذي يمول هذا المشروع إلى جانب أخرى — ليست عملية شراء لكل مشروع."
,
"pricingTitle": "تسعير السند",
"priceHistory": "سجل الأسعار",
"priceLabel": "السعر",
"yieldHistory": "سجل العائد",
"yieldLabel": "العائد"
},
"PasswordResetEmail": {
"subject": "إعادة تعيين كلمة مرور Heliobond",
Expand Down
12 changes: 6 additions & 6 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@
"riskCaption": "Based on the mix of bond ratings in your holdings.",
"activityTitle": "Activity",
"activityNote": "every entry → Stellar Expert",
"transactionHashLabel": "transaction hash",
"referralProgram": "Referral program",
"referralLink": "Your referral link",
"referralCaption": "Share your link to invite friends. You both get rewards when they invest.",
Expand Down Expand Up @@ -383,11 +382,6 @@
"scoreGaugeAria": "{label}: {value} out of {max}",
"onChainNote": "every oracle update is on-chain ↗",
"verifiedAgo": "verified 2h ago ↗",
"pricingTitle": "Bond pricing & yield history",
"priceLabel": "Price history (% of par)",
"priceHistory": "Price history",
"yieldLabel": "Yield to maturity",
"yieldHistory": "Yield history",
"fundingTitle": "Funding timeline",
"fundingGoalLabel": "{raised} raised of {goal} goal",
"contributionTitle": "This project's contribution",
Expand All @@ -396,6 +390,12 @@
"calcFormula": "expected return = investment × (credit + green) ÷ 200 — read it in the contract ↗",
"investCta": "Invest in the pool that funds this",
"investNote": "You invest in the shared pool, which funds this project alongside others — not a per-project checkout."
,
"pricingTitle": "Bond pricing",
"priceHistory": "Price history",
"priceLabel": "Price",
"yieldHistory": "Yield history",
"yieldLabel": "Yield"
},
"PasswordResetEmail": {
"subject": "Reset your Heliobond password",
Expand Down
12 changes: 6 additions & 6 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@
"riskCaption": "Basado en la combinación de calificaciones de bonos en sus tenencias.",
"activityTitle": "Activity",
"activityNote": "every entry → Stellar Expert",
"transactionHashLabel": "hash de transacción",
"riskScore": "Portfolio risk score",
"riskTierConservative": "Conservative",
"riskTierModerate": "Moderate",
Expand Down Expand Up @@ -387,11 +386,6 @@
"scoreGaugeAria": "{label}: {value} out of {max}",
"onChainNote": "every oracle update is on-chain ↗",
"verifiedAgo": "verified 2h ago ↗",
"pricingTitle": "Historial de precios y rendimiento del bono",
"priceLabel": "Historial de precios (% a la par)",
"priceHistory": "Historial de precios",
"yieldLabel": "Rendimiento al vencimiento",
"yieldHistory": "Historial de rendimiento",
"fundingTitle": "Funding timeline",
"fundingGoalLabel": "{raised} raised of {goal} goal",
"contributionTitle": "This project's contribution",
Expand All @@ -400,6 +394,12 @@
"calcFormula": "rentabilidad esperada = inversión × (crédito + impacto verde) ÷ 200 — consulta el contrato ↗",
"investCta": "Invest in the pool that funds this",
"investNote": "You invest in the shared pool, which funds this project alongside others — not a per-project checkout."
,
"pricingTitle": "Precio del bono",
"priceHistory": "Historial del precio",
"priceLabel": "Precio",
"yieldHistory": "Historial del rendimiento",
"yieldLabel": "Rendimiento"
},
"PasswordResetEmail": {
"subject": "Reset your Heliobond password",
Expand Down
12 changes: 6 additions & 6 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@
"riskCaption": "Basé sur la répartition des notations d'obligations dans vos avoirs.",
"activityTitle": "Activité",
"activityNote": "chaque entrée → Stellar Expert",
"transactionHashLabel": "le hash de transaction",
"riskScore": "Portfolio risk score",
"riskTierConservative": "Conservative",
"riskTierModerate": "Moderate",
Expand Down Expand Up @@ -383,11 +382,6 @@
"scoreGaugeAria": "{label} : {value} sur {max}",
"onChainNote": "chaque mise à jour oracle est sur la chaîne ↗",
"verifiedAgo": "vérifié il y a 2h ↗",
"pricingTitle": "Historique des prix et des rendements",
"priceLabel": "Historique des prix (% du pair)",
"priceHistory": "Historique des prix",
"yieldLabel": "Rendement à l'échéance",
"yieldHistory": "Historique des rendements",
"fundingTitle": "Chronologie de financement",
"fundingGoalLabel": "{raised} collectés sur un objectif de {goal}",
"contributionTitle": "Contribution de ce projet",
Expand All @@ -396,6 +390,12 @@
"calcFormula": "rendement attendu = investissement × (crédit + vert) ÷ 200 — lisez-le dans le contrat ↗",
"investCta": "Investir dans le pool qui finance cela",
"investNote": "Vous investissez dans le pool commun, qui finance ce projet parmi d'autres — pas une caisse par projet."
,
"pricingTitle": "Cours de l'obligation",
"priceHistory": "Historique des prix",
"priceLabel": "Prix",
"yieldHistory": "Historique des rendements",
"yieldLabel": "Rendement"
},
"PasswordResetEmail": {
"subject": "Réinitialisez votre mot de passe Heliobond",
Expand Down
12 changes: 6 additions & 6 deletions messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@
"riskCaption": "Based on the mix of bond ratings in your holdings.",
"activityTitle": "Activity",
"activityNote": "every entry → Stellar Expert",
"transactionHashLabel": "transaction hash",
"referralProgram": "Referral program",
"referralLink": "Your referral link",
"referralCaption": "Share your link to invite friends. You both get rewards when they invest.",
Expand Down Expand Up @@ -376,11 +375,6 @@
"scoreGaugeAria": "{label}: {value} out of {max}",
"onChainNote": "every oracle update is on-chain ↗",
"verifiedAgo": "verified 2h ago ↗",
"pricingTitle": "Histórico de preços e rendimentos do título",
"priceLabel": "Histórico de preços (% do valor nominal)",
"priceHistory": "Histórico de preços",
"yieldLabel": "Rendimento até o vencimento",
"yieldHistory": "Histórico de rendimentos",
"fundingTitle": "Funding timeline",
"fundingGoalLabel": "{raised} raised of {goal} goal",
"contributionTitle": "This project's contribution",
Expand All @@ -389,6 +383,12 @@
"calcFormula": "expected return = investment × (credit + green) ÷ 200 — read it in the contract ↗",
"investCta": "Invest in the pool that funds this",
"investNote": "You invest in the shared pool, which funds this project alongside others — not a per-project checkout."
,
"pricingTitle": "Preço do título",
"priceHistory": "Histórico de preços",
"priceLabel": "Preço",
"yieldHistory": "Histórico de rendimento",
"yieldLabel": "Rendimento"
},
"PasswordResetEmail": {
"subject": "Redefinição de Senha - Heliobond",
Expand Down
4 changes: 2 additions & 2 deletions src/alerts/YieldAlertProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
type AlertOperator,
} from '../lib/yieldAlerts'
import { useToast } from '../components/Toast'
import { HB_DATA } from '../data'
import { selectProjects } from '../state/selectors'

interface YieldAlertContextValue {
/** All saved alerts, most-recently-added last. */
Expand Down Expand Up @@ -84,7 +84,7 @@ export function YieldAlertProvider({ children }: { children: ReactNode }) {
const current = alertsRef.current
if (current.length === 0) return

const projects = HB_DATA.projects
const projects = selectProjects()
const triggered = evaluateAlerts(current, projects)

if (triggered.length === 0) return
Expand Down
25 changes: 15 additions & 10 deletions src/app/project/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTranslations } from 'next-intl'
import { Button } from '../../../components'
import { ProjectDetail } from '../../../screens/ProjectDetail'
import { getProject, type ProjectWithDetail } from '../../../lib/api'
import { PriceHistoryChart } from '../../../components/PriceHistoryChart'

export default function ProjectDetailPage() {
const router = useRouter()
Expand All @@ -26,7 +27,13 @@ export default function ProjectDetailPage() {
}, [id])

if (data === 'loading') {
return <div id="main-content" aria-label="Loading project" style={{ maxWidth: 860, margin: '0 auto', padding: '40px 24px 96px' }}>Loading...</div>
return <div
id="main-content"
aria-label="Loading project"
style={{ maxWidth: 860, margin: '0 auto', padding: '40px 24px 96px' }}
>
Loading...
</div>
}

if (!data) {
Expand All @@ -40,14 +47,12 @@ export default function ProjectDetailPage() {
}

return (
<ProjectDetail
project={data.project}
detail={data.detail}
onInvest={async () => {
router.push('/connect')
return ''
}}
onBack={() => router.push('/explore')}
/>
<>
<ProjectDetail project={data.project} detail={data.detail} onInvest={() => {
router.push('/connect')
return Promise.resolve('/connect')
}} onBack={() => router.push('/explore')} />
<PriceHistoryChart projectId={id} />
</>
)
}
4 changes: 2 additions & 2 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MetadataRoute } from 'next'
import { HB_DATA } from '../data'
import { selectProjects } from '../state/selectors'

export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://heliobond.vercel.app'
Expand Down Expand Up @@ -31,7 +31,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
},
]

const projectRoutes = HB_DATA.projects.map((project) => ({
const projectRoutes = selectProjects().map((project) => ({
url: `${baseUrl}/project/${project.id}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
Expand Down
15 changes: 6 additions & 9 deletions src/components/AddressVerificationForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,12 @@ describe('AddressVerificationForm', () => {
expect(onSubmit).toHaveBeenCalled()
})

// Additional security-focused edge case tests for all fields
const setField = (label: string, value: string) => {
const el = screen.getByLabelText(label) as HTMLInputElement | HTMLSelectElement
if (el.tagName === 'SELECT') {
const opt = document.createElement('option')
opt.value = value
el.appendChild(opt)
}
fireEvent.change(el, { target: { value } })
fireEvent.change(screen.getByLabelText(label), { target: { value } })
}

const fillForm = (values: Record<string, string>) => {
const fillForm = (values: { street: string; city: string; state: string; zip: string; country: string; apartment?: string }) => {
setField('Street address *', values.street)
setField('City *', values.city)
setField('State / Province *', values.state)
Expand All @@ -69,7 +64,9 @@ describe('AddressVerificationForm', () => {
{ name: 'city', error: 'City contains invalid characters' },
{ name: 'state', error: 'State / Province contains invalid characters' },
{ name: 'zip', error: 'ZIP / Postal code contains invalid characters' },
{ name: 'country', error: 'Country contains invalid characters' },
// NOTE: `country` is intentionally excluded — it renders as a fixed
// <select> whitelist, so free-text XSS/SQL payloads can never be entered.
// A select whitelist is the injection defense for that field.
]

fieldTestCases.forEach(({ name, error }) => {
Expand Down
25 changes: 25 additions & 0 deletions src/components/PriceHistoryChart.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import PriceHistoryChart from './PriceHistoryChart'
import { selectPriceHistory } from '../state/selectors'

describe('PriceHistoryChart', () => {
it('renders price and yield series for a project with history', () => {
render(<PriceHistoryChart projectId={1} />)
expect(screen.getByLabelText('Bond price history')).toBeInTheDocument()
expect(screen.getByLabelText('Price history trend')).toBeInTheDocument()
expect(screen.getByLabelText('Yield history trend')).toBeInTheDocument()
})

it('reflects the latest observation from the flat selector', () => {
render(<PriceHistoryChart projectId={1} />)
const history = selectPriceHistory(1)
const last = history[history.length - 1]
expect(screen.getByText(`${last.yield.toFixed(2)}%`)).toBeInTheDocument()
})

it('renders nothing for an unknown project (safe empty state)', () => {
const { container } = render(<PriceHistoryChart projectId={9999} />)
expect(container).toBeEmptyDOMElement()
})
})
Loading
Loading