diff --git a/src/app/developers/tokenize/page.tsx b/src/app/developers/tokenize/page.tsx index 19c84818..e8db083a 100644 --- a/src/app/developers/tokenize/page.tsx +++ b/src/app/developers/tokenize/page.tsx @@ -12,7 +12,8 @@ import { ArrowLeft, Upload, Info, - ShieldCheck + ShieldCheck, + Monitor } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -351,6 +352,32 @@ export default function TokenizationWizardPage() { + + {/* Widget Embed Card */} + + +
+
+ +
+
+

+ Embed Investment Calculator on Your Website +

+

+ Drive more investors to your properties with our embeddable calculator widget. + Customize branding, show projected returns, and add a direct "Invest on PropChain" CTA button. +

+ + + +
+
+
+
); diff --git a/src/app/properties/[id]/PropertyDetailPageClient.tsx b/src/app/properties/[id]/PropertyDetailPageClient.tsx new file mode 100644 index 00000000..c1aea4c2 --- /dev/null +++ b/src/app/properties/[id]/PropertyDetailPageClient.tsx @@ -0,0 +1,61 @@ +'use client'; + +import React from 'react'; +import { PropertyDetail } from '@/components/PropertyDetail'; +import { WalletConnector } from '@/components/WalletConnector'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { ArrowLeft, Monitor } from 'lucide-react'; + +interface PropertyDetailPageClientProps { + propertyId: string; +} + +export function PropertyDetailPageClient({ propertyId }: PropertyDetailPageClientProps) { + return ( +
+ {/* Header */} +
+
+
+
+ + + +
+
+ PC +
+

+ PropChain +

+
+
+
+ + + + + + + +
+
+
+
+ + {/* Property Detail */} +
+ +
+
+ ); +} diff --git a/src/app/properties/[id]/page.tsx b/src/app/properties/[id]/page.tsx index 3b1a7114..7bc0d862 100644 --- a/src/app/properties/[id]/page.tsx +++ b/src/app/properties/[id]/page.tsx @@ -1,5 +1,40 @@ -'use client'; +import { Suspense } from 'react'; +import type { Metadata } from 'next'; +import { PropertyDetailPageClient } from './PropertyDetailPageClient'; +import { propertyService } from '@/lib/propertyService'; +interface PropertyDetailPageProps { + params: Promise<{ id: string }>; +} + +export async function generateMetadata({ + params, +}: PropertyDetailPageProps): Promise { + const { id } = await params; + + try { + const property = await propertyService.getPropertyById(id); + if (!property) { + return { + title: 'Property Not Found - PropChain', + description: 'The requested property could not be found.', + }; + } + return { + title: `${property.name} - PropChain`, + description: property.description, + openGraph: { + title: `${property.name} - PropChain`, + description: `${property.name} in ${property.location.city}, ${property.location.state} — ${property.propertyType} investment from $${property.price.perToken.toLocaleString()} per token.`, + }, + }; + } catch { + return { + title: 'Property Not Found - PropChain', + description: 'The requested property could not be found.', + }; + } +} import React from 'react'; import { useParams } from 'next/navigation'; import { PropertyDetail } from '@/components/PropertyDetail'; @@ -9,11 +44,24 @@ import { Button } from '@/components/ui/button'; import Link from 'next/link'; import { ArrowLeft, Loader2 } from 'lucide-react'; -export default function PropertyDetailPage() { - const params = useParams(); - const propertyId = params?.id as string; +export default async function PropertyDetailPage({ + params, +}: PropertyDetailPageProps) { + const { id } = await params; return ( + +
+
+

Loading property...

+
+
+ } + > + +
{/* Header */}
diff --git a/src/app/widget/embed-code/page.tsx b/src/app/widget/embed-code/page.tsx new file mode 100644 index 00000000..4cd3476f --- /dev/null +++ b/src/app/widget/embed-code/page.tsx @@ -0,0 +1,318 @@ +'use client'; + +import React, { useState, useCallback } from 'react'; +import Link from 'next/link'; +import { ArrowLeft, Copy, Check, Code, ExternalLink } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Slider } from '@/components/ui/slider'; +import { InvestmentCalculatorWidget } from '@/components/widget/InvestmentCalculatorWidget'; + +const BASE_URL = typeof window !== 'undefined' ? window.location.origin : 'https://propchain.io'; + +export default function WidgetEmbedCodePage() { + const [primaryColor, setPrimaryColor] = useState('#2563eb'); + const [secondaryColor, setSecondaryColor] = useState('#1e40af'); + const [brandName, setBrandName] = useState('PropChain'); + const [logoUrl, setLogoUrl] = useState(''); + const [investment, setInvestment] = useState(1000); + const [yieldRate, setYieldRate] = useState(8); + const [ctaText, setCtaText] = useState('Invest on PropChain'); + const [compact, setCompact] = useState(false); + const [copied, setCopied] = useState(false); + + const widgetWidth = compact ? '400px' : '600px'; + const widgetHeight = compact ? '450px' : '800px'; + + const generateEmbedCode = useCallback(() => { + const params = new URLSearchParams(); + params.set('primaryColor', primaryColor); + params.set('secondaryColor', secondaryColor); + params.set('brandName', brandName); + if (logoUrl) params.set('logo', logoUrl); + params.set('investment', String(investment)); + params.set('yield', String(yieldRate)); + params.set('ctaText', ctaText); + if (compact) params.set('compact', 'true'); + + const widgetUrl = `${BASE_URL}/widget/investment-calculator?${params.toString()}`; + + return ``; + }, [primaryColor, secondaryColor, brandName, logoUrl, investment, yieldRate, ctaText, compact, widgetHeight, widgetWidth]); + + const handleCopy = async () => { + const code = generateEmbedCode(); + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Header */} +
+
+
+ + + +
+
+ PC +
+

+ Widget Embed Generator +

+
+
+
+
+ +
+
+ {/* Customization Panel */} +
+ + + Widget Configuration + Customize the appearance and behavior of your investment calculator widget + + +
+ + setBrandName(e.target.value)} + placeholder="Your Brand" + /> +
+ +
+ + setLogoUrl(e.target.value)} + placeholder="https://example.com/logo.png" + /> +
+ +
+ +
+ { + setPrimaryColor(e.target.value); + setSecondaryColor(e.target.value); + }} + className="w-16 h-10" + /> + setPrimaryColor(e.target.value)} + /> +
+
+ +
+ + setInvestment(v[0])} + /> +
+ +
+ + setYieldRate(v[0])} + /> +
+ +
+ + setCtaText(e.target.value)} + /> +
+ +
+ setCompact(e.target.checked)} + className="w-4 h-4" + /> + +
+
+
+ + {/* Embed Code */} + + + + + Embed Code + + Copy and paste this code into your website + + +
+
+                    {generateEmbedCode()}
+                  
+ +
+
+
+
+ + {/* Live Preview */} +
+ + + Live Preview + This is how your widget will look + + + + + +
+
+ + {/* Documentation */} + + + Integration Guide + + +
+

URL Parameters

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
primaryColorstring#2563ebPrimary brand color
secondaryColorstring#1e40afHover state color
brandNamestringPropChainBrand name displayed in widget
logostring-Logo URL (optional)
investmentnumber1000Default investment amount
yieldnumber8Default annual yield %
ctaTextstringInvest on PropChainCTA button text
compactbooleanfalseEnable compact mode
+
+
+ +
+

Pro Tips

+
    +
  • • Use compact=true for sidebar placements
  • +
  • • The widget is fully responsive and will adapt to your container width
  • +
  • • All CTA clicks open in a new tab with noopener security
  • +
  • • The widget loads asynchronously with loading="lazy" for optimal performance
  • +
+
+
+
+
+
+ ); +} diff --git a/src/app/widget/investment-calculator/page.tsx b/src/app/widget/investment-calculator/page.tsx new file mode 100644 index 00000000..a8e94c5b --- /dev/null +++ b/src/app/widget/investment-calculator/page.tsx @@ -0,0 +1,48 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { InvestmentCalculatorWidget } from '@/components/widget/InvestmentCalculatorWidget'; + +export default function InvestmentCalculatorEmbedPage() { + const searchParams = useSearchParams(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return
; + } + + const theme = { + primaryColor: searchParams.get('primaryColor') || '#2563eb', + secondaryColor: searchParams.get('secondaryColor') || '#1e40af', + backgroundColor: searchParams.get('backgroundColor') || '#ffffff', + textColor: searchParams.get('textColor') || '#111827', + logoUrl: searchParams.get('logo') || undefined, + brandName: searchParams.get('brandName') || 'PropChain', + }; + + const defaultInvestment = Number(searchParams.get('investment')) || 1000; + const defaultYield = Number(searchParams.get('yield')) || 8; + const ctaUrl = searchParams.get('ctaUrl') || 'https://propchain.io/properties'; + const ctaText = searchParams.get('ctaText') || 'Invest on PropChain'; + const propertyId = searchParams.get('propertyId') || undefined; + const compact = searchParams.get('compact') === 'true'; + + return ( +
+ +
+ ); +} diff --git a/src/components/widget/InvestmentCalculatorWidget.tsx b/src/components/widget/InvestmentCalculatorWidget.tsx new file mode 100644 index 00000000..52e5b27b --- /dev/null +++ b/src/components/widget/InvestmentCalculatorWidget.tsx @@ -0,0 +1,419 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; + +interface WidgetTheme { + primaryColor: string; + secondaryColor: string; + backgroundColor: string; + textColor: string; + logoUrl?: string; + brandName: string; +} + +interface CalculatorResults { + totalReturn: number; + annualReturn: number; + roi: number; + breakEvenMonths: number; + irr: number; + finalValue: number; + totalRentalIncome: number; + capitalGains: number; +} + +interface InvestmentCalculatorWidgetProps { + theme?: Partial; + defaultInvestment?: number; + defaultYield?: number; + ctaUrl?: string; + ctaText?: string; + propertyId?: string; + compact?: boolean; +} + +const DEFAULT_THEME: WidgetTheme = { + primaryColor: '#2563eb', + secondaryColor: '#1e40af', + backgroundColor: '#ffffff', + textColor: '#111827', + brandName: 'PropChain', +}; + +function formatCurrency(value: number): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 0, + }).format(value); +} + +export function InvestmentCalculatorWidget({ + theme = {}, + defaultInvestment = 1000, + defaultYield = 8, + ctaUrl = 'https://propchain.io/properties', + ctaText = 'Invest on PropChain', + propertyId, + compact = false, +}: InvestmentCalculatorWidgetProps) { + const [investment, setInvestment] = useState(defaultInvestment); + const [yieldRate, setYieldRate] = useState(defaultYield); + const [holdingPeriod, setHoldingPeriod] = useState(5); + const [appreciation, setAppreciation] = useState(3); + const [results, setResults] = useState({ + totalReturn: 0, + annualReturn: 0, + roi: 0, + breakEvenMonths: 0, + irr: 0, + finalValue: 0, + totalRentalIncome: 0, + capitalGains: 0, + }); + + const mergedTheme: WidgetTheme = { ...DEFAULT_THEME, ...theme }; + + const calculate = useCallback(() => { + const annualRentalIncome = investment * (yieldRate / 100); + const totalRentalIncome = annualRentalIncome * holdingPeriod; + const finalValue = investment * Math.pow(1 + appreciation / 100, holdingPeriod); + const capitalGains = finalValue - investment; + const totalReturn = totalRentalIncome + capitalGains; + const roi = (totalReturn / investment) * 100; + const annualReturn = totalReturn / holdingPeriod; + const breakEvenMonths = annualRentalIncome > 0 + ? Math.ceil((investment / annualRentalIncome) * 12) + : 0; + const irr = (Math.pow((investment + totalReturn) / investment, 1 / holdingPeriod) - 1) * 100; + + setResults({ + totalReturn, + annualReturn, + roi, + breakEvenMonths, + irr, + finalValue, + totalRentalIncome, + capitalGains, + }); + }, [investment, yieldRate, holdingPeriod, appreciation]); + + useEffect(() => { + calculate(); + }, [calculate]); + + const handleCtaClick = () => { + const url = propertyId + ? `${ctaUrl.replace(/\/properties$/, `/properties/${propertyId}`)}` + : ctaUrl; + window.open(url, '_blank', 'noopener,noreferrer'); + }; + + const sliderStyle: React.CSSProperties = { + accentColor: mergedTheme.primaryColor, + }; + + if (compact) { + return ( +
+ {mergedTheme.logoUrl && ( +
+ {mergedTheme.brandName} +
+ )} + +
+

+ Investment Calculator +

+

+ Project your returns +

+
+ +
+ + setInvestment(Number(e.target.value))} + style={{ ...sliderStyle, width: '100%' }} + /> +
+ +
+ + setYieldRate(Number(e.target.value))} + style={{ ...sliderStyle, width: '100%' }} + /> +
+ +
+
+

Projected Value ({holdingPeriod} yrs)

+

+ {formatCurrency(investment + results.totalReturn)} +

+
+
+ + + +

+ Powered by {mergedTheme.brandName} +

+
+ ); + } + + return ( +
+ {mergedTheme.logoUrl && ( +
+ {mergedTheme.brandName} +
+ )} + +
+

+ Investment Calculator +

+

+ Estimate your potential returns from tokenized real estate +

+
+ +
+
+ + setInvestment(Number(e.target.value))} + style={{ + width: '100%', + padding: '10px', + borderRadius: '8px', + border: `1px solid ${mergedTheme.primaryColor}30`, + fontSize: '14px', + boxSizing: 'border-box', + }} + /> +
+ +
+ + setYieldRate(Number(e.target.value))} + style={{ ...sliderStyle, width: '100%' }} + /> +
+ +
+ + setHoldingPeriod(Number(e.target.value))} + style={{ ...sliderStyle, width: '100%' }} + /> +
+ +
+ + setAppreciation(Number(e.target.value))} + style={{ ...sliderStyle, width: '100%' }} + /> +
+
+ +
+

+ Projected Returns +

+ +
+
+

Projected ROI

+

+ +{results.roi.toFixed(1)}% +

+
+
+

Annual Return

+

+ {formatCurrency(results.annualReturn)} +

+
+
+

IRR (Est.)

+

+ {results.irr.toFixed(1)}% +

+
+
+

Break-even

+

+ {results.breakEvenMonths} mo +

+
+
+ +
+
+ Total Projected Value + + {formatCurrency(investment + results.totalReturn)} + +
+ +
+
+
+
+ Principal +
+
+
+ Yield + Appreciation +
+
+
+
+
+ + + +

+ Powered by {mergedTheme.brandName} · Returns are estimates only +

+
+ ); +}