diff --git a/frontend/app/api/join-requests/route.ts b/frontend/app/api/join-requests/route.ts
new file mode 100644
index 0000000..935b69e
--- /dev/null
+++ b/frontend/app/api/join-requests/route.ts
@@ -0,0 +1,26 @@
+import { supabase } from '@/lib/supabase'
+import { NextRequest, NextResponse } from 'next/server'
+
+export async function POST(req: NextRequest) {
+ try {
+ const body = await req.json()
+ const { pool_id, requester_address } = body
+ if (!pool_id || !requester_address) {
+ return NextResponse.json({ error: 'pool_id and requester_address required' }, { status: 400 })
+ }
+
+ const { data, error } = await supabase.from('join_requests').insert([
+ { pool_id, requester_address: requester_address.toLowerCase(), status: 'pending' }
+ ])
+
+ if (error) {
+ console.error('Join request error:', error)
+ return NextResponse.json({ error: error.message || 'Failed to create join request' }, { status: 500 })
+ }
+
+ return NextResponse.json({ success: true, request: data?.[0] }, { status: 201 })
+ } catch (err) {
+ console.error('Join request exception:', err)
+ return NextResponse.json({ error: err instanceof Error ? err.message : 'Unknown error' }, { status: 500 })
+ }
+}
diff --git a/frontend/app/join/[contractId]/page.tsx b/frontend/app/join/[contractId]/page.tsx
new file mode 100644
index 0000000..3ee437c
--- /dev/null
+++ b/frontend/app/join/[contractId]/page.tsx
@@ -0,0 +1,41 @@
+import { supabase } from '@/lib/supabase'
+import { notFound } from 'next/navigation'
+import JoinActions from '@/components/join/join-actions'
+import { Card } from '@/components/ui/card'
+
+export default async function Page({ params }: { params: { contractId: string } }) {
+ const contractId = params.contractId
+
+ // Fetch pool by contract_address
+ const { data: pool, error } = await supabase
+ .from('pools')
+ .select('*')
+ .eq('contract_address', contractId)
+ .limit(1)
+ .single()
+
+ if (error || !pool) {
+ return (
+
+ Pool not found
+ The invite link appears invalid or the pool no longer exists.
+
+ )
+ }
+
+ return (
+
+
+ {pool.name}
+
+ Type: {pool.type}
+ Members: {pool.members_count}
+
+ Creator: {pool.creator_address}
+ Contract: {pool.contract_address}
+
+
+
+
+ )
+}
diff --git a/frontend/components/group/group-details.tsx b/frontend/components/group/group-details.tsx
index 0d3dc94..33239f3 100644
--- a/frontend/components/group/group-details.tsx
+++ b/frontend/components/group/group-details.tsx
@@ -7,15 +7,17 @@ import { Calendar, TrendingUp, Users, Clock, Loader2, RefreshCw } from "lucide-r
import { Button } from "@/components/ui/button"
import { motion } from "framer-motion"
import { useState, useEffect, useCallback } from "react"
+import { useToast } from "@/hooks/use-toast"
+import { useStellar } from "@/components/web3-provider"
import {
fetchRotationalState, fetchTargetState, fetchFlexibleState,
stroopsToXlm, RotationalPoolState, TargetPoolState, FlexiblePoolState,
} from "@/hooks/useJointSaveContracts"
-import { useStellar } from "@/components/web3-provider"
interface GroupData {
id: string; name: string; type: "rotational" | "target" | "flexible"
status: "active" | "completed" | "paused"; description: string | null
+ creator_address: string
total_saved: number; target_amount: number | null; progress: number
members_count: number; next_payout: string | null; next_recipient: string | null
created_at: string; contribution_amount: number | null; frequency: string | null
@@ -24,6 +26,7 @@ interface GroupData {
export function GroupDetails({ groupId }: { groupId: string }) {
const { address } = useStellar()
+ const { toast } = useToast()
const [group, setGroup] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState("")
@@ -152,9 +155,24 @@ export function GroupDetails({ groupId }: { groupId: string }) {
{onchainState && Live onchain}
-