Skip to content

Commit 1d91a97

Browse files
authored
Merge pull request #10 from legend4tech/build-pages
Build pages
2 parents 5bc941a + 1628a9a commit 1d91a97

39 files changed

Lines changed: 3492 additions & 491 deletions
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use server"
2+
3+
import { signIn, signOut } from "~~/auth"
4+
import { hash } from "bcryptjs"
5+
import { AuthError } from "next-auth"
6+
import { getDb } from "~~/lib/mongodb"
7+
8+
export async function registerUser(formData: {
9+
name: string
10+
email: string
11+
password: string
12+
role: "investor" | "realtor"
13+
phone?: string
14+
nin?: string
15+
businessName?: string
16+
}) {
17+
try {
18+
const db = await getDb()
19+
20+
const existingUser = await db.collection("users").findOne({
21+
email: formData.email,
22+
})
23+
24+
if (existingUser) {
25+
return { error: "User with this email already exists" }
26+
}
27+
28+
const hashedPassword = await hash(formData.password, 12)
29+
30+
const result = await db.collection("users").insertOne({
31+
name: formData.name,
32+
email: formData.email,
33+
password: hashedPassword,
34+
role: formData.role,
35+
phone: formData.phone || null,
36+
nin: formData.nin || null,
37+
businessName: formData.businessName || null,
38+
createdAt: new Date(),
39+
})
40+
41+
if (!result.insertedId) {
42+
return { error: "Failed to create user" }
43+
}
44+
45+
return { success: true }
46+
} catch (error) {
47+
console.error("Registration error:", error)
48+
return { error: "An error occurred during registration" }
49+
}
50+
}
51+
52+
export async function loginUser(email: string, password: string) {
53+
try {
54+
await signIn("credentials", {
55+
email,
56+
password,
57+
redirect: false,
58+
})
59+
60+
return { success: true }
61+
} catch (error) {
62+
if (error instanceof AuthError) {
63+
switch (error.type) {
64+
case "CredentialsSignin":
65+
return { error: "Invalid email or password" }
66+
default:
67+
return { error: "An error occurred during login" }
68+
}
69+
}
70+
throw error
71+
}
72+
}
73+
74+
export async function logoutUser() {
75+
await signOut({ redirectTo: "/" })
76+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use server"
2+
3+
import bcrypt from "bcryptjs"
4+
import { z } from "zod"
5+
import { getDb } from "~~/lib/mongodb"
6+
7+
export async function verifyCredentials(email: string, password: string) {
8+
try {
9+
const parsedCredentials = z
10+
.object({ email: z.string().email(), password: z.string().min(6) })
11+
.safeParse({ email, password })
12+
13+
if (!parsedCredentials.success) {
14+
return { success: false, error: "Invalid credentials format" }
15+
}
16+
17+
const db = await getDb()
18+
const user = await db.collection("users").findOne({ email })
19+
20+
if (!user || !user.password) {
21+
return { success: false, error: "Invalid credentials" }
22+
}
23+
24+
const passwordMatch = await bcrypt.compare(password, user.password)
25+
26+
if (!passwordMatch) {
27+
return { success: false, error: "Invalid credentials" }
28+
}
29+
30+
return {
31+
success: true,
32+
user: {
33+
id: user._id.toString(),
34+
email: user.email,
35+
name: user.name,
36+
role: user.role || "investor",
37+
},
38+
}
39+
} catch (error) {
40+
console.error("[v0] Credential verification error:", error)
41+
return { success: false, error: "Authentication failed" }
42+
}
43+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { handlers } from "~~/auth"
2+
3+
export const { GET, POST } = handlers
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { BusinessMetrics } from "~~/components/investor-dashboard/business-metrics"
2+
import { DashboardStats } from "~~/components/investor-dashboard/dashboard-stats"
3+
import { MyProperties } from "~~/components/investor-dashboard/my-properties"
4+
import { PortfolioChart } from "~~/components/investor-dashboard/portfolio-chart"
5+
import { QuickActions } from "~~/components/investor-dashboard/quick-actions"
6+
import { RecentTransactions } from "~~/components/investor-dashboard/recent-transactions"
7+
import { redirect } from "next/navigation"
8+
import { auth } from "~~/auth"
9+
import { SignOutButton } from "~~/components/auth/sign-out-button"
10+
11+
export const metadata = {
12+
title: "Investor Dashboard | reAI",
13+
description: "Manage your tokenized property investments",
14+
}
15+
16+
export default async function InvestorDashboard() {
17+
const session = await auth()
18+
19+
if (!session?.user) {
20+
redirect("/login")
21+
}
22+
23+
if (session.user.role !== "investor") {
24+
redirect("/dashboard/realtor")
25+
}
26+
27+
const userName = session.user.name || "Investor"
28+
29+
return (
30+
<div className="min-h-screen bg-gray-50">
31+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
32+
<div className="flex items-center justify-between mb-8">
33+
<div>
34+
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-2">Welcome Back, {userName}!</h1>
35+
<p className="text-gray-600 text-lg">Track your tokenized property investments and portfolio performance</p>
36+
</div>
37+
<SignOutButton />
38+
</div>
39+
40+
{/* Stats Cards */}
41+
<DashboardStats />
42+
43+
{/* Main Content Grid */}
44+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-8">
45+
{/* Left Column - 2/3 width */}
46+
<div className="lg:col-span-2 space-y-6">
47+
<PortfolioChart />
48+
<MyProperties />
49+
</div>
50+
51+
{/* Right Column - 1/3 width */}
52+
<div className="space-y-6">
53+
<QuickActions />
54+
<RecentTransactions />
55+
<BusinessMetrics />
56+
</div>
57+
</div>
58+
</div>
59+
</div>
60+
)
61+
}
62+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client"
2+
3+
import { redirect } from "next/navigation"
4+
import { auth } from "~~/auth"
5+
import { RealtorDashboardClient } from "~~/components/realtor-dashboard/realtor-dashboard-client"
6+
7+
export default async function RealtorDashboard() {
8+
const session = await auth()
9+
10+
if (!session?.user) {
11+
redirect("/login")
12+
}
13+
14+
if (session.user.role !== "realtor") {
15+
redirect("/dashboard/investor")
16+
}
17+
18+
const userName = session.user.name || "Realtor"
19+
20+
return <RealtorDashboardClient userName={userName} />
21+
}

packages/nextjs/app/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
22
import { ScaffoldStarkAppWithProviders } from "~~/components/ScaffoldStarkAppWithProviders";
33
import "~~/styles/globals.css";
44
import { ThemeProvider } from "~~/components/ThemeProvider";
5+
import { Toaster } from "sonner";
56

67
export const metadata: Metadata = {
78
title: "Scaffold-Stark",
@@ -15,6 +16,8 @@ const ScaffoldStarkApp = ({ children }: { children: React.ReactNode }) => {
1516
<body suppressHydrationWarning>
1617
<ThemeProvider enableSystem>
1718
<ScaffoldStarkAppWithProviders>
19+
<Toaster position="top-right" richColors/>
20+
1821
{children}
1922
</ScaffoldStarkAppWithProviders>
2023
</ThemeProvider>

packages/nextjs/app/login/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ export const metadata: Metadata = {
1010

1111
export default function LoginPage() {
1212
return (
13-
<div className="min-h-screen bg-white flex items-center justify-center p-4">
13+
<div className="min-h-screen bg-white flex items-center justify-center p-4 sm:p-6 lg:p-8">
1414
<div className="w-full max-w-md">
15-
<div className="relative bg-white border border-gray-200 rounded-2xl p-8 shadow-sm">
15+
<div className="relative bg-white border border-gray-200 rounded-2xl p-6 sm:p-8 shadow-sm">
1616
{/* Close button */}
1717
<Link href="/" className="absolute top-4 right-4 p-2 text-gray-400 hover:text-gray-600 transition-colors">
1818
<X className="w-5 h-5" />
1919
</Link>
2020

2121
{/* Header */}
2222
<div className="text-center mb-8">
23-
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome Back</h1>
24-
<p className="text-gray-600">Sign in to your reAI account</p>
23+
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">Welcome Back</h1>
24+
<p className="text-sm sm:text-base text-gray-600">Sign in to your reAI account</p>
2525
</div>
2626

2727
{/* Form */}

packages/nextjs/auth.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import NextAuth from "next-auth"
2+
import Credentials from "next-auth/providers/credentials"
3+
import Google from "next-auth/providers/google"
4+
5+
export const { handlers, auth, signIn, signOut } = NextAuth({
6+
session: {
7+
strategy: "jwt",
8+
},
9+
pages: {
10+
signIn: "/login",
11+
},
12+
providers: [
13+
Google({
14+
clientId: process.env.AUTH_GOOGLE_ID!,
15+
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
16+
authorization: {
17+
params: {
18+
prompt: "consent",
19+
access_type: "offline",
20+
response_type: "code",
21+
},
22+
},
23+
}),
24+
Credentials({
25+
name: "credentials",
26+
credentials: {
27+
email: { label: "Email", type: "email" },
28+
password: { label: "Password", type: "password" },
29+
id: { label: "ID", type: "text" },
30+
name: { label: "Name", type: "text" },
31+
role: { label: "Role", type: "text" },
32+
},
33+
async authorize(credentials) {
34+
if (!credentials?.email || !credentials?.id) {
35+
return null
36+
}
37+
38+
const user = credentials as {
39+
id: string
40+
email: string
41+
name: string
42+
role: string
43+
}
44+
45+
return {
46+
id: user.id,
47+
email: user.email,
48+
name: user.name,
49+
role: user.role,
50+
}
51+
},
52+
}),
53+
],
54+
callbacks: {
55+
async jwt({ token, user, account }) {
56+
if (user) {
57+
token.role = (user.role as string) || "investor"
58+
token.id = user.id as string
59+
}
60+
if (account?.provider) {
61+
token.provider = account.provider as string
62+
}
63+
return token
64+
},
65+
async session({ session, token }) {
66+
if (session.user) {
67+
session.user.role = (token.role as string) || "investor"
68+
session.user.id = token.id as string
69+
session.user.provider = token.provider as string | undefined
70+
}
71+
return session
72+
},
73+
},
74+
trustHost: true,
75+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client"
2+
3+
import { signIn } from "next-auth/react"
4+
import { Button } from "~~/components/ui/button"
5+
import { useState } from "react"
6+
7+
export function GoogleSignInButton() {
8+
const [isLoading, setIsLoading] = useState(false)
9+
10+
const handleGoogleSignIn = async () => {
11+
try {
12+
setIsLoading(true)
13+
await signIn("google", { callbackUrl: "/dashboard/investor" })
14+
} catch (error) {
15+
console.error("Google sign-in error:", error)
16+
} finally {
17+
setIsLoading(false)
18+
}
19+
}
20+
21+
return (
22+
<Button
23+
type="button"
24+
variant="outline"
25+
className="w-full h-12 bg-transparent"
26+
onClick={handleGoogleSignIn}
27+
disabled={isLoading}
28+
>
29+
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
30+
<path
31+
fill="currentColor"
32+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
33+
/>
34+
<path
35+
fill="currentColor"
36+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
37+
/>
38+
<path
39+
fill="currentColor"
40+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
41+
/>
42+
<path
43+
fill="currentColor"
44+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
45+
/>
46+
</svg>
47+
{isLoading ? "Signing in..." : "Continue with Google"}
48+
</Button>
49+
)
50+
}

0 commit comments

Comments
 (0)