diff --git a/packages/nextjs/app/actions/auth.ts b/packages/nextjs/app/actions/auth.ts new file mode 100644 index 0000000..6db0bc9 --- /dev/null +++ b/packages/nextjs/app/actions/auth.ts @@ -0,0 +1,76 @@ +"use server" + +import { signIn, signOut } from "~~/auth" +import { hash } from "bcryptjs" +import { AuthError } from "next-auth" +import { getDb } from "~~/lib/mongodb" + +export async function registerUser(formData: { + name: string + email: string + password: string + role: "investor" | "realtor" + phone?: string + nin?: string + businessName?: string +}) { + try { + const db = await getDb() + + const existingUser = await db.collection("users").findOne({ + email: formData.email, + }) + + if (existingUser) { + return { error: "User with this email already exists" } + } + + const hashedPassword = await hash(formData.password, 12) + + const result = await db.collection("users").insertOne({ + name: formData.name, + email: formData.email, + password: hashedPassword, + role: formData.role, + phone: formData.phone || null, + nin: formData.nin || null, + businessName: formData.businessName || null, + createdAt: new Date(), + }) + + if (!result.insertedId) { + return { error: "Failed to create user" } + } + + return { success: true } + } catch (error) { + console.error("Registration error:", error) + return { error: "An error occurred during registration" } + } +} + +export async function loginUser(email: string, password: string) { + try { + await signIn("credentials", { + email, + password, + redirect: false, + }) + + return { success: true } + } catch (error) { + if (error instanceof AuthError) { + switch (error.type) { + case "CredentialsSignin": + return { error: "Invalid email or password" } + default: + return { error: "An error occurred during login" } + } + } + throw error + } +} + +export async function logoutUser() { + await signOut({ redirectTo: "/" }) +} diff --git a/packages/nextjs/app/actions/verify-credentials.ts b/packages/nextjs/app/actions/verify-credentials.ts new file mode 100644 index 0000000..286c2e3 --- /dev/null +++ b/packages/nextjs/app/actions/verify-credentials.ts @@ -0,0 +1,43 @@ +"use server" + +import bcrypt from "bcryptjs" +import { z } from "zod" +import { getDb } from "~~/lib/mongodb" + +export async function verifyCredentials(email: string, password: string) { + try { + const parsedCredentials = z + .object({ email: z.string().email(), password: z.string().min(6) }) + .safeParse({ email, password }) + + if (!parsedCredentials.success) { + return { success: false, error: "Invalid credentials format" } + } + + const db = await getDb() + const user = await db.collection("users").findOne({ email }) + + if (!user || !user.password) { + return { success: false, error: "Invalid credentials" } + } + + const passwordMatch = await bcrypt.compare(password, user.password) + + if (!passwordMatch) { + return { success: false, error: "Invalid credentials" } + } + + return { + success: true, + user: { + id: user._id.toString(), + email: user.email, + name: user.name, + role: user.role || "investor", + }, + } + } catch (error) { + console.error("[v0] Credential verification error:", error) + return { success: false, error: "Authentication failed" } + } +} diff --git a/packages/nextjs/app/api/auth/[...nextauth]/route.ts b/packages/nextjs/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..ba0756a --- /dev/null +++ b/packages/nextjs/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "~~/auth" + +export const { GET, POST } = handlers diff --git a/packages/nextjs/app/dashboard/investor/page.tsx b/packages/nextjs/app/dashboard/investor/page.tsx new file mode 100644 index 0000000..fca53ca --- /dev/null +++ b/packages/nextjs/app/dashboard/investor/page.tsx @@ -0,0 +1,62 @@ +import { BusinessMetrics } from "~~/components/investor-dashboard/business-metrics" +import { DashboardStats } from "~~/components/investor-dashboard/dashboard-stats" +import { MyProperties } from "~~/components/investor-dashboard/my-properties" +import { PortfolioChart } from "~~/components/investor-dashboard/portfolio-chart" +import { QuickActions } from "~~/components/investor-dashboard/quick-actions" +import { RecentTransactions } from "~~/components/investor-dashboard/recent-transactions" +import { redirect } from "next/navigation" +import { auth } from "~~/auth" +import { SignOutButton } from "~~/components/auth/sign-out-button" + +export const metadata = { + title: "Investor Dashboard | reAI", + description: "Manage your tokenized property investments", +} + +export default async function InvestorDashboard() { + const session = await auth() + + if (!session?.user) { + redirect("/login") + } + + if (session.user.role !== "investor") { + redirect("/dashboard/realtor") + } + + const userName = session.user.name || "Investor" + + return ( +
+
+
+
+

Welcome Back, {userName}!

+

Track your tokenized property investments and portfolio performance

+
+ +
+ + {/* Stats Cards */} + + + {/* Main Content Grid */} +
+ {/* Left Column - 2/3 width */} +
+ + +
+ + {/* Right Column - 1/3 width */} +
+ + + +
+
+
+
+ ) +} + diff --git a/packages/nextjs/app/dashboard/realtor/page.tsx b/packages/nextjs/app/dashboard/realtor/page.tsx new file mode 100644 index 0000000..2abcef6 --- /dev/null +++ b/packages/nextjs/app/dashboard/realtor/page.tsx @@ -0,0 +1,21 @@ +"use client" + +import { redirect } from "next/navigation" +import { auth } from "~~/auth" +import { RealtorDashboardClient } from "~~/components/realtor-dashboard/realtor-dashboard-client" + +export default async function RealtorDashboard() { + const session = await auth() + + if (!session?.user) { + redirect("/login") + } + + if (session.user.role !== "realtor") { + redirect("/dashboard/investor") + } + + const userName = session.user.name || "Realtor" + + return +} diff --git a/packages/nextjs/app/layout.tsx b/packages/nextjs/app/layout.tsx index f7b3979..31e9e64 100644 --- a/packages/nextjs/app/layout.tsx +++ b/packages/nextjs/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { ScaffoldStarkAppWithProviders } from "~~/components/ScaffoldStarkAppWithProviders"; import "~~/styles/globals.css"; import { ThemeProvider } from "~~/components/ThemeProvider"; +import { Toaster } from "sonner"; export const metadata: Metadata = { title: "Scaffold-Stark", @@ -15,6 +16,8 @@ const ScaffoldStarkApp = ({ children }: { children: React.ReactNode }) => { + + {children} diff --git a/packages/nextjs/app/login/page.tsx b/packages/nextjs/app/login/page.tsx index 8edfe45..41c2d25 100644 --- a/packages/nextjs/app/login/page.tsx +++ b/packages/nextjs/app/login/page.tsx @@ -10,9 +10,9 @@ export const metadata: Metadata = { export default function LoginPage() { return ( -
+
-
+
{/* Close button */} @@ -20,8 +20,8 @@ export default function LoginPage() { {/* Header */}
-

Welcome Back

-

Sign in to your reAI account

+

Welcome Back

+

Sign in to your reAI account

{/* Form */} diff --git a/packages/nextjs/auth.ts b/packages/nextjs/auth.ts new file mode 100644 index 0000000..4dc9f2d --- /dev/null +++ b/packages/nextjs/auth.ts @@ -0,0 +1,75 @@ +import NextAuth from "next-auth" +import Credentials from "next-auth/providers/credentials" +import Google from "next-auth/providers/google" + +export const { handlers, auth, signIn, signOut } = NextAuth({ + session: { + strategy: "jwt", + }, + pages: { + signIn: "/login", + }, + providers: [ + Google({ + clientId: process.env.AUTH_GOOGLE_ID!, + clientSecret: process.env.AUTH_GOOGLE_SECRET!, + authorization: { + params: { + prompt: "consent", + access_type: "offline", + response_type: "code", + }, + }, + }), + Credentials({ + name: "credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + id: { label: "ID", type: "text" }, + name: { label: "Name", type: "text" }, + role: { label: "Role", type: "text" }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.id) { + return null + } + + const user = credentials as { + id: string + email: string + name: string + role: string + } + + return { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + } + }, + }), + ], + callbacks: { + async jwt({ token, user, account }) { + if (user) { + token.role = (user.role as string) || "investor" + token.id = user.id as string + } + if (account?.provider) { + token.provider = account.provider as string + } + return token + }, + async session({ session, token }) { + if (session.user) { + session.user.role = (token.role as string) || "investor" + session.user.id = token.id as string + session.user.provider = token.provider as string | undefined + } + return session + }, + }, + trustHost: true, +}) diff --git a/packages/nextjs/components/auth/google-signin-button.tsx b/packages/nextjs/components/auth/google-signin-button.tsx new file mode 100644 index 0000000..b09b109 --- /dev/null +++ b/packages/nextjs/components/auth/google-signin-button.tsx @@ -0,0 +1,50 @@ +"use client" + +import { signIn } from "next-auth/react" +import { Button } from "~~/components/ui/button" +import { useState } from "react" + +export function GoogleSignInButton() { + const [isLoading, setIsLoading] = useState(false) + + const handleGoogleSignIn = async () => { + try { + setIsLoading(true) + await signIn("google", { callbackUrl: "/dashboard/investor" }) + } catch (error) { + console.error("Google sign-in error:", error) + } finally { + setIsLoading(false) + } + } + + return ( + + ) +} diff --git a/packages/nextjs/components/auth/investor-signup-form.tsx b/packages/nextjs/components/auth/investor-signup-form.tsx index f485ec1..45d4474 100644 --- a/packages/nextjs/components/auth/investor-signup-form.tsx +++ b/packages/nextjs/components/auth/investor-signup-form.tsx @@ -10,15 +10,16 @@ import { useRouter } from "next/navigation" import { Button } from "~~/components/ui/button" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~~/components/ui/form" import { Input } from "~~/components/ui/input" +import { toast } from "sonner" +import { registerUser } from "~~/app/actions/auth" +import { signOut } from "next-auth/react" +import { GoogleSignInButton } from "./google-signin-button" const investorSignupSchema = z.object({ - fullName: z.string().min(2, { message: "Full name must be at least 2 characters" }), - nin: z - .string() - .length(11, { message: "NIN must be exactly 11 digits" }) - .regex(/^\d+$/, { message: "NIN must contain only numbers" }), - email: z.string().email({ message: "Please enter a valid email address" }), - password: z.string().min(8, { message: "Password must be at least 8 characters" }), + fullName: z.string().min(2, "Full name must be at least 2 characters"), + nin: z.string().length(11, "NIN must be exactly 11 digits").regex(/^\d+$/, "NIN must contain only numbers"), + email: z.string().email("Please enter a valid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), phone: z.string().optional(), }) @@ -42,132 +43,172 @@ export function InvestorSignupForm() { async function onSubmit(data: InvestorSignupFormValues) { setIsLoading(true) - console.log("[v0] Investor signup form submitted:", data) - await new Promise((resolve) => setTimeout(resolve, 1000)) + + await signOut({ redirect: false }) + + const result = await registerUser({ + name: data.fullName, + email: data.email, + password: data.password, + role: "investor", + phone: data.phone, + nin: data.nin, + }) + + if (result.error) { + toast.error(result.error) + setIsLoading(false) + return + } + + toast.success("Registration successful! Please login.", { + description: "Redirecting to login page...", + }) + + setTimeout(() => { + router.push("/login") + }, 1000) setIsLoading(false) - router.push("/dashboard") } return ( -
- - ( - - - Full Name * - - - - - - - )} - /> - - ( - - - NIN (National Identification Number) * - - - - - - - )} - /> - - ( - - - Email Address * - - - - - - - )} - /> - - ( - - - Password * - - -
+
+ + +
+
+ +
+
+ Or continue with email +
+
+ + + + ( + + + Full Name * + + + + + + + )} + /> + + ( + + + NIN (National Identification Number) * + + + + + + + )} + /> + + ( + + + Email Address * + + + + + + + )} + /> + + ( + + + Password * + + +
+ + +
+
+ +
+ )} + /> + + ( + + Phone Number + - -
- - - - )} - /> - - ( - - Phone Number - - - - - - )} - /> - -
- - -
+ + + + )} + /> + +
+ + +
-

- Already have an account?{" "} - - Login - -

- - +

+ Already have an account?{" "} + + Login + +

+ + +
) } \ No newline at end of file diff --git a/packages/nextjs/components/auth/login-form.tsx b/packages/nextjs/components/auth/login-form.tsx index 91a2e11..5ee6838 100644 --- a/packages/nextjs/components/auth/login-form.tsx +++ b/packages/nextjs/components/auth/login-form.tsx @@ -6,10 +6,17 @@ import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { Eye, EyeOff } from "lucide-react" import Link from "next/link" +import { useRouter, useSearchParams } from "next/navigation" import { Button } from "~~/components/ui/button" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~~/components/ui/form" import { Input } from "~~/components/ui/input" import { Checkbox } from "~~/components/ui/checkbox" +import { RadioGroup, RadioGroupItem } from "~~/components/ui/radio-group" +import { toast } from "sonner" +import { signIn } from "next-auth/react" +import { verifyCredentials } from "~~/app/actions/verify-credentials" +import { GoogleSignInButton } from "./google-signin-button" + const loginSchema = z.object({ email: z.string().email("Please enter a valid email address"), @@ -22,6 +29,9 @@ type LoginFormValues = z.infer export function LoginForm() { const [showPassword, setShowPassword] = useState(false) const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + const searchParams = useSearchParams() + const callbackUrl = searchParams.get("callbackUrl") const form = useForm({ resolver: zodResolver(loginSchema), @@ -34,93 +44,133 @@ export function LoginForm() { async function onSubmit(data: LoginFormValues) { setIsLoading(true) - console.log("[v0] Login form submitted:", data) - // TODO: Implement login logic - await new Promise((resolve) => setTimeout(resolve, 1000)) - setIsLoading(false) + + try { + const verificationResult = await verifyCredentials(data.email, data.password) + + if (!verificationResult.success || !verificationResult.user) { + toast.error("Invalid email or password") + setIsLoading(false) + return + } + + const result = await signIn("credentials", { + email: verificationResult.user.email, + id: verificationResult.user.id, + name: verificationResult.user.name, + role: verificationResult.user.role, + redirect: false, + }) + + if (result?.error) { + toast.error("Authentication failed") + setIsLoading(false) + return + } + + toast.success("Login successful! Welcome back", { + description: "Redirecting to your dashboard...", + }) + + setTimeout(() => { + const dashboardUrl = verificationResult.user.role === "realtor" ? "/dashboard/realtor" : "/dashboard/investor" + router.push(callbackUrl || dashboardUrl) + router.refresh() + }, 1000) + } catch (error) { + console.error("[v0] Login error:", error) + toast.error("An error occurred during login") + setIsLoading(false) + } } return ( -
- - ( - - Email Address - - - - - - )} - /> - - ( - - Password - -
- - -
-
- -
- )} - /> - -
+
+ + +
+
+ +
+
+ Or continue with email +
+
+ + + ( - + + Email Address - + - Remember me + )} /> - - Forgot password? - -
- - -

- Don't have an account?{" "} - - Sign up - -

- - + ( + + Password + +
+ + +
+
+ +
+ )} + /> + +
+ ( + + + + + Remember me + + )} + /> + + Forgot password? + +
+ + + + +
) } diff --git a/packages/nextjs/components/auth/realtor-signup-form.tsx b/packages/nextjs/components/auth/realtor-signup-form.tsx index fded500..5362dbe 100644 --- a/packages/nextjs/components/auth/realtor-signup-form.tsx +++ b/packages/nextjs/components/auth/realtor-signup-form.tsx @@ -10,6 +10,11 @@ import { useRouter } from "next/navigation" import { Button } from "~~/components/ui/button" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~~/components/ui/form" import { Input } from "~~/components/ui/input" +import { toast } from "sonner" +import { registerUser } from "~~/app/actions/auth" +import { signOut } from "next-auth/react" +import { GoogleSignInButton } from "./google-signin-button" + const MAX_FILE_SIZE = 5 * 1024 * 1024 const ACCEPTED_FILE_TYPES = ["image/png", "image/jpeg", "image/jpg", "application/pdf"] @@ -52,196 +57,242 @@ export function RealtorSignupForm() { async function onSubmit(data: RealtorSignupFormValues) { setIsLoading(true) - console.log("[v0] Realtor signup form submitted:", data) - await new Promise((resolve) => setTimeout(resolve, 1000)) + + await signOut({ redirect: false }) + + const result = await registerUser({ + name: data.fullName, + email: data.email, + password: data.password, + role: "realtor", + phone: data.phone, + businessName: data.businessName, + }) + + if (result.error) { + toast.error(result.error) + setIsLoading(false) + return + } + + toast.success("Registration successful! Please login.", { + description: "Redirecting to login page...", + }) + + setTimeout(() => { + router.push("/login") + }, 1000) setIsLoading(false) - router.push("/dashboard") } return ( -
- - ( - - - Full Name * - - - - - - - )} - /> - - ( - - - Email Address * - - - - - - - )} - /> - - ( - - - Password * - - -
+
+ + +
+
+ +
+
+ Or continue with email +
+
+ + + + ( + + + Full Name * + + + + + + + )} + /> + + ( + + + Email Address * + + + + + + + )} + /> + + ( + + + Password * + + +
+ + +
+
+ +
+ )} + /> + + ( + + Phone Number + - -
- - - - )} - /> - - ( - - Phone Number - - - - - - )} - /> - - ( - - Business Name or Company - - - - - - )} - /> - - ( - - - Upload Government-issued ID * - - -
- onChange(e.target.files)} - {...fieldProps} - /> - -
-
- -
- )} - /> - - ( - - - Upload Company Document (CAC or License) * - - -
- onChange(e.target.files)} - {...fieldProps} - /> - -
-
- -
- )} - /> - -
- - -
+ + + + )} + /> + + ( + + + Business Name or Company + + + + + + + )} + /> + + ( + + + Upload Government-issued ID * + + +
+ onChange(e.target.files)} + {...fieldProps} + /> + +
+
+ +
+ )} + /> + + ( + + + Upload Company Document (CAC or License) * + + +
+ onChange(e.target.files)} + {...fieldProps} + /> + +
+
+ +
+ )} + /> + +
+ + +
-

- Already have an account?{" "} - - Login - -

- - +

+ Already have an account?{" "} + + Login + +

+ + +
) -} +} \ No newline at end of file diff --git a/packages/nextjs/components/auth/sign-out-button.tsx b/packages/nextjs/components/auth/sign-out-button.tsx new file mode 100644 index 0000000..99b3d7c --- /dev/null +++ b/packages/nextjs/components/auth/sign-out-button.tsx @@ -0,0 +1,29 @@ +"use client" + +import { LogOut } from "lucide-react" +import { signOut } from "next-auth/react" +import { Button } from "~~/components/ui/button" +import { toast } from "sonner" + +export function SignOutButton() { + const handleSignOut = async () => { + try { + await signOut({ callbackUrl: "/login" }) + toast.success("Signed out successfully") + } catch (error) { + console.error("[v0] Sign out error:", error) + toast.error("Failed to sign out") + } + } + + return ( + + ) +} diff --git a/packages/nextjs/components/investor-dashboard/business-metrics.tsx b/packages/nextjs/components/investor-dashboard/business-metrics.tsx new file mode 100644 index 0000000..2bf4d27 --- /dev/null +++ b/packages/nextjs/components/investor-dashboard/business-metrics.tsx @@ -0,0 +1,41 @@ +"use client" + +import { Card } from "~~/components/ui/card" +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts" + +const data = [ + { name: "Lagos Properties", value: 60, color: "#10b981" }, + { name: "Abuja Properties", value: 30, color: "#3b82f6" }, + { name: "Other Cities", value: 10, color: "#a855f7" }, +] + +export function BusinessMetrics() { + return ( + +

Business Metrics

+
+ {data.map((item) => ( +
+
+
+ {item.name} +
+ {item.value}% +
+ ))} +
+
+ + + + {data.map((entry, index) => ( + + ))} + + `${value}%`} /> + + +
+ + ) +} diff --git a/packages/nextjs/components/investor-dashboard/dashboard-stats.tsx b/packages/nextjs/components/investor-dashboard/dashboard-stats.tsx new file mode 100644 index 0000000..e141236 --- /dev/null +++ b/packages/nextjs/components/investor-dashboard/dashboard-stats.tsx @@ -0,0 +1,66 @@ +"use client" + +import { TrendingUp, Wallet, DollarSign, ArrowUpRight } from "lucide-react" +import { Card } from "~~/components/ui/card" + +const stats = [ + { + title: "Total Portfolio Value", + value: "1,250 STRK", + change: "+16.67%", + icon: TrendingUp, + iconBg: "bg-emerald-100", + iconColor: "text-emerald-600", + }, + { + title: "Total Invested", + value: "1,070 STRK", + subtitle: "Across 3 Properties", + icon: Wallet, + iconBg: "bg-blue-100", + iconColor: "text-blue-600", + }, + { + title: "Monthly Income", + value: "45 STRK", + subtitle: "From rental yields", + icon: DollarSign, + iconBg: "bg-purple-100", + iconColor: "text-purple-600", + }, + { + title: "Total Returns", + value: "180 STRK", + change: "+16.67%", + icon: ArrowUpRight, + iconBg: "bg-orange-100", + iconColor: "text-orange-600", + }, +] + +export function DashboardStats() { + return ( +
+ {stats.map((stat) => ( + +
+
+

{stat.title}

+

{stat.value}

+ {stat.change && ( +

+ + {stat.change} +

+ )} + {stat.subtitle &&

{stat.subtitle}

} +
+
+ +
+
+
+ ))} +
+ ) +} diff --git a/packages/nextjs/components/investor-dashboard/my-properties.tsx b/packages/nextjs/components/investor-dashboard/my-properties.tsx new file mode 100644 index 0000000..d683ce7 --- /dev/null +++ b/packages/nextjs/components/investor-dashboard/my-properties.tsx @@ -0,0 +1,123 @@ +"use client" + +import { Card } from "~~/components/ui/card" +import { Badge } from "~~/components/ui/badge" +import { Progress } from "~~/components/ui/progress" +import { MapPin, TrendingUp } from "lucide-react" +import { Button } from "~~/components/ui/button" +import Link from "next/link" + +const properties = [ + { + id: 1, + name: "Lekki Pearl Towers", + location: "Lekki Phase 1, Lagos", + image: "/estate-img-4.jpg", + + invested: "500 STRK", + currentValue: "580 STRK", + monthlyIncome: "25 STRK", + change: "+16.0%", + ownership: 168000, + ownershipPercent: 70, + status: "Active", + }, + { + id: 2, + name: "Banana Island Mansion", + location: "Banana Island, Lagos", + image: "/estate-img-3.jpg", + + invested: "450 STRK", + currentValue: "520 STRK", + monthlyIncome: "18 STRK", + change: "+15.6%", + ownership: 150000, + ownershipPercent: 65, + status: "Active", + }, + { + id: 3, + name: "Ikeja GRA Residence", + + location: "Ikeja GRA, Lagos", + + image: "/estate-img-2.jpg", + + invested: "120 STRK", + currentValue: "150 STRK", + monthlyIncome: "2 STRK", + change: "+25.0%", + ownership: 40000, + ownershipPercent: 40, + status: "Active", + }, +] + +export function MyProperties() { + return ( + +
+

My Properties

+ + + +
+
+ {properties.map((property) => ( + +
+ {property.name} +
+
+
+

{property.name}

+

+ + {property.location} +

+
+ {property.status} +
+ +
+
+

Invested

+

{property.invested}

+
+
+

Current Value

+

{property.currentValue}

+
+
+

Monthly Income

+

{property.monthlyIncome}

+
+
+

Change

+

+ + {property.change} +

+
+
+ +
+
+ Ownership: {property.ownership.toLocaleString()} + {property.ownershipPercent}% +
+ +
+
+
+
+ ))} +
+
+ ) +} diff --git a/packages/nextjs/components/investor-dashboard/portfolio-chart.tsx b/packages/nextjs/components/investor-dashboard/portfolio-chart.tsx new file mode 100644 index 0000000..9d580ad --- /dev/null +++ b/packages/nextjs/components/investor-dashboard/portfolio-chart.tsx @@ -0,0 +1,78 @@ +"use client" + +import { useState } from "react" +import { Card } from "~~/components/ui/card" +import { Button } from "~~/components/ui/button" +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts" + +const data7d = [ + { date: "Mon", value: 1050 }, + { date: "Tue", value: 1100 }, + { date: "Wed", value: 1080 }, + { date: "Thu", value: 1150 }, + { date: "Fri", value: 1200 }, + { date: "Sat", value: 1220 }, + { date: "Sun", value: 1250 }, +] + +const data30d = [ + { date: "Week 1", value: 950 }, + { date: "Week 2", value: 1050 }, + { date: "Week 3", value: 1150 }, + { date: "Week 4", value: 1250 }, +] + +const data90d = [ + { date: "Month 1", value: 800 }, + { date: "Month 2", value: 1000 }, + { date: "Month 3", value: 1250 }, +] + +export function PortfolioChart() { + const [period, setPeriod] = useState<"7d" | "30d" | "90d">("7d") + + const data = period === "7d" ? data7d : period === "30d" ? data30d : data90d + + return ( + +
+

Investment Growth

+
+ {(["7d", "30d", "90d"] as const).map((p) => ( + + ))} +
+
+ + + + + + + + + + + + [`${value} STRK`, "Portfolio Value"]} + /> + + + +
+ ) +} diff --git a/packages/nextjs/components/investor-dashboard/quick-actions.tsx b/packages/nextjs/components/investor-dashboard/quick-actions.tsx new file mode 100644 index 0000000..f1c75ff --- /dev/null +++ b/packages/nextjs/components/investor-dashboard/quick-actions.tsx @@ -0,0 +1,50 @@ + +import { Card } from "~~/components/ui/card" +import { Button } from "~~/components/ui/button" +import { Search, Target, FileText, Users } from "lucide-react" +import Link from "next/link" + +const actions = [ + { + label: "Explore New Properties", + icon: Search, + href: "/properties", + variant: "default" as const, + }, + { + label: "Set Investment Goal", + icon: Target, + href: "#", + variant: "outline" as const, + }, + { + label: "Download Documents", + icon: FileText, + href: "#", + variant: "outline" as const, + }, + +] + +export function QuickActions() { + return ( + +

Quick Actions

+
+ {actions.map((action) => ( + + + + ))} +
+
+ ) +} diff --git a/packages/nextjs/components/investor-dashboard/recent-transactions.tsx b/packages/nextjs/components/investor-dashboard/recent-transactions.tsx new file mode 100644 index 0000000..cf1a944 --- /dev/null +++ b/packages/nextjs/components/investor-dashboard/recent-transactions.tsx @@ -0,0 +1,61 @@ + +import { Card } from "~~/components/ui/card" +import { ArrowUpRight, ArrowDownLeft } from "lucide-react" + +const transactions = [ + { + id: 1, + type: "Investment", + property: "Lagos Duplex", + amount: "100 STRK", + date: "2024-01-15", + icon: ArrowUpRight, + iconBg: "bg-blue-100", + iconColor: "text-blue-600", + }, + { + id: 2, + type: "Dividend", + property: "Abuja Apartments", + amount: "18.5 STRK", + date: "2024-01-10", + icon: ArrowDownLeft, + iconBg: "bg-emerald-100", + iconColor: "text-emerald-600", + }, + { + id: 3, + type: "Investment", + property: "Lekki Terrace", + amount: "50 STRK", + date: "2024-01-08", + icon: ArrowUpRight, + iconBg: "bg-blue-100", + iconColor: "text-blue-600", + }, +] + +export function RecentTransactions() { + return ( + +

Recent Transactions

+
+ {transactions.map((transaction) => ( +
+
+ +
+
+

{transaction.type}

+

{transaction.property}

+
+
+

{transaction.amount}

+

{transaction.date}

+
+
+ ))} +
+
+ ) +} diff --git a/packages/nextjs/components/landing/header.tsx b/packages/nextjs/components/landing/header.tsx index 4b4e9fb..3d61bbb 100644 --- a/packages/nextjs/components/landing/header.tsx +++ b/packages/nextjs/components/landing/header.tsx @@ -38,13 +38,7 @@ export function Header() {
- + -
diff --git a/packages/nextjs/components/landing/properties-list.tsx b/packages/nextjs/components/landing/properties-list.tsx index 966e9d9..1a5893c 100644 --- a/packages/nextjs/components/landing/properties-list.tsx +++ b/packages/nextjs/components/landing/properties-list.tsx @@ -9,9 +9,11 @@ import { Input } from "~~/components/ui/input" import { MapPin, TrendingUp, Search } from "lucide-react" import { properties, Property } from "~~/data/properties-data" import { PropertyAnalysisModal } from "./property-analysis-modal" +import { useRouter } from "next/navigation" export function PropertiesList() { + const router = useRouter() const [searchQuery, setSearchQuery] = useState("") const [selectedProperty, setSelectedProperty] = useState(null) const [isAnalysisOpen, setIsAnalysisOpen] = useState(false) @@ -27,6 +29,10 @@ export function PropertiesList() { setIsAnalysisOpen(true) } + const handleInvest = () => { + router.push("/signup?type=investor") + } + return ( <> {/* Search Bar */} @@ -124,7 +130,10 @@ export function PropertiesList() { > Analyse -
diff --git a/packages/nextjs/components/realtor-dashboard/add-property-modal.tsx b/packages/nextjs/components/realtor-dashboard/add-property-modal.tsx new file mode 100644 index 0000000..7ce7bec --- /dev/null +++ b/packages/nextjs/components/realtor-dashboard/add-property-modal.tsx @@ -0,0 +1,404 @@ +"use client" + +import { useState } from "react" +import { Building2, ArrowLeft, ArrowRight } from "lucide-react" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "~~/components/ui/dialog" +import { Button } from "~~/components/ui/button" +import { Input } from "~~/components/ui/input" +import { Label } from "~~/components/ui/label" +import { Textarea } from "~~/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~~/components/ui/select" +import { toast } from "sonner" + +interface AddPropertyModalProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function AddPropertyModal({ open, onOpenChange }: AddPropertyModalProps) { + const [currentStep, setCurrentStep] = useState(1) + const [formData, setFormData] = useState({ + // Step 1: Basic Info + propertyName: "", + propertyType: "", + description: "", + constructionStatus: "", + address: "", + city: "", + state: "", + // Step 2: Property Details + size: "", + bedrooms: "", + bathrooms: "", + amenities: "", + // Step 3: Financial Info + totalValue: "", + minInvestment: "", + apy: "", + rentalYield: "", + // Step 4: Geolocation + latitude: "", + longitude: "", + // Step 5: Documents + documents: "", + }) + + const steps = [ + { number: 1, title: "Basic Info" }, + { number: 2, title: "Property Details" }, + { number: 3, title: "Financial Info" }, + { number: 4, title: "Geolocation" }, + { number: 5, title: "Documents" }, + ] + + const handleNext = () => { + if (currentStep < 5) { + setCurrentStep(currentStep + 1) + } + } + + const handlePrevious = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1) + } + } + + const handleSubmit = () => { + toast.success("Property added successfully!", { + description: "Your property has been listed for tokenization.", + }) + onOpenChange(false) + setCurrentStep(1) + setFormData({ + propertyName: "", + propertyType: "", + description: "", + constructionStatus: "", + address: "", + city: "", + state: "", + size: "", + bedrooms: "", + bathrooms: "", + amenities: "", + totalValue: "", + minInvestment: "", + apy: "", + rentalYield: "", + latitude: "", + longitude: "", + documents: "", + }) + } + + const updateFormData = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })) + } + + return ( + + + + + + Add New Property + + + + {/* Step Indicator */} +
+ {steps.map((step, index) => ( +
+
+
step.number + ? "bg-emerald-100 text-emerald-700" + : "bg-gray-200 text-gray-600" + }`} + > + {step.number} +
+ {index < steps.length - 1 && ( +
step.number ? "bg-emerald-600" : "bg-gray-200" + }`} + /> + )} +
+

+ {step.title} +

+
+ ))} +
+ + {/* Step 1: Basic Info */} + {currentStep === 1 && ( +
+
+
+ + updateFormData("propertyName", e.target.value)} + /> +
+
+ + +
+
+ +
+ +