Compare commits

..

21 Commits

Author SHA1 Message Date
d3b792dc1d Add more pages using v0 2024-11-30 12:48:48 -06:00
6ff62ca0a6 Made a lot of the front end 2024-11-29 19:37:14 -06:00
04a6322b55 Good start 2024-11-29 14:02:50 -06:00
02628e02ab fix commands 2024-10-05 18:05:28 -05:00
eabf5fd836 dont even know what teh changes are but meh 2024-10-05 16:29:56 -05:00
70b99228e6 begin working on form 2024-09-01 21:41:44 -05:00
3272c83f09 cant remember 2024-09-01 20:31:09 -05:00
cb00826b16 did more stuff 2024-09-01 19:48:10 -05:00
91b947c608 work on bill tracker page. add calendar. 2024-08-31 19:45:24 -05:00
52b8a4c1cb Move components to layout. fix css for children objects 2024-08-30 16:51:14 -05:00
251d74cc05 update db schema 2024-08-09 16:56:37 -05:00
c21ae7452b Add to db schema 2024-08-09 12:39:53 -05:00
f070bb0175 Update README 2024-08-09 08:51:37 -05:00
c68a0d105a Nav bar front end is done 2024-08-08 22:21:54 -05:00
9e5080591a Nav bar front end is done 2024-08-08 22:20:19 -05:00
4adf59d440 Begin working on home page 2024-08-08 19:52:48 -05:00
2989cdd421 Adding image link now works for pfp 2024-08-08 16:35:59 -05:00
40130b65e5 When a new user logs in with Apple, they are prompted to add their name & pfp since Apple does not provide that. 2024-08-08 15:46:27 -05:00
17486bd00c Fix db connection 2024-08-08 07:59:11 -05:00
46ddc6d7f4 make sure I can push db as it is 2024-08-08 04:51:54 -05:00
a575ccd3a1 make sure I can push db as it is 2024-08-08 04:51:14 -05:00
100 changed files with 8323 additions and 348 deletions

View File

@ -1,17 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
# Examples:
# SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar"
# Drizzle
## WAN
DATABASE_URL="postgresql://postgres:password@localhost:5432/example_db"
## LAN
DATABASE_URL="postgresql://postgres:password@localhost:5432/example_db"
# Auth.js
# openssl rand -base64 33
AUTH_SECRET=""
# Auth.js Providers:

0
.eslintrc.cjs Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

24
README.md Normal file → Executable file
View File

@ -1,3 +1,27 @@
# Rent Portal
## Portal for tenants to pay rent
#### To Do:
- [ ] Fix First Sign In Form to reload auth session after updating user's name & pfp
- [ ] Maybe add additional option to upload PFP
- [ ] Break Home Page into components
- [ ] Move necessary components from page to layout
- [ ] Add necessary pages & set up routing for each link
- [ ] Add all necessary tables to database schema.
- [ ] Write functions to access data from database
- [ ] Add Admin Portal to Avatar Popover, conditionally render based on user. (Probably email)
#### Questions:
- How will the data get into the database?
- How will the portal know which property the user is renting?
- Should the user select the property from a dropdown list? Probably not...
- Admin Portal? Admin Privileges?
- Does the breadcrumb make sense? Will there be any nested pages?
- If it doesn't, then what should go up there?
- What needs to be done before we can start building the dashboard?
- Properties need to be tied to a user/users.
- Payments & Payment Methods need to be tied to users.
- Workorders need to be tied to users.

0
components.json Normal file → Executable file
View File

0
drizzle.config.ts Normal file → Executable file
View File

0
next.config.js Normal file → Executable file
View File

29
package.json Normal file → Executable file
View File

@ -13,17 +13,32 @@
"lint": "next lint",
"start": "next start",
"generate-apple-secret": "tsx src/scripts/generate_apple_secret.ts",
"go": "git pull && next dev",
"goprod": "git pull && next build && next start"
"go": "next dev",
"goprod": "next build && next start"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.4.2",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.1",
"@t3-oss/env-nextjs": "^0.10.1",
"@types/jsonwebtoken": "^9.0.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^3.6.0",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.30.10",
"geist": "^1.3.1",
@ -34,9 +49,15 @@
"next-themes": "^0.3.0",
"postgres": "^3.4.4",
"react": "^18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.2",
"recharts": "^2.13.3",
"server-only": "^0.0.1",
"sonner": "^1.5.0",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.1",
"zod": "^3.23.8"
},
"devDependencies": {
@ -53,9 +74,9 @@
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.8",
"tailwindcss": "^3.4.9",
"ts-node": "^10.9.2",
"tsx": "^4.16.5",
"tsx": "^4.17.0",
"typescript": "^5.5.4"
},
"ct3aMetadata": {

1689
pnpm-lock.yaml generated Normal file → Executable file

File diff suppressed because it is too large Load Diff

0
postcss.config.cjs Normal file → Executable file
View File

0
prettier.config.js Normal file → Executable file
View File

0
public/favicon.ico Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

0
public/logos/Apple_logo_black.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 660 B

0
public/logos/Apple_logo_grey.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,148 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Calendar } from "~/components/ui/calendar"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
import { Input } from "~/components/ui/input"
import { Label } from "~/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
import { Plus, DollarSign } from 'lucide-react'
// Mock data for bills
const initialBills = [
{ id: 1, name: 'Electricity', amount: 80, dueDate: new Date(2023, 6, 15), category: 'Utilities' },
{ id: 2, name: 'Internet', amount: 60, dueDate: new Date(2023, 6, 20), category: 'Utilities' },
{ id: 3, name: 'Water', amount: 40, dueDate: new Date(2023, 6, 25), category: 'Utilities' },
]
export default function BillTrackerPage() {
const [bills, setBills] = useState(initialBills)
const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date())
const [isDialogOpen, setIsDialogOpen] = useState(false)
const handleAddBill = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const newBill = {
id: bills.length + 1,
name: formData.get('billName') as string,
amount: Number(formData.get('amount')),
dueDate: selectedDate as Date,
category: formData.get('category') as string,
}
setBills([...bills, newBill])
setIsDialogOpen(false)
}
const getDayContent = (day: Date | undefined) => {
if (!day) return null;
const dayBills = bills.filter(bill =>
bill.dueDate.getDate() === day.getDate() &&
bill.dueDate.getMonth() === day.getMonth() &&
bill.dueDate.getFullYear() === day.getFullYear()
)
return dayBills.length > 0 ? (
<div className="w-full h-full flex items-center justify-center">
<div className="h-2 w-2 bg-primary rounded-full" />
</div>
) : null
}
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Bill Tracker</CardTitle>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Add Bill
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleAddBill}>
<DialogHeader>
<DialogTitle>Add New Bill</DialogTitle>
<DialogDescription>
Enter the details of the bill you want to track.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="billName" className="text-right">
Bill Name
</Label>
<Input id="billName" name="billName" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="amount" className="text-right">
Amount
</Label>
<Input id="amount" name="amount" type="number" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="category" className="text-right">
Category
</Label>
<Select name="category" required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Utilities">Utilities</SelectItem>
<SelectItem value="Rent">Rent</SelectItem>
<SelectItem value="Insurance">Insurance</SelectItem>
<SelectItem value="Subscriptions">Subscriptions</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit">Add Bill</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex space-x-4">
<div className="flex-1">
<Calendar
mode="single"
selected={selectedDate}
onSelect={setSelectedDate}
className="rounded-md border"
components={{
DayContent: ({ date }) => getDayContent(date),
}}
/>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold mb-4">Upcoming Bills</h3>
<ul className="space-y-2">
{bills.map((bill) => (
<li key={bill.id} className="flex justify-between items-center p-2 bg-muted rounded-md">
<div>
<p className="font-medium">{bill.name}</p>
<p className="text-sm text-muted-foreground">{bill.dueDate.toLocaleDateString()}</p>
</div>
<div className="flex items-center">
<DollarSign className="h-4 w-4 mr-1 text-primary" />
<span className="font-semibold">{bill.amount.toFixed(2)}</span>
</div>
</li>
))}
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,19 @@
"use server"
import { auth } from "~/auth"
import BreadCrumbBillTracker from "~/components/portal/home/breadcrumb/BreadCrumbBillTracker"
import BillTrackerCalendar from "~/components/portal/billtracker/BillTrackerCalendar"
export default async function HomePage() {
const session = await auth()
if (!session?.user) return <></>
return (
<div className="w-2/3 flex flex-col p-6">
<div className="flex flex-row">
<div className="">
<BreadCrumbBillTracker />
</div>
</div>
< BillTrackerCalendar />
</div>
);
}

View File

@ -0,0 +1,63 @@
import { Button } from "~/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { FileText, Download, Upload } from 'lucide-react'
// Mock data for documents
const documents = [
{ id: 1, name: 'Lease Agreement', type: 'PDF', size: '2.5 MB', date: '2023-01-15' },
{ id: 2, name: 'Move-in Checklist', type: 'DOCX', size: '1.2 MB', date: '2023-01-15' },
{ id: 3, name: 'Rent Payment Receipt - June 2023', type: 'PDF', size: '0.5 MB', date: '2023-06-01' },
{ id: 4, name: 'Property Rules and Regulations', type: 'PDF', size: '1.8 MB', date: '2023-01-15' },
]
export default function DocumentsPage() {
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Documents</CardTitle>
<Button>
<Upload className="mr-2 h-4 w-4" /> Upload Document
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Size</TableHead>
<TableHead>Date</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{documents.map((doc) => (
<TableRow key={doc.id}>
<TableCell className="font-medium">
<div className="flex items-center">
<FileText className="mr-2 h-4 w-4" />
{doc.name}
</div>
</TableCell>
<TableCell>{doc.type}</TableCell>
<TableCell>{doc.size}</TableCell>
<TableCell>{doc.date}</TableCell>
<TableCell>
<Button variant="ghost" size="sm">
<Download className="mr-2 h-4 w-4" /> Download
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,59 @@
import Link from 'next/link'
import { Button } from "~/components/ui/button"
import { Card, CardContent } from "~/components/ui/card"
import { CreditCard, FileText, MessageSquare, PenToolIcon as Tool, BarChart } from 'lucide-react'
export default function AccountLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8 text-primary">My Account</h1>
<div className="flex flex-col md:flex-row gap-8">
<Card className="w-full md:w-64 h-fit">
<CardContent className="p-4">
<nav className="space-y-2">
<Link href="/account" passHref>
<Button variant="ghost" className="w-full justify-start">
<BarChart className="mr-2 h-4 w-4" /> Dashboard
</Button>
</Link>
<Link href="/account/payments" passHref>
<Button variant="ghost" className="w-full justify-start">
<CreditCard className="mr-2 h-4 w-4" /> Payments
</Button>
</Link>
<Link href="/account/workorders" passHref>
<Button variant="ghost" className="w-full justify-start">
<Tool className="mr-2 h-4 w-4" /> Work Orders
</Button>
</Link>
<Link href="/account/messages" passHref>
<Button variant="ghost" className="w-full justify-start">
<MessageSquare className="mr-2 h-4 w-4" /> Messages
</Button>
</Link>
<Link href="/account/documents" passHref>
<Button variant="ghost" className="w-full justify-start">
<FileText className="mr-2 h-4 w-4" /> Documents
</Button>
</Link>
<Link href="/account/billtracker" passHref>
<Button variant="ghost" className="w-full justify-start">
<BarChart className="mr-2 h-4 w-4" /> Bill Tracker
</Button>
</Link>
</nav>
</CardContent>
</Card>
<main className="flex-1">
{children}
</main>
</div>
</div>
)
}

View File

@ -0,0 +1,70 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Input } from "~/components/ui/input"
import { Avatar, AvatarFallback } from "~/components/ui/avatar"
// Mock data for messages
const initialMessages = [
{ id: 1, sender: 'Property Manager', content: 'Your maintenance request has been received and scheduled for next Tuesday.', timestamp: '2023-06-20T10:30:00Z' },
{ id: 2, sender: 'Tenant', content: 'Thank you for the quick response. I&apos;ll make sure to be available on Tuesday.', timestamp: '2023-06-20T11:15:00Z' },
{ id: 3, sender: 'Property Manager', content: 'Great! The maintenance team will arrive between 9 AM and 12 PM. Please ensure they have access to the affected area.', timestamp: '2023-06-20T14:00:00Z' },
]
export default function MessagesPage() {
const [messages, setMessages] = useState(initialMessages)
const [newMessage, setNewMessage] = useState('')
const handleSendMessage = (e: React.FormEvent) => {
e.preventDefault()
if (newMessage.trim()) {
const message = {
id: messages.length + 1,
sender: 'Tenant',
content: newMessage,
timestamp: new Date().toISOString(),
}
setMessages([...messages, message])
setNewMessage('')
}
}
return (
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{messages.map((message) => (
<div key={message.id} className={`flex ${message.sender === 'Tenant' ? 'justify-end' : 'justify-start'}`}>
<div className={`flex ${message.sender === 'Tenant' ? 'flex-row-reverse' : 'flex-row'} items-start space-x-2`}>
<Avatar className="w-10 h-10">
<AvatarFallback>{message.sender[0]}</AvatarFallback>
</Avatar>
<div className={`rounded-lg p-3 ${message.sender === 'Tenant' ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
<p className="text-sm font-medium">{message.sender}</p>
<p className="mt-1">{message.content}</p>
<p className="mt-1 text-xs opacity-70">{new Date(message.timestamp).toLocaleString()}</p>
</div>
</div>
</div>
))}
</div>
<form onSubmit={handleSendMessage} className="mt-4 flex space-x-2">
<Input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type your message..."
className="flex-grow"
/>
<Button type="submit">Send</Button>
</form>
</CardContent>
</Card>
</div>
)
}

112
src/app/account/page.tsx Normal file
View File

@ -0,0 +1,112 @@
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Button } from "~/components/ui/button"
import { Progress } from "~/components/ui/progress"
import { CreditCard, AlertTriangle, CheckCircle } from 'lucide-react'
export default function AccountHomePage() {
// This data would typically come from your backend
const accountData = {
balance: 1200,
nextPaymentDue: "2023-07-01",
recentPayment: {
amount: 1200,
date: "2023-06-01"
},
workOrders: [
{ id: 1, title: "Leaky faucet", status: "In Progress" },
{ id: 2, title: "Broken AC", status: "Scheduled" }
],
documents: [
{ id: 1, title: "Lease Agreement", date: "2023-01-15" },
{ id: 2, title: "Move-in Checklist", date: "2023-01-15" }
]
}
return (
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle>Account Overview</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Current Balance</CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${accountData.balance}</div>
<p className="text-xs text-muted-foreground">
Next payment due: {accountData.nextPaymentDue}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Recent Payment</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${accountData.recentPayment.amount}</div>
<p className="text-xs text-muted-foreground">
Paid on: {accountData.recentPayment.date}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Lease Progress</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<Progress value={33} className="w-full" />
<p className="text-xs text-muted-foreground mt-2">
4 months remaining
</p>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Recent Work Orders</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-4">
{accountData.workOrders.map(order => (
<li key={order.id} className="flex justify-between items-center">
<span>{order.title}</span>
<span className="text-sm text-muted-foreground">{order.status}</span>
</li>
))}
</ul>
<Button className="w-full mt-4">View All Work Orders</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Recent Documents</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-4">
{accountData.documents.map(doc => (
<li key={doc.id} className="flex justify-between items-center">
<span>{doc.title}</span>
<span className="text-sm text-muted-foreground">{doc.date}</span>
</li>
))}
</ul>
<Button className="w-full mt-4">View All Documents</Button>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@ -0,0 +1,80 @@
import { Button } from "~/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { CreditCard, DollarSign } from 'lucide-react'
// This would typically come from your database
const paymentHistory = [
{ id: 1, date: '2023-06-01', amount: 1200, status: 'Paid' },
{ id: 2, date: '2023-05-01', amount: 1200, status: 'Paid' },
{ id: 3, date: '2023-04-01', amount: 1200, status: 'Paid' },
]
export default function PaymentsPage() {
return (
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Current Balance</CardTitle>
</CardHeader>
<CardContent>
<div className="text-4xl font-bold">$1,200.00</div>
<p className="text-muted-foreground mt-2">Due on July 1, 2023</p>
</CardContent>
<CardFooter>
<Button className="w-full">
<DollarSign className="mr-2 h-4 w-4" /> Make a Payment
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Payment Methods</CardTitle>
<CardDescription>Manage your payment methods</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center">
<CreditCard className="mr-2 h-4 w-4" />
<div>
<p className="font-medium">Visa ending in 1234</p>
<p className="text-sm text-muted-foreground">Expires 12/2025</p>
</div>
</div>
<Button variant="outline">Edit</Button>
</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">Add Payment Method</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Payment History</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paymentHistory.map((payment) => (
<TableRow key={payment.id}>
<TableCell>{payment.date}</TableCell>
<TableCell>${payment.amount.toFixed(2)}</TableCell>
<TableCell>{payment.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,135 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
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 { PenToolIcon as Tool, Plus } from 'lucide-react'
// This would typically come from your database
const workOrders = [
{ id: 1, date: '2023-06-15', issue: 'Leaky faucet', status: 'In Progress' },
{ id: 2, date: '2023-06-10', issue: 'Broken AC', status: 'Scheduled' },
{ id: 3, date: '2023-05-28', issue: 'Clogged drain', status: 'Completed' },
]
export default function WorkOrdersPage() {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
// Here you would typically send the form data to your server
setIsDialogOpen(false)
}
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Work Orders</CardTitle>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> New Work Order
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Create New Work Order</DialogTitle>
<DialogDescription>
Describe the issue you're experiencing. We'll get on it as soon as possible.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="issue" className="text-right">
Issue
</Label>
<Input id="issue" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="location" className="text-right">
Location
</Label>
<Select required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="kitchen">Kitchen</SelectItem>
<SelectItem value="bathroom">Bathroom</SelectItem>
<SelectItem value="bedroom">Bedroom</SelectItem>
<SelectItem value="livingroom">Living Room</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea id="description" className="col-span-3" required />
</div>
</div>
<DialogFooter>
<Button type="submit">Submit Work Order</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Issue</TableHead>
<TableHead>Status</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workOrders.map((order) => (
<TableRow key={order.id}>
<TableCell>{order.date}</TableCell>
<TableCell>{order.issue}</TableCell>
<TableCell>{order.status}</TableCell>
<TableCell>
<Button variant="outline" size="sm">View Details</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Maintenance Tips</CardTitle>
<CardDescription>Keep your living space in top condition with these tips</CardDescription>
</CardHeader>
<CardContent>
<ul className="list-disc pl-4 space-y-2">
<li>Regularly clean or replace HVAC filters</li>
<li>Check and clean gutters seasonally</li>
<li>Test smoke and carbon monoxide detectors monthly</li>
<li>Inspect plumbing fixtures for leaks</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="link" className="w-full">View More Tips</Button>
</CardFooter>
</Card>
</div>
)
}

View File

@ -0,0 +1,223 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
import { Label } from "~/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import { Badge } from "~/components/ui/badge"
import { Search, Plus, FileText, Download, Trash2, Share2, Upload } from 'lucide-react'
// Mock data for documents
const documents = [
{ id: 1, name: "Lease Agreement - John Doe", type: "PDF", size: "2.5 MB", date: "2023-07-01", status: "Active" },
{ id: 2, name: "Rent Receipt - Jane Smith", type: "PDF", size: "1.2 MB", date: "2023-07-02", status: "Archived" },
{ id: 3, name: "Maintenance Report - Riverside Condos", type: "DOCX", size: "3.7 MB", date: "2023-07-03", status: "Active" },
{ id: 4, name: "Property Rules - Sunset Apartments", type: "PDF", size: "1.8 MB", date: "2023-07-04", status: "Active" },
// Add more mock data as needed
]
export default function DocumentsPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedStatus, setSelectedStatus] = useState("All")
const filteredDocuments = documents.filter(doc =>
doc.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
(selectedStatus === "All" || doc.status === selectedStatus)
)
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Documents</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Upload Document
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Upload New Document</DialogTitle>
<DialogDescription>
Upload a new document and assign it to tenants or properties.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="document" className="text-right">
Document
</Label>
<Input id="document" type="file" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="assign" className="text-right">
Assign To
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select recipients" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tenants</SelectItem>
<SelectItem value="sunset">Sunset Apartments</SelectItem>
<SelectItem value="oakwood">Oakwood Residences</SelectItem>
<SelectItem value="riverside">Riverside Condos</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit">Upload Document</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search documents..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Archived">Archived</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Size</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredDocuments.map((doc) => (
<TableRow key={doc.id}>
<TableCell className="font-medium">{doc.name}</TableCell>
<TableCell>{doc.type}</TableCell>
<TableCell>{doc.size}</TableCell>
<TableCell>{doc.date}</TableCell>
<TableCell>
<Badge variant={doc.status === 'Active' ? 'default' : 'secondary'}>
{doc.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button variant="ghost" size="icon">
<Download className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Share2 className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Document Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="usage">Usage</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Documents
</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{documents.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Documents
</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{documents.filter(doc => doc.status === 'Active').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Storage Used
</CardTitle>
<Upload className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">9.2 MB</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Most Common Type
</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">PDF</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="usage">
<p>Document usage statistics and charts will be displayed here.</p>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

62
src/app/admin/layout.tsx Normal file
View File

@ -0,0 +1,62 @@
import Link from 'next/link'
import { Button } from "~/components/ui/button"
import { Card, CardContent } from "~/components/ui/card"
import { CreditCard, FileText, MessageSquare, PenToolIcon as Tool, BarChart, Users, Home } from 'lucide-react'
export default function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8 text-primary">Admin Dashboard</h1>
<div className="flex flex-col md:flex-row gap-8">
<Card className="w-full md:w-64 h-fit">
<CardContent className="p-4">
<nav className="space-y-2">
<Link href="/admin" passHref>
<Button variant="ghost" className="w-full justify-start">
<BarChart className="mr-2 h-4 w-4" /> Dashboard
</Button>
</Link>
<Link href="/admin/tenants" passHref>
<Button variant="ghost" className="w-full justify-start">
<Users className="mr-2 h-4 w-4" /> Tenants
</Button>
</Link>
<Link href="/admin/properties" passHref>
<Button variant="ghost" className="w-full justify-start">
<Home className="mr-2 h-4 w-4" /> Properties
</Button>
</Link>
<Link href="/admin/payments" passHref>
<Button variant="ghost" className="w-full justify-start">
<CreditCard className="mr-2 h-4 w-4" /> Payments
</Button>
</Link>
<Link href="/admin/workorders" passHref>
<Button variant="ghost" className="w-full justify-start">
<Tool className="mr-2 h-4 w-4" /> Work Orders
</Button>
</Link>
<Link href="/admin/messages" passHref>
<Button variant="ghost" className="w-full justify-start">
<MessageSquare className="mr-2 h-4 w-4" /> Messages
</Button>
</Link>
<Link href="/admin/documents" passHref>
<Button variant="ghost" className="w-full justify-start">
<FileText className="mr-2 h-4 w-4" /> Documents
</Button>
</Link>
</nav>
</CardContent>
</Card>
<main className="flex-1">
{children}
</main>
</div>
</div>
)
}

View File

@ -0,0 +1,249 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
import { Label } from "~/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
import { Textarea } from "~/components/ui/textarea"
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"
import { Badge } from "~/components/ui/badge"
import { Search, Plus, MessageSquare, Users, ArrowUpRight } from 'lucide-react'
// Mock data for messages
const messages = [
{ id: 1, tenant: "John Doe", property: "Sunset Apartments, Apt 4B", subject: "Maintenance Request", date: "2023-07-05", status: "Unread" },
{ id: 2, tenant: "Jane Smith", property: "Oakwood Residences, Apt 2A", subject: "Rent Inquiry", date: "2023-07-04", status: "Read" },
{ id: 3, tenant: "Bob Johnson", property: "Riverside Condos, Apt 3C", subject: "Lease Renewal", date: "2023-07-03", status: "Replied" },
{ id: 4, tenant: "Alice Brown", property: "Sunset Apartments, Apt 2C", subject: "Noise Complaint", date: "2023-07-02", status: "Unread" },
// Add more mock data as needed
]
export default function MessagesPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedStatus, setSelectedStatus] = useState("All")
const [selectedMessage, setSelectedMessage] = useState<typeof messages[0] | null>(null)
const filteredMessages = messages.filter(message =>
(message.tenant.toLowerCase().includes(searchTerm.toLowerCase()) ||
message.property.toLowerCase().includes(searchTerm.toLowerCase()) ||
message.subject.toLowerCase().includes(searchTerm.toLowerCase())) &&
(selectedStatus === "All" || message.status === selectedStatus)
)
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Messages</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> New Message
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Send New Message</DialogTitle>
<DialogDescription>
Compose a new message to send to a tenant or multiple tenants.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="recipients" className="text-right">
To
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select recipients" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tenants</SelectItem>
<SelectItem value="sunset">Sunset Apartments</SelectItem>
<SelectItem value="oakwood">Oakwood Residences</SelectItem>
<SelectItem value="riverside">Riverside Condos</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="subject" className="text-right">
Subject
</Label>
<Input id="subject" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="message" className="text-right">
Message
</Label>
<Textarea id="message" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Send Message</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Unread">Unread</SelectItem>
<SelectItem value="Read">Read</SelectItem>
<SelectItem value="Replied">Replied</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Tenant</TableHead>
<TableHead>Property</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredMessages.map((message) => (
<TableRow key={message.id}>
<TableCell>{message.tenant}</TableCell>
<TableCell>{message.property}</TableCell>
<TableCell>{message.subject}</TableCell>
<TableCell>{message.date}</TableCell>
<TableCell>
<Badge variant={
message.status === 'Unread' ? 'default' :
message.status === 'Read' ? 'secondary' :
'outline'
}>
{message.status}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon" onClick={() => setSelectedMessage(message)}>
<MessageSquare className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{selectedMessage && (
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Message Details</CardTitle>
<Button variant="outline" onClick={() => setSelectedMessage(null)}>Close</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/placeholder-avatar.jpg" alt={selectedMessage.tenant} />
<AvatarFallback>{selectedMessage.tenant[0]}</AvatarFallback>
</Avatar>
<div>
<h3 className="font-semibold">{selectedMessage.tenant}</h3>
<p className="text-sm text-muted-foreground">{selectedMessage.property}</p>
</div>
</div>
<div>
<h4 className="font-semibold">Subject: {selectedMessage.subject}</h4>
<p className="text-sm text-muted-foreground">Received on {selectedMessage.date}</p>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<Textarea placeholder="Type your reply here..." className="mt-4" />
<div className="flex justify-end space-x-2">
<Button variant="outline">Save Draft</Button>
<Button>Send Reply</Button>
</div>
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>Message Statistics</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Messages
</CardTitle>
<MessageSquare className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{messages.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Unread Messages
</CardTitle>
<ArrowUpRight className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{messages.filter(m => m.status === 'Unread').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Response Rate
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{Math.round((messages.filter(m => m.status === 'Replied').length / messages.length) * 100)}%
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Avg. Response Time
</CardTitle>
<MessageSquare className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">4.2 hours</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</div>
)
}

174
src/app/admin/page.tsx Normal file
View File

@ -0,0 +1,174 @@
'use client'
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Button } from "~/components/ui/button"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/components/ui/chart"
import { Bar, BarChart, Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts"
import { CreditCard, Users, Home, Wrench, AlertTriangle } from 'lucide-react'
// Mock data for charts
const rentData = [
{ month: "Jan", collected: 95 },
{ month: "Feb", collected: 98 },
{ month: "Mar", collected: 92 },
{ month: "Apr", collected: 97 },
{ month: "May", collected: 99 },
{ month: "Jun", collected: 94 },
]
const occupancyData = [
{ month: "Jan", rate: 92 },
{ month: "Feb", rate: 94 },
{ month: "Mar", rate: 96 },
{ month: "Apr", rate: 95 },
{ month: "May", rate: 97 },
{ month: "Jun", rate: 98 },
]
export default function AdminDashboard() {
return (
<div className="space-y-8">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$24,560</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Occupancy Rate</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">98%</div>
<p className="text-xs text-muted-foreground">+2% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Properties</CardTitle>
<Home className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">45</div>
<p className="text-xs text-muted-foreground">+3 new this month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Work Orders</CardTitle>
<Wrench className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground">-5 from last week</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Rent Collection Rate</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
collected: {
label: "Collected",
color: "hsl(var(--chart-1))",
},
}} className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={rentData}>
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="collected" fill="var(--color-collected)" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Occupancy Trend</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
rate: {
label: "Occupancy Rate",
color: "hsl(var(--chart-2))",
},
}} className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={occupancyData}>
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Line type="monotone" dataKey="rate" stroke="var(--color-rate)" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Recent Activities</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center">
<AlertTriangle className="h-4 w-4 text-yellow-500 mr-2" />
<span className="text-sm">New work order: Leaky faucet at 123 Main St, Apt 4B</span>
</div>
<div className="flex items-center">
<Users className="h-4 w-4 text-green-500 mr-2" />
<span className="text-sm">New tenant: John Doe moved into 456 Elm St, Apt 2A</span>
</div>
<div className="flex items-center">
<CreditCard className="h-4 w-4 text-blue-500 mr-2" />
<span className="text-sm">Rent payment received: $1,200 from Jane Smith, 789 Oak Rd, Apt 3C</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Upcoming Lease Renewals</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div>
<p className="font-medium">Sarah Johnson</p>
<p className="text-sm text-muted-foreground">123 Main St, Apt 2B</p>
</div>
<div className="text-right">
<p className="font-medium">Expires in 30 days</p>
<Button size="sm">Send Renewal</Button>
</div>
</div>
<div className="flex justify-between items-center">
<div>
<p className="font-medium">Michael Brown</p>
<p className="text-sm text-muted-foreground">456 Elm St, Apt 1A</p>
</div>
<div className="text-right">
<p className="font-medium">Expires in 45 days</p>
<Button size="sm">Send Renewal</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,269 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
import { Label } from "~/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import { Badge } from "~/components/ui/badge"
import { Search, Plus, FileText, DollarSign, CreditCard, Calendar } from 'lucide-react'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/components/ui/chart"
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"
// Mock data for payments
const payments = [
{ id: 1, tenant: "John Doe", property: "Sunset Apartments, Apt 4B", amount: 1200, date: "2023-07-01", status: "Paid" },
{ id: 2, tenant: "Jane Smith", property: "Oakwood Residences, Apt 2A", amount: 1500, date: "2023-07-02", status: "Paid" },
{ id: 3, tenant: "Bob Johnson", property: "Riverside Condos, Apt 3C", amount: 1800, date: "2023-07-05", status: "Pending" },
{ id: 4, tenant: "Alice Brown", property: "Sunset Apartments, Apt 2C", amount: 1100, date: "2023-07-03", status: "Late" },
// Add more mock data as needed
]
// Mock data for payment chart
const paymentChartData = [
{ month: "Jan", amount: 45000 },
{ month: "Feb", amount: 42000 },
{ month: "Mar", amount: 47000 },
{ month: "Apr", amount: 44000 },
{ month: "May", amount: 46000 },
{ month: "Jun", amount: 48000 },
]
export default function PaymentsPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedStatus, setSelectedStatus] = useState("All")
const filteredPayments = payments.filter(payment =>
(payment.tenant.toLowerCase().includes(searchTerm.toLowerCase()) ||
payment.property.toLowerCase().includes(searchTerm.toLowerCase())) &&
(selectedStatus === "All" || payment.status === selectedStatus)
)
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Payments</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Record Payment
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Record New Payment</DialogTitle>
<DialogDescription>
Enter the details of the new payment. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tenant" className="text-right">
Tenant
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select tenant" />
</SelectTrigger>
<SelectContent>
<SelectItem value="john-doe">John Doe</SelectItem>
<SelectItem value="jane-smith">Jane Smith</SelectItem>
<SelectItem value="bob-johnson">Bob Johnson</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="amount" className="text-right">
Amount
</Label>
<Input id="amount" type="number" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="date" className="text-right">
Date
</Label>
<Input id="date" type="date" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="method" className="text-right">
Payment Method
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="credit-card">Credit Card</SelectItem>
<SelectItem value="bank-transfer">Bank Transfer</SelectItem>
<SelectItem value="cash">Cash</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit">Save Payment</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search payments..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Paid">Paid</SelectItem>
<SelectItem value="Pending">Pending</SelectItem>
<SelectItem value="Late">Late</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Tenant</TableHead>
<TableHead>Property</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredPayments.map((payment) => (
<TableRow key={payment.id}>
<TableCell>{payment.tenant}</TableCell>
<TableCell>{payment.property}</TableCell>
<TableCell>${payment.amount}</TableCell>
<TableCell>{payment.date}</TableCell>
<TableCell>
<Badge variant={
payment.status === 'Paid' ? 'default' :
payment.status === 'Pending' ? 'secondary' :
'destructive'
}>
{payment.status}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon">
<FileText className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Payment Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="trends">Trends</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Collected
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,600</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Pending Payments
</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$3,800</div>
<p className="text-xs text-muted-foreground">5 payments pending</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Late Payments
</CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$1,200</div>
<p className="text-xs text-muted-foreground">2 payments overdue</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Collection Rate
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">98%</div>
<p className="text-xs text-muted-foreground">+2% from last month</p>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="trends">
<Card>
<CardHeader>
<CardTitle>Monthly Payment Trends</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
amount: {
label: "Amount",
color: "hsl(var(--chart-1))",
},
}} className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={paymentChartData}>
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="amount" fill="var(--color-amount)" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,218 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
import { Label } from "~/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import { Badge } from "~/components/ui/badge"
import { Search, Plus, Edit, Trash2, Home, DollarSign, Users } from 'lucide-react'
// Mock data for properties
const properties = [
{ id: 1, name: "Sunset Apartments", address: "123 Main St", units: 20, occupancy: 18, rentRange: "$1000 - $1500" },
{ id: 2, name: "Oakwood Residences", address: "456 Elm St", units: 15, occupancy: 14, rentRange: "$1200 - $1800" },
{ id: 3, name: "Riverside Condos", address: "789 Oak Rd", units: 30, occupancy: 28, rentRange: "$1500 - $2200" },
// Add more mock data as needed
]
export default function PropertiesPage() {
const [searchTerm, setSearchTerm] = useState("")
const filteredProperties = properties.filter(property =>
property.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
property.address.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Properties</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Add Property
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Property</DialogTitle>
<DialogDescription>
Enter the details of the new property. Click save when you&apos;re done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="address" className="text-right">
Address
</Label>
<Input id="address" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="units" className="text-right">
Total Units
</Label>
<Input id="units" type="number" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rentMin" className="text-right">
Min Rent
</Label>
<Input id="rentMin" type="number" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rentMax" className="text-right">
Max Rent
</Label>
<Input id="rentMax" type="number" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save Property</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search properties..."placeholder="Search properties..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Address</TableHead>
<TableHead>Units</TableHead>
<TableHead>Occupancy</TableHead>
<TableHead>Rent Range</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredProperties.map((property) => (
<TableRow key={property.id}>
<TableCell>{property.name}</TableCell>
<TableCell>{property.address}</TableCell>
<TableCell>{property.units}</TableCell>
<TableCell>
<Badge variant={property.occupancy === property.units ? 'default' : 'secondary'}>
{property.occupancy}/{property.units}
</Badge>
</TableCell>
<TableCell>{property.rentRange}</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Property Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="financial">Financial</TabsTrigger>
<TabsTrigger value="maintenance">Maintenance</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Properties
</CardTitle>
<Home className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{properties.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Units
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{properties.reduce((sum, p) => sum + p.units, 0)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Occupancy Rate
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{Math.round(
(properties.reduce((sum, p) => sum + p.occupancy, 0) /
properties.reduce((sum, p) => sum + p.units, 0)) * 100
)}%
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Avg. Rent
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$1,450</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="financial">
<p>Detailed financial information and statistics will be displayed here.</p>
</TabsContent>
<TabsContent value="maintenance">
<p>Property maintenance history and upcoming tasks will be displayed here.</p>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,236 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
import { Label } from "~/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import { Badge } from "~/components/ui/badge"
import { Search, Plus, Edit, Trash2, Mail, Phone } from 'lucide-react'
// Mock data for tenants
const tenants = [
{ id: 1, name: "John Doe", email: "john@example.com", phone: "123-456-7890", property: "123 Main St, Apt 4B", leaseEnd: "2023-12-31", status: "Active" },
{ id: 2, name: "Jane Smith", email: "jane@example.com", phone: "098-765-4321", property: "456 Elm St, Apt 2A", leaseEnd: "2024-03-15", status: "Active" },
{ id: 3, name: "Bob Johnson", email: "bob@example.com", phone: "555-123-4567", property: "789 Oak Rd, Apt 3C", leaseEnd: "2023-09-30", status: "Notice Given" },
// Add more mock data as needed
]
export default function TenantsPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedStatus, setSelectedStatus] = useState("All")
const filteredTenants = tenants.filter(tenant =>
(tenant.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tenant.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
tenant.property.toLowerCase().includes(searchTerm.toLowerCase())) &&
(selectedStatus === "All" || tenant.status === selectedStatus)
)
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Tenants</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Add Tenant
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Tenant</DialogTitle>
<DialogDescription>
Enter the details of the new tenant. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="email" className="text-right">
Email
</Label>
<Input id="email" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="phone" className="text-right">
Phone
</Label>
<Input id="phone" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="property" className="text-right">
Property
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select property" />
</SelectTrigger>
<SelectContent>
<SelectItem value="123 Main St, Apt 4B">123 Main St, Apt 4B</SelectItem>
<SelectItem value="456 Elm St, Apt 2A">456 Elm St, Apt 2A</SelectItem>
<SelectItem value="789 Oak Rd, Apt 3C">789 Oak Rd, Apt 3C</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="leaseEnd" className="text-right">
Lease End Date
</Label>
<Input id="leaseEnd" type="date" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save Tenant</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search tenants..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Notice Given">Notice Given</SelectItem>
<SelectItem value="Inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Property</TableHead>
<TableHead>Lease End</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTenants.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell>{tenant.name}</TableCell>
<TableCell>{tenant.property}</TableCell>
<TableCell>{tenant.leaseEnd}</TableCell>
<TableCell>
<Badge variant={tenant.status === 'Active' ? 'default' : 'secondary'}>
{tenant.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button variant="ghost" size="icon">
<Mail className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Phone className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Tenant Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="leases">Leases</TabsTrigger>
<TabsTrigger value="payments">Payments</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Tenants
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{tenants.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Leases
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{tenants.filter(t => t.status === 'Active').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Expiring Soon
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">2</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Vacant Units
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">3</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="leases">
<p>Detailed lease information and statistics will be displayed here.</p>
</TabsContent>
<TabsContent value="payments">
<p>Tenant payment history and statistics will be displayed here.</p>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,298 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
import { Label } from "~/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import { Badge } from "~/components/ui/badge"
import { Textarea } from "~/components/ui/textarea"
import { Search, Plus, Wrench, Clock, CheckCircle, AlertTriangle } from 'lucide-react'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/components/ui/chart"
import { Pie, PieChart, ResponsiveContainer, Cell } from "recharts"
// Mock data for work orders
const workOrders = [
{ id: 1, property: "Sunset Apartments, Apt 4B", issue: "Leaky faucet", tenant: "John Doe", date: "2023-07-01", status: "Open" },
{ id: 2, property: "Oakwood Residences, Apt 2A", issue: "Broken AC", tenant: "Jane Smith", date: "2023-07-02", status: "In Progress" },
{ id: 3, property: "Riverside Condos, Apt 3C", issue: "Clogged drain", tenant: "Bob Johnson", date: "2023-07-03", status: "Completed" },
{ id: 4, property: "Sunset Apartments, Apt 2C", issue: "Electrical issue", tenant: "Alice Brown", date: "2023-07-04", status: "Open" },
// Add more mock data as needed
]
// Mock data for work order status chart
const statusChartData = [
{ name: "Open", value: 5 },
{ name: "In Progress", value: 3 },
{ name: "Completed", value: 8 },
]
const COLORS = ['#0088FE', '#00C49F', '#FFBB28']
export default function WorkOrdersPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedStatus, setSelectedStatus] = useState("All")
const filteredWorkOrders = workOrders.filter(order =>
(order.property.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.issue.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.tenant.toLowerCase().includes(searchTerm.toLowerCase())) &&
(selectedStatus === "All" || order.status === selectedStatus)
)
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Work Orders</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Create Work Order
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Work Order</DialogTitle>
<DialogDescription>
Enter the details of the new work order. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="property" className="text-right">
Property
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select property" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sunset-apartments">Sunset Apartments</SelectItem>
<SelectItem value="oakwood-residences">Oakwood Residences</SelectItem>
<SelectItem value="riverside-condos">Riverside Condos</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tenant" className="text-right">
Tenant
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select tenant" />
</SelectTrigger>
<SelectContent>
<SelectItem value="john-doe">John Doe</SelectItem>
<SelectItem value="jane-smith">Jane Smith</SelectItem>
<SelectItem value="bob-johnson">Bob Johnson</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="issue" className="text-right">
Issue
</Label>
<Input id="issue" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea id="description" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="priority" className="text-right">
Priority
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit">Create Work Order</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search work orders..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Open">Open</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Property</TableHead>
<TableHead>Issue</TableHead>
<TableHead>Tenant</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredWorkOrders.map((order) => (
<TableRow key={order.id}>
<TableCell>{order.property}</TableCell>
<TableCell>{order.issue}</TableCell>
<TableCell>{order.tenant}</TableCell>
<TableCell>{order.date}</TableCell>
<TableCell>
<Badge variant={
order.status === 'Open' ? 'default' :
order.status === 'In Progress' ? 'secondary' :
'success'
}>
{order.status}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon">
<Wrench className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Work Order Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="trends">Trends</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Work Orders
</CardTitle>
<Wrench className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{workOrders.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Open Work Orders
</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{workOrders.filter(order => order.status === 'Open').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
In Progress
</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{workOrders.filter(order => order.status === 'In Progress').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Completed
</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{workOrders.filter(order => order.status === 'Completed').length}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="trends">
<Card>
<CardHeader>
<CardTitle>Work Order Status Distribution</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
status: {
label: "Status",
color: "hsl(var(--chart-1))",
},
}} className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusChartData}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{statusChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<ChartTooltip content={<ChartTooltipContent />} />
</PieChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

0
src/app/api/auth/[...nextauth]/route.ts Normal file → Executable file
View File

View File

@ -0,0 +1,27 @@
"use server"
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { set_users_name_by_email } from "~/server/functions"
import { auth } from "~/auth"
type updateNameData = {
users_name: string;
users_email: string;
};
export const POST = async (req: NextRequest) => {
const session = await auth();
if (!session) return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
const { users_name, users_email } = await req.json() as updateNameData;
console.log('API received users_name:', users_name, 'users_id:', users_email); // Log received data
try {
await set_users_name_by_email(users_name, users_email);
return NextResponse.json({ message: "Username updated successfully", users_name }, { status: 200 });
} catch (error) {
console.error('Error in API route:', error);
return NextResponse.json({ error: "Error updating username" }, { status: 500 });
}
};

View File

@ -0,0 +1,27 @@
"use server"
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { set_users_pfp_by_email } from "~/server/functions"
import { auth } from "~/auth"
type updateNameData = {
users_pfp: string;
users_email: string;
};
export const POST = async (req: NextRequest) => {
const session = await auth();
if (!session) return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
const { users_pfp, users_email } = await req.json() as updateNameData;
console.log('API received users_pfp:', users_pfp, 'users_id:', users_email); // Log received data
try {
await set_users_pfp_by_email(users_pfp, users_email);
return NextResponse.json({ message: "Username updated successfully", users_pfp }, { status: 200 });
} catch (error) {
console.error('Error in API route:', error);
return NextResponse.json({ error: "Error updating username" }, { status: 500 });
}
};

83
src/app/contact/page.tsx Normal file
View File

@ -0,0 +1,83 @@
import type { Metadata } from 'next'
import ContactForm from '~/components/contact/contact-form'
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { MapPin, Phone, Mail, Clock } from 'lucide-react'
export const metadata: Metadata = {
title: 'Contact Us | PropertyPro',
description: 'Get in touch with PropertyPro for any inquiries about our properties or services.',
}
export default function ContactPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8 text-primary">Contact Us</h1>
<div className="grid md:grid-cols-2 gap-8">
<div>
<ContactForm />
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Our Office</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<p className="flex items-center">
<MapPin className="mr-2 h-4 w-4 text-primary" />
123 Property Street, Cityville, State 12345
</p>
<p className="flex items-center">
<Phone className="mr-2 h-4 w-4 text-primary" />
(123) 456-7890
</p>
<p className="flex items-center">
<Mail className="mr-2 h-4 w-4 text-primary" />
info@propertypro.com
</p>
<p className="flex items-center">
<Clock className="mr-2 h-4 w-4 text-primary" />
Mon-Fri: 9am-5pm, Sat: 10am-3pm, Sun: Closed
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>FAQ</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h3 className="font-semibold mb-1">How do I schedule a property viewing?</h3>
<p className="text-sm text-muted-foreground">You can schedule a viewing by contacting our office or using the form on this page.</p>
</div>
<div>
<h3 className="font-semibold mb-1">What documents do I need to apply for a rental?</h3>
<p className="text-sm text-muted-foreground">Typically, you'll need proof of income, identification, and references. Specific requirements may vary.</p>
</div>
<div>
<h3 className="font-semibold mb-1">How do I report maintenance issues?</h3>
<p className="text-sm text-muted-foreground">Tenants can report maintenance issues through their online account or by contacting our office directly.</p>
</div>
</CardContent>
</Card>
<div className="aspect-w-16 aspect-h-9">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3022.1422937950147!2d-73.98731968459391!3d40.74844797932681!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x89c259a9b3117469%3A0xd134e199a405a163!2sEmpire%20State%20Building!5e0!3m2!1sen!2sus!4v1616562308246!5m2!1sen!2sus"
width="600"
height="450"
style={{border:0}}
allowFullScreen={true}
loading="lazy"
className="w-full h-full rounded-lg"
title="PropertyPro Office Location"
></iframe>
</div>
</div>
</div>
</div>
)
}

134
src/app/layout.tsx Normal file → Executable file
View File

@ -1,9 +1,14 @@
import "~/styles/globals.css";
import { Inter as FontSans } from "next/font/google";
import { cn } from "~/lib/utils"
import { auth } from "~/auth"
import { SessionProvider } from "next-auth/react";
import Theme_Provider from "~/components/theme/theme_provider"
import { type Metadata } from "next";
import Theme_Toggle from '~/components/theme/theme_toggle'
import { Button } from "~/components/ui/button"
import Link from 'next/link'
import Avatar_Popover from "~/components/auth/AvatarPopover";
export const metadata: Metadata = {
title: "Tenant Portal",
@ -16,10 +21,81 @@ const fontSans = FontSans({
variable: "--font-sans",
});
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
const session = await auth();
if (!session?.user) {
return (
<html lang="en">
<body
className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable)}
>
<Theme_Provider
attribute="class"
defaultTheme="system"
enableSystem={true}
disableTransitionOnChange={true}
>
<SessionProvider>
<div className="flex flex-col min-h-screen">
<header className="bg-background shadow-sm">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="md:text-2xl font-bold text-primary">Magnolia Coast Properties</Link>
<nav className="hidden md:flex space-x-2 lg:space-x-4 text-sm lg:text-lg">
<Link href="/properties" className="text-gray-600 hover:text-primary">Properties</Link>
<Link href="/services" className="text-gray-600 hover:text-primary">Services</Link>
<Link href="/about" className="text-gray-600 hover:text-primary">About</Link>
<Link href="/contact" className="text-gray-600 hover:text-primary">Contact</Link>
</nav>
<div className="flex space-x-2 md:space-x-4">
<Theme_Toggle/>
<Link href="/login">
<Button variant="outline">Login</Button>
</Link>
</div>
</div>
</header>
{children}
<footer className="bg-background text-primary py-8">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 className="text-xl font-bold mb-4">Magnolia Coast Property Management</h3>
<p>Your trusted partner in property management</p>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Quick Links</h4>
<ul className="space-y-2">
<li><Link href="/properties" className="hover:text-primary-foreground">Properties</Link></li>
<li><Link href="/services" className="hover:text-primary-foreground">Services</Link></li>
<li><Link href="/about" className="hover:text-primary-foreground">About Us</Link></li>
<li><Link href="/contact" className="hover:text-primary-foreground">Contact</Link></li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Contact Us</h4>
<p>123 Property Street</p>
<p>City, State 12345</p>
<p>Phone: (123) 456-7890</p>
<p>Email: info@propertypro.com</p>
</div>
</div>
<div className="mt-8 text-center">
<p>&copy; 2024 Magnolia Coast Property Management LLC. All rights reserved.</p>
</div>
</div>
</footer>
</div>
</SessionProvider>
</Theme_Provider>
</body>
</html>
);
} else {
return (
<html lang="en">
<body
className={cn(
@ -33,10 +109,60 @@ export default function RootLayout({
disableTransitionOnChange={true}
>
<SessionProvider>
{children}
<div className="flex flex-col min-h-screen">
<header className="bg-background shadow-sm">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="md:text-2xl font-bold text-primary">Magnolia Coast Properties</Link>
<nav className="hidden md:flex space-x-2 lg:space-x-4 text-sm lg:text-lg">
<Link href="/account" className="text-gray-600 hover:text-primary">My Account</Link>
<Link href="/properties" className="text-gray-600 hover:text-primary">Properties</Link>
<Link href="/services" className="text-gray-600 hover:text-primary">Services</Link>
<Link href="/about" className="text-gray-600 hover:text-primary">About</Link>
<Link href="/contact" className="text-gray-600 hover:text-primary">Contact</Link>
</nav>
<div className="flex space-x-2 md:space-x-4">
<Theme_Toggle/>
<Avatar_Popover/>
</div>
</div>
</header>
{children}
<footer className="bg-background text-primary py-8">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<h3 className="text-xl font-bold mb-4">Magnolia Coast Property Management</h3>
<p>Your trusted partner in property management</p>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Quick Links</h4>
<ul className="space-y-2">
<li><Link href="/account" className="hover:text-primary-foreground">My Account</Link></li>
<li><Link href="/properties" className="hover:text-primary-foreground">Properties</Link></li>
<li><Link href="/services" className="hover:text-primary-foreground">Services</Link></li>
<li><Link href="/about" className="hover:text-primary-foreground">About Us</Link></li>
<li><Link href="/contact" className="hover:text-primary-foreground">Contact</Link></li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Contact Us</h4>
<p>123 Property Street</p>
<p>City, State 12345</p>
<p>Phone: (123) 456-7890</p>
<p>Email: info@propertypro.com</p>
</div>
</div>
<div className="mt-8 text-center">
<p>&copy; 2024 Magnolia Coast Property Management LLC. All rights reserved.</p>
</div>
</div>
</footer>
</div>
</SessionProvider>
</Theme_Provider>
</body>
</html>
);
);
}
}

127
src/app/login/page.tsx Normal file
View File

@ -0,0 +1,127 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import { Label } from "~/components/ui/label"
import { Eye, EyeOff } from 'lucide-react'
import Sign_In from "~/components/auth/client/SignInAppleButton"
export default function AuthPage() {
const [showPassword, setShowPassword] = useState(false)
const togglePasswordVisibility = () => setShowPassword(!showPassword)
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl font-bold text-center text-primary">
Welcome to Magnolia Coast Property Management
</CardTitle>
<CardDescription className="text-center text-muted-foreground">
Login or create an account to get started
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="login" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
</TabsList>
<TabsContent value="login">
<form>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" placeholder="Enter your email" type="email" required />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Input
id="password"
placeholder="Enter your password"
type={showPassword ? "text" : "password"}
required
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={togglePasswordVisibility}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
</div>
</div>
<div className="flex flex-col justify-center">
<Button className="w-full mt-6 mb-4 text-md" type="submit">Login</Button>
<Sign_In/>
</div>
</form>
</TabsContent>
<TabsContent value="register">
<form>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="register-name">Full Name</Label>
<Input id="register-name" placeholder="Enter your full name" required />
</div>
<div className="space-y-2">
<Label htmlFor="register-email">Email</Label>
<Input id="register-email" placeholder="Enter your email" type="email" required />
</div>
<div className="space-y-2">
<Label htmlFor="register-password">Password</Label>
<div className="relative">
<Input
id="register-password"
placeholder="Create a password"
type={showPassword ? "text" : "password"}
required
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={togglePasswordVisibility}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
</div>
</div>
<div className="flex flex-col justify-center">
<Button className="w-full mt-6 mb-4 text-md" type="submit">Register</Button>
<Sign_In/>
</div>
</form>
</TabsContent>
</Tabs>
</CardContent>
<CardFooter className="flex justify-center">
<p className="text-sm text-muted-foreground">
By continuing, you agree to our{' '}
<a href="#" className="underline text-primary">Terms of Service</a> and{' '}
<a href="#" className="underline text-primary">Privacy Policy</a>
</p>
</CardFooter>
</Card>
</div>
);
}

116
src/app/page.tsx Normal file → Executable file
View File

@ -1,36 +1,90 @@
"use server"
import Theme_Toggle from "~/components/theme/theme_toggle"
import { auth } from "~/auth"
import Sign_In_Apple_Button from "~/components/auth/server/SignInAppleButton"
import Sign_Out_Button from "~/components/auth/server/SignOutButton"
import Title from "~/components/home/Title"
import Link from 'next/link'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Search, Home, Key, Wrench, DollarSign } from 'lucide-react'
export default async function HomePage() {
const session = await auth();
if (!session) {
return (
<main className="min-h-screen">
<div className="w-full justify-end items-end p-3 flex flex-col">
<Theme_Toggle />
</div>
<div className="w-full flex flex-col justify-center items-center">
<Title />
<Sign_In_Apple_Button />
</div>
</main>
);
} else {
const email = session?.user?.email;
return (
<main className="min-h-screen">
<div className="w-full justify-end items-end p-3 flex flex-col">
<Theme_Toggle />
<div className="w-full flex flex-col justify-center items-center">
<h1>Welcome, {email}</h1>
<Sign_Out_Button />
return (
<main className="flex-grow">
<section className="bg-gradient-to-r from-background to-primary-foreground text-primary py-20">
<div className="container mx-auto px-4 text-center">
<h1 className="text-4xl md:text-5xl font-bold mb-4">Find Your Perfect Property</h1>
<p className="text-xl mb-8">Discover a wide range of properties for sale and rent</p>
<div className="max-w-2xl mx-auto flex">
<Input type="text" placeholder="Search properties..." className="flex-grow" />
<Button className="ml-2">
<Search className="mr-2 h-4 w-4" /> Search
</Button>
</div>
</div>
</main>
);
}
</section>
<section className="py-16">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 text-center">Featured Properties</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[1, 2, 3].map((i) => (
<Card key={i}>
<CardHeader>
<CardTitle>Beautiful Property {i}</CardTitle>
</CardHeader>
<CardContent>
<img src={`/placeholder.svg?height=200&width=300`} alt={`Property ${i}`} className="w-full h-48 object-cover mb-4 rounded" />
<p className="text-gray-600">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</CardContent>
<CardFooter>
<Link href={`/properties/${i}`} passHref>
<Button className="w-full">View Details</Button>
</Link>
</CardFooter>
</Card>
))}
</div>
</div>
</section>
<section className="bg-background py-16">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 text-center">Our Services</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{[
{ icon: Home, title: "Property Management" },
{ icon: Key, title: "Rentals" },
{ icon: Wrench, title: "Maintenance" },
{ icon: DollarSign, title: "Rent Collection" },
].map((service, i) => (
<Card key={i} className="text-center">
<CardHeader>
<service.icon className="mx-auto h-12 w-12 text-primary" />
<CardTitle>{service.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
</main>
)
}
//"use server"
//import { auth } from "~/auth"
//import Breadcrumb_Home from "~/components/home/breadcrumb/BreadcrumbHome"
//export default async function HomePage() {
//const session = await auth()
//if (!session?.user) return <></>
//return (
//<div className="w-2/3 flex flex-col p-6">
//<div className="flex flex-row">
//<div className="">
//<Breadcrumb_Home />
//</div>
//</div>
//</div>
//);
//}

View File

@ -0,0 +1,86 @@
import Link from 'next/link'
import { Button } from "~/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Home, Bed, Bath, Square, MapPin, Calendar, DollarSign } from 'lucide-react'
// This would typically come from your database
const property = {
id: 1,
title: "Luxurious Family Home",
type: "House",
bedrooms: 4,
bathrooms: 3,
area: 2500,
price: 750000,
address: "123 Main St, Anytown, USA",
description: "This beautiful family home features spacious living areas, a modern kitchen, and a large backyard perfect for entertaining. Located in a quiet neighborhood with easy access to schools and shopping.",
amenities: ["Central Air", "Garage", "Fireplace", "Hardwood Floors", "Swimming Pool"],
yearBuilt: 2015,
}
export default function PropertyPage({ params }: { params: { id: string } }) {
return (
<div className="container mx-auto px-4 py-8">
<Link href="/properties" className="text-primary hover:underline mb-4 inline-block">
&larr; Back to Properties
</Link>
<div className="grid md:grid-cols-2 gap-8">
<div>
<img src="/placeholder.svg?height=400&width=600" alt={property.title} className="w-full h-[400px] object-cover rounded-lg" />
<div className="mt-4 grid grid-cols-2 gap-4">
<img src="/placeholder.svg?height=150&width=250" alt="Additional view 1" className="w-full h-[150px] object-cover rounded" />
<img src="/placeholder.svg?height=150&width=250" alt="Additional view 2" className="w-full h-[150px] object-cover rounded" />
</div>
</div>
<div>
<h1 className="text-3xl font-bold mb-4 text-primary">{property.title}</h1>
<p className="text-xl font-semibold mb-4 text-primary">${property.price.toLocaleString()}</p>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="flex items-center text-muted-foreground">
<Home className="mr-2" /> {property.type}
</div>
<div className="flex items-center text-muted-foreground">
<Bed className="mr-2" /> {property.bedrooms} Bedrooms
</div>
<div className="flex items-center text-muted-foreground">
<Bath className="mr-2" /> {property.bathrooms} Bathrooms
</div>
<div className="flex items-center text-muted-foreground">
<Square className="mr-2" /> {property.area} sqft
</div>
<div className="flex items-center text-muted-foreground">
<MapPin className="mr-2" /> {property.address}
</div>
<div className="flex items-center text-muted-foreground">
<Calendar className="mr-2" /> Built in {property.yearBuilt}
</div>
</div>
<p className="mb-6 text-muted-foreground">{property.description}</p>
<Card className="mb-6">
<CardHeader>
<CardTitle>Amenities</CardTitle>
</CardHeader>
<CardContent>
<ul className="grid grid-cols-2 gap-2">
{property.amenities.map((amenity, index) => (
<li key={index} className="flex items-center text-muted-foreground">
<DollarSign className="mr-2 h-4 w-4" /> {amenity}
</li>
))}
</ul>
</CardContent>
</Card>
<Button className="w-full">Schedule a Viewing</Button>
</div>
</div>
</div>
)
}

116
src/app/properties/page.tsx Normal file
View File

@ -0,0 +1,116 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
import { Slider } from "~/components/ui/slider"
import { Search, Home, Bed, Bath, Square } from 'lucide-react'
// Mock data for properties
const properties = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
title: `Property ${i + 1}`,
type: i % 3 === 0 ? 'House' : i % 3 === 1 ? 'Apartment' : 'Condo',
bedrooms: Math.floor(Math.random() * 5) + 1,
bathrooms: Math.floor(Math.random() * 3) + 1,
area: Math.floor(Math.random() * 1000) + 500,
price: Math.floor(Math.random() * 500000) + 100000,
}))
export default function PropertiesPage() {
const [sortBy, setSortBy] = useState('price')
const [filterType, setFilterType] = useState('All')
const [priceRange, setPriceRange] = useState([0, 1000000])
const [searchTerm, setSearchTerm] = useState('')
const filteredAndSortedProperties = properties
.filter(property =>
(filterType === 'All' || property.type === filterType) &&
property.price >= priceRange[0] && property.price <= priceRange[1] &&
property.title.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price
if (sortBy === 'bedrooms') return b.bedrooms - a.bedrooms
return 0
})
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8 text-primary">Available Properties</h1>
<div className="mb-8 flex flex-wrap gap-4">
<Input
placeholder="Search properties..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="max-w-xs"
/>
<Select onValueChange={setSortBy} defaultValue={sortBy}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="price">Price</SelectItem>
<SelectItem value="bedrooms">Bedrooms</SelectItem>
</SelectContent>
</Select>
<Select onValueChange={setFilterType} defaultValue={filterType}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Property type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All</SelectItem>
<SelectItem value="House">House</SelectItem>
<SelectItem value="Apartment">Apartment</SelectItem>
<SelectItem value="Condo">Condo</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mb-8">
<h2 className="text-xl font-semibold mb-2 text-primary">Price Range</h2>
<Slider
min={0}
max={1000000}
step={10000}
value={priceRange}
onValueChange={setPriceRange}
className="max-w-md"
/>
<div className="mt-2 text-sm text-muted-foreground">
${priceRange[0].toLocaleString()} - ${priceRange[1].toLocaleString()}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredAndSortedProperties.map((property) => (
<Card key={property.id}>
<CardHeader>
<CardTitle>{property.title}</CardTitle>
</CardHeader>
<CardContent>
<img src={`/placeholder.svg?height=200&width=300`} alt={property.title} className="w-full h-48 object-cover mb-4 rounded" />
<div className="flex justify-between text-muted-foreground">
<span><Home className="inline mr-1" /> {property.type}</span>
<span><Bed className="inline mr-1" /> {property.bedrooms}</span>
<span><Bath className="inline mr-1" /> {property.bathrooms}</span>
<span><Square className="inline mr-1" /> {property.area} sqft</span>
</div>
<p className="mt-4 text-xl font-bold text-primary">${property.price.toLocaleString()}</p>
</CardContent>
<CardFooter>
<Link href={`/properties/${property.id}`} passHref>
<Button className="w-full">View Details</Button>
</Link>
</CardFooter>
</Card>
))}
</div>
</div>
)
}

0
src/auth.config.ts Normal file → Executable file
View File

0
src/auth.ts Normal file → Executable file
View File

View File

@ -0,0 +1,47 @@
"use server"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "~/components/ui/avatar"
import { auth } from "~/auth"
import Sign_Out_Button from "~/components/auth/server/SignOutButton"
export default async function Avatar_Popover() {
const session = await auth();
const pfp = session?.user?.image ?? "";
const users_name = session?.user?.name ?? "New User";
const initials = users_name.split(" ").map((name) => name[0]).join("");
console.log(pfp);
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src={pfp} alt="@shadcn"/>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>{users_name}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<div className="w-full flex flex-row
justify-center items-center">
Settings
</div>
</DropdownMenuItem>
<DropdownMenuItem>
<Sign_Out_Button />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -0,0 +1,148 @@
"use client"
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from "~/components/ui/alert-dialog"
import { Button } from "~/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "~/components/ui/form"
import { Input } from "~/components/ui/input"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { useEffect, useState } from "react"
const formSchema = z.object({
users_name: z.string().min(2, {
message: "Really? Your name is one letter? You expect me to believe that?",
}),
users_profile_image: z.string().url().optional(),
});
export default function First_Sign_In_Form({ users_name, users_email }: { users_name: string, users_email: string }) {
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});
const update_users_name = async (users_name: string, users_email: string) => {
try {
const res = await fetch("/api/users/set_users_name_by_email", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
users_name: users_name,
users_email: users_email,
}),
});
if (!res.ok) {
throw new Error("Failed to update user's name");
}
} catch (error) {
console.error("Could not update user's name", error);
}
};
const update_users_pfp = async (users_pfp: string, users_email: string) => {
try {
const res = await fetch("/api/users/set_users_pfp_by_email", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
users_pfp: users_pfp,
users_email: users_email,
}),
});
if (!res.ok) {
throw new Error("Failed to update user's name");
}
} catch (error) {
console.error("Could not update user's name", error);
}
};
const onSubmit = async (data: z.infer<typeof formSchema>) => {
if (data.users_profile_image === undefined) {
data.users_profile_image = "";
}
await update_users_name(data.users_name, users_email);
await update_users_pfp(data.users_profile_image, users_email);
setIsOpen(false);
};
useEffect(() => {
if (users_name === "New User") {
setIsOpen(true);
}
}, [users_name]);
if (users_name === "New User") {
return (
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Welcome to the Tenant Portal</AlertDialogTitle>
<AlertDialogDescription>
Please fill out the form to complete your account setup.
</AlertDialogDescription>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="users_name"
render={({ field }) => (
<FormItem>
<FormLabel>Enter your name</FormLabel>
<FormControl>
<Input placeholder="First & Last Name" {...field} />
</FormControl>
</FormItem>
)}
>
</FormField>
<FormField
control={form.control}
name="users_profile_image"
render={({ field }) => (
<FormItem>
<FormLabel>Enter a URL path to your profile picture</FormLabel>
<FormControl>
<Input placeholder="https://example.com/profile.jpg" {...field} />
</FormControl>
</FormItem>
)}
>
</FormField>
<div className="w-full flex justify-end">
<Button type="submit" variant="secondary"
className="mt-2"
>
Submit
</Button>
</div>
</form>
</Form>
</AlertDialogHeader>
</AlertDialogContent>
</AlertDialog>
);
} else {
return (
<div/>
);
}
};

View File

@ -0,0 +1,9 @@
const No_Session = () => {
return (
<div className="w-2/3 mx-auto text-center pt-10">
<h1 className="text-3xl font-semibold pb-4">Unauthorized Access</h1>
<p className="text-xl">Please sign in to view this page.</p>
</div>
);
}
export default No_Session;

14
src/components/auth/client/SignInAppleButton.tsx Normal file → Executable file
View File

@ -4,12 +4,12 @@ import Image from "next/image"
export default function Sign_In() {
return (
<div className="flex flex-row bg-primary py-3 px-10 rounded-xl text-lg font-semibold
mt-10 text-background my-auto">
<Image src="/logos/Apple_logo_black.svg" alt="Apple logo" width={20} height={20}
className="mr-4 my-auto"
/>
<Button onClick={() => signIn("apple")}>Sign in with Apple</Button>
</div>
<Button onClick={() => signIn("apple")} className="flex flex-row bg-primary py-3
px-10 rounded-md text-md font-semibold text-background">
<Image src="/logos/Apple_logo_black.svg" alt="Apple logo" width={16} height={16}
className="mr-4"
/>
Sign in with Apple
</Button>
);
}

10
src/components/auth/client/SignOutButton.tsx Normal file → Executable file
View File

@ -2,5 +2,13 @@ import { signOut } from "next-auth/react"
import { Button } from "~/components/ui/button"
export default function Sign_Out() {
return <Button onClick={() => signOut()}>Sign Out</Button>
return (
<Button
onClick={() => signOut()}
className="flex flex-row bg-yellow py-3
px-10 rounded-md text-md font-semibold text-background"
>
Sign out
</Button>
);
}

0
src/components/auth/server/SignInAppleButton.tsx Normal file → Executable file
View File

7
src/components/auth/server/SignOutButton.tsx Normal file → Executable file
View File

@ -1,14 +1,19 @@
import { Button } from "~/components/ui/button"
import { signOut } from "~/auth"
export default function Sign_Out() {
return (
<form
className="w-full"
action={async () => {
"use server"
await signOut()
}}
>
<button type="submit">Sign Out</button>
<Button type="submit" className="w-full"
>
Sign Out
</Button>
</form>
)
}

View File

@ -0,0 +1,82 @@
'use client'
import { useState } from 'react'
import { Button } from "~/components/ui/button"
import { Input } from "~/components/ui/input"
import { Textarea } from "~/components/ui/textarea"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
import { Label } from "~/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
export default function ContactForm() {
const [formStatus, setFormStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle')
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setFormStatus('submitting')
// Here you would typically send the form data to your server
// For this example, we'll just simulate a submission
setTimeout(() => {
setFormStatus('success')
}, 2000)
}
return (
<Card>
<CardHeader>
<CardTitle>Send us a message</CardTitle>
<CardDescription>We&apos;ll get back to you as soon as possible.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" name="name" placeholder="Your name" required />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" placeholder="Your email" type="email" required />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="phone">Phone</Label>
<Input id="phone" name="phone" placeholder="Your phone number" type="tel" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="subject">Subject</Label>
<Select name="subject" required>
<SelectTrigger>
<SelectValue placeholder="Select a subject" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">General Inquiry</SelectItem>
<SelectItem value="viewing">Property Viewing</SelectItem>
<SelectItem value="application">Rental Application</SelectItem>
<SelectItem value="maintenance">Maintenance Request</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="message">Message</Label>
<Textarea id="message" name="message" placeholder="Your message" required />
</div>
</div>
<CardFooter className="flex justify-between mt-4 px-0">
<Button type="submit" disabled={formStatus === 'submitting'}>
{formStatus === 'submitting' ? 'Sending...' : 'Send Message'}
</Button>
{formStatus === 'success' && (
<p className="text-sm text-green-600">Message sent successfully!</p>
)}
{formStatus === 'error' && (
<p className="text-sm text-red-600">There was an error sending your message. Please try again.</p>
)}
</CardFooter>
</form>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,87 @@
"use server"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
const schema = z.object({
billType: z.enum(["Rent", "Power", "Internet", "Gas", "Water", "Phone Bill", "Cable",
"Security Deposit", "Other"]),
billDescription: z.string().optional(),
amount: z.number(),
recurrence: z.enum(["No recurrence", "Monthly", "Bi-weekly", "Weekly", "Annually"]),
includeUserInSplit: z.boolean().optional(),
roommates: z.array(z.object({
userID: z.string(),
includeInSplit: z.boolean(),
})).optional(),
});
export async function createBill(formData: FormData) {
const validateFields = schema.safeParse({
billType: formData.get("billType"),
billDescription: formData.get("billDescription"),
amount: formData.get("amount"),
recurrence: formData.get("recurrence"),
includeUserInSplit: formData.get("includeUserInSplit"),
roommates: formData.get("roommates"),
});
if (!validateFields.success) {
return {
errors: validateFields.error.flatten().fieldErrors,
}
}
}
export default async function BillForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: {
//billType: "Rent",
recurrence: "No recurrence",
includeUserInSplit: true,
},
});
return (
<div className="">
<form action={createBill}>
<div>
<label htmlFor="billType">Bill Type</label>
<select id="billType" name="billType" required className="">
<option value="Rent">Rent</option>
<option value="Power">Power</option>
<option value="Internet">Internet</option>
<option value="Gas">Gas</option>
<option value="Water">Water</option>
<option value="Phone Bill">Phone Bill</option>
<option value="Cable">Cable</option>
<option value="Security Deposit">Security Deposit</option>
<option value="Other">Other</option>
</select>
</div>
<div>
<label htmlFor="billDescription">Bill Description</label>
<input type="text" id="billDescription" name="billDescription" className="" />
</div>
<div>
<label htmlFor="amount">Amount</label>
<input type="number" id="amount" name="amount" required className="" />
</div>
<div>
<label htmlFor="recurrence">Recurrence</label>
<select id="recurrence" name="recurrence" required className="">
<option value="No recurrence">No recurrence</option>
<option value="Monthly">Monthly</option>
<option value="Bi-weekly">Bi-weekly</option>
<option value="Weekly">Weekly</option>
<option value="Annually">Annually</option>
</select>
</div>
<div>
<label htmlFor="includeUserInSplit">Include Yourself in Bill Split?</label>
<input type="checkbox" id="includeUserInSplit" name="includeUserInSplit" className="" />
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,65 @@
"use client"
import * as React from "react"
import { Calendar } from "~/components/ui/BillTrackerCalendar"
import CreateBillForm from "~/components/portal/billtracker/CreateBillForm"
export default function BillTrackerCalendar() {
const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(undefined)
const [isOpen, setIsOpen] = React.useState(false)
const calendarRef = React.useRef<HTMLDivElement>(null)
const popoverRef = React.useRef<HTMLDivElement>(null)
const handleSelect = (date: Date | undefined) => {
if (date) {
if (selectedDate && date.getTime() === selectedDate.getTime())
setIsOpen(!isOpen)
else {
setSelectedDate(date)
setIsOpen(true)
}
} else
setIsOpen(false)
}
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
calendarRef.current &&
popoverRef.current &&
!calendarRef.current.contains(event.target as Node) &&
!popoverRef.current.contains(event.target as Node)
) {
//setIsOpen(false)
console.log('Calendar closed');
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
return (
<div className="m-auto p-2 relative mt-10" ref={calendarRef}>
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleSelect}
className="rounded-md border m-auto"
/>
{isOpen && selectedDate && (
<div
ref={popoverRef}
className="absolute top-full left-1/2 transform -translate-x-1/2
border rounded-lg shadow-lg px-4 pb-4 w-80"
>
<div className="grid gap-4">
<div className="space-y-2">
< CreateBillForm date={selectedDate} setIsOpen={setIsOpen} />
</div>
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,319 @@
"use client"
import * as React from "react"
import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "~/lib/utils"
import {
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "~/components/ui/drawer"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { Input } from "~/components/ui/input"
import { Button } from "~/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "~/components/ui/form"
import {
Command,
CommandEmpty,
CommandList,
CommandGroup,
CommandItem,
} from "~/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover"
import { Checkbox } from "~/components/ui/checkbox"
type CreateBillDrawerProps = {
date: Date;
};
type ComboOption = {
label: string;
value: string;
};
const FormSchema = z.object({
billType: z.enum(["Rent", "Power", "Internet", "Gas", "Water", "Phone Bill", "Cable",
"Security Deposit", "Other"]),
billDescription: z.string().optional(),
amount: z.number(),
recurrence: z.enum(["No recurrence", "Monthly", "Bi-weekly", "Weekly", "Annually"]),
includeUserInSplit: z.boolean().optional(),
roommates: z.array(z.object({
userID: z.string(),
includeInSplit: z.boolean(),
})).optional(),
});
const billTypeCombo: ComboOption[] =
FormSchema.shape.billType._def.values.map((billType: string) => ({
value: billType,
label: billType,
}));
const recurrenceCombo: ComboOption[] =
FormSchema.shape.recurrence._def.values.map((recurrence: string) => ({
value: recurrence,
label: recurrence,
}));
export default function CreateBillDrawer({date}: CreateBillDrawerProps) {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
//billType: "Rent",
recurrence: "No recurrence",
includeUserInSplit: true,
},
});
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
console.log('API received data:', data);
try {
console.log('API received data:', data);
//const res = await fetch("/api/bills/createBill", {
//method: "POST",
//headers: {
//"Content-Type": "application/json",
//},
//body: JSON.stringify({
//date: date,
//billType: data.billType,
//billDescription: data.billDescription,
//amount: data.amount,
//recurrence: data.recurrence,
//includeUserInSplit: data.includeUserInSplit,
//roommates: data.roommates,
//}),
//});
//if (!res.ok) {
//throw new Error("Failed to create bill");
//}
} catch (error) {
console.error("Could not create bill", error);
}
};
return (
<DrawerContent className="w-full mx-auto items-center justify-center">
<DrawerHeader>
<DrawerTitle className="text-center md:text-2xl">
{date.toDateString()}
</DrawerTitle>
</DrawerHeader>
<div className="flex flex-row w-full mx-auto justify-center">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex flex-row mt-4">
<FormField
control={form.control}
name="billType"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="my-auto">
Bill Type
</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[160px] justify-between mr-2",
!field.value && "text-muted-foreground"
)}
>
{field.value
? billTypeCombo.find(
(billType) => billType.value === field.value
)?.label
: "Select bill type"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[200px]">
<Command>
<CommandList>
<CommandEmpty>No bill type found.</CommandEmpty>
<CommandGroup>
{billTypeCombo.map((billType) => (
<CommandItem
value={billType.label}
key={billType.value}
onSelect={() => {
form.setValue("billType", billType.value as typeof field.value);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
billType.value === field.value
? "opacity-100"
: "opacity-0"
)}
/>
{billType.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</FormItem>
)}
>
</FormField>
<FormField
control={form.control}
name="billDescription"
render={({ field }) => (
<FormItem>
<FormLabel>Bill Description</FormLabel>
<FormControl>
<Input
placeholder="Optional Bill Description or any additional notes"
{...field}
className="md:w-[400px]"
/>
</FormControl>
</FormItem>
)}
>
</FormField>
</div>
<div className="flex flex-row my-4">
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem className="mr-2">
<FormLabel>Amount</FormLabel>
<FormControl>
<Input placeholder="Amount" {...field} />
</FormControl>
</FormItem>
)}
>
</FormField>
<FormField
control={form.control}
name="recurrence"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="my-auto">Recurrence</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[160px] justify-between mr-2",
!field.value && "text-muted-foreground"
)}
>
{field.value
? recurrenceCombo.find(
(recurrence) => recurrence.value === field.value
)?.label
: "Select recurrence"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[200px]">
<Command>
<CommandList>
<CommandEmpty>No recurrence found.</CommandEmpty>
<CommandGroup>
{recurrenceCombo.map((recurrence) => (
<CommandItem
value={recurrence.label}
key={recurrence.value}
onSelect={() => {
form.setValue("recurrence", recurrence.value as typeof field.value);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
recurrence.value === field.value
? "opacity-100"
: "opacity-0"
)}
/>
{recurrence.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</FormItem>
)}
>
</FormField>
<FormField
control={form.control}
name="includeUserInSplit"
render={({ field }) => (
<FormItem className="mt-10">
<div className="flex flex-row">
<FormLabel>Include Yourself in Bill Split?</FormLabel>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
className="ml-2"
/>
</FormControl>
</div>
</FormItem>
)}
>
</FormField>
</div>
<FormField
control={form.control}
name="roommates"
render={({ field }) => (
<FormItem>
<FormLabel>Roommates</FormLabel>
<FormControl>
</FormControl>
</FormItem>
)}
>
</FormField>
<div className="w-full flex justify-end">
<Button type="submit" variant="secondary"
className="mt-2"
>
Submit
</Button>
</div>
</form>
</Form>
</div>
<DrawerFooter>
<DrawerClose />
</DrawerFooter>
</DrawerContent>
);
}

View File

@ -0,0 +1,42 @@
import React from "react";
import {
Drawer,
DrawerTrigger,
} from "~/components/ui/drawer"
import CreateBillDrawer from "~/components/portal/billtracker/CreateBillDrawer"
import { Button } from "~/components/ui/button"
type CreateBillFormProps = {
date: Date;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
};
// Create Bill Form. Get date from calendar
export default function CreateBillForm({date, setIsOpen}: CreateBillFormProps) {
return (
<div>
<div className="flex flex-row w-full">
<h3 className="font-medium leading-none
text-center mx-auto mt-2 py-2 md:text-xl">
{date.toDateString()}
</h3>
<button className="justify-self-end ml-auto bg-none
text-primary text-m md:text-xl"
onClick={() => setIsOpen(false)}
>
x
</button>
</div>
<Drawer>
<DrawerTrigger className="w-full">
<Button variant="outline" size="icon"
className="border-none w-full"
>
Create new Bill
</Button>
</DrawerTrigger>
<CreateBillDrawer date={date} />
</Drawer>
</div>
);
}

View File

@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import {
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
} from "~/components/ui/drawer"
import BillForm from "~/components/portal/billtracker/BillForm"
type CreateBillDrawerProps = {
date: Date;
};
export default function CreateBillDrawer({date}: CreateBillDrawerProps) {
return (
<DrawerContent className="w-full mx-auto items-center justify-center">
<DrawerHeader>
<DrawerTitle className="text-center md:text-2xl">
{date.toDateString()}
</DrawerTitle>
</DrawerHeader>
<div className="flex flex-row w-full mx-auto justify-center">
<BillForm />
</div>
<DrawerFooter>
<DrawerClose />
</DrawerFooter>
</DrawerContent>
);
}

View File

@ -0,0 +1,27 @@
import { Outfit as FontSans } from "next/font/google";
import { cn } from "~/lib/utils"
import Link from "next/link"
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export default function Hero() {
return (
<Link href="/">
<div className="flex flex-col justify-start items-start">
<h1 className={cn("text-4xl md:text-5xl lg:text-6xl font-bold text-center font-sans antialiased",
fontSans.variable)}
>
TENANT
</h1>
<h1 className={cn("text-4xl md:text-5xl lg:text-6xl font-bold text-center font-sans antialiased",
fontSans.variable)}
>
PORTAL
</h1>
</div>
</Link>
);
};

View File

@ -0,0 +1,48 @@
import { Outfit as FontSans } from "next/font/google";
import { cn } from "~/lib/utils"
import Link from "next/link"
import {
Card,
CardContent,
} from "~/components/ui/card"
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export default function Nav_Bar() {
return (
<div className={cn("flex flex-col justify-start items-start h-5/6 my-auto" +
"py-6 text-lg md:text-xl lg:text-2xl font-semibold font-sans antialiased", fontSans.variable)}
>
<Card className="md:p-4">
<CardContent className="py-4">
<Link href="/">
Payments
</Link>
</CardContent>
<CardContent className="py-4">
<Link href="/">
Workorders
</Link>
</CardContent>
<CardContent className="py-4">
<Link href="/">
Messages
</Link>
</CardContent>
<CardContent className="py-4">
<Link href="/">
Documents
</Link>
</CardContent>
<CardContent className="pt-4">
<Link href="/account/billtracker">
Bill Tracker
</Link>
</CardContent>
</Card>
</div>
);
};

View File

View File

@ -0,0 +1,31 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbSeparator,
} from "~/components/ui/breadcrumb"
import Link from "next/link"
export default function Breadcrumb_Home() {
return (
<Breadcrumb className="w-full m-auto flex flex-row justify-center items-center">
<BreadcrumbList>
<BreadcrumbItem>
<Link href="/">
<h1 className="text-xl pl-20 pt-4 lg:text-3xl lg:pl-0 lg:pt-0 font-bold text-center font-sans antialiased">
Dashboard
</h1>
</Link>
</BreadcrumbItem>
<BreadcrumbSeparator className="pt-4 lg:pt-0"/>
<BreadcrumbItem>
<Link href="/billtracker">
<h1 className="text-xl pt-4 lg:text-3xl lg:pt-0 font-bold text-center font-sans antialiased">
Bill Tracker
</h1>
</Link>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@ -0,0 +1,22 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
} from "~/components/ui/breadcrumb"
import Link from "next/link"
export default function Breadcrumb_Home() {
return (
<Breadcrumb className="w-full m-auto flex flex-row justify-center items-center">
<BreadcrumbList>
<BreadcrumbItem>
<Link href="/account">
<h1 className="text-3xl font-bold text-center font-sans antialiased">
Dashboard
</h1>
</Link>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
};

0
src/components/theme/theme_provider.tsx Normal file → Executable file
View File

4
src/components/theme/theme_toggle.tsx Normal file → Executable file
View File

@ -13,9 +13,10 @@ import {
export default function Theme_Toggle() {
const { setTheme } = useTheme()
return (
<div className="mx-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Button variant="outline" size="icon" className="border-none">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
@ -33,5 +34,6 @@ export default function Theme_Toggle() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

View File

@ -0,0 +1,65 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "~/lib/utils"
import { buttonVariants } from "~/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-2xl lg:text-4xl font-semibold p-2",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-12 w-12 lg:h-18 lg:w-18 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-16 lg:w-24 font-normal text-[1.2rem]",
row: "flex w-full mt-2",
cell: "h-16 w-16 lg:h-24 lg:w-24 text-center text-sm lg:text-xl p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-16 w-16 lg:h-24 lg:w-24 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-6 w-6" />,
IconRight: ({ ...props }) => <ChevronRight className="h-6 w-6" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "~/lib/utils"
import { buttonVariants } from "~/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

50
src/components/ui/avatar.tsx Executable file
View File

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "~/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

115
src/components/ui/breadcrumb.tsx Executable file
View File

@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "~/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

0
src/components/ui/button.tsx Normal file → Executable file
View File

View File

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "~/lib/utils"
import { buttonVariants } from "~/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

79
src/components/ui/card.tsx Executable file
View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "~/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

365
src/components/ui/chart.tsx Normal file
View File

@ -0,0 +1,365 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "~/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

30
src/components/ui/checkbox.tsx Executable file
View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "~/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

155
src/components/ui/command.tsx Executable file
View File

@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "~/lib/utils"
import { Dialog, DialogContent } from "~/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

122
src/components/ui/dialog.tsx Executable file
View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "~/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

118
src/components/ui/drawer.tsx Executable file
View File

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "~/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

0
src/components/ui/dropdown-menu.tsx Normal file → Executable file
View File

178
src/components/ui/form.tsx Executable file
View File

@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "~/lib/utils"
import { Label } from "~/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

25
src/components/ui/input.tsx Executable file
View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "~/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

26
src/components/ui/label.tsx Executable file
View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "~/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

31
src/components/ui/popover.tsx Executable file
View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "~/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "~/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "~/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "~/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

31
src/components/ui/sonner.tsx Executable file
View File

@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

117
src/components/ui/table.tsx Executable file
View File

@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "~/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "~/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "~/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

129
src/components/ui/toast.tsx Executable file
View File

@ -0,0 +1,129 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "~/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

35
src/components/ui/toaster.tsx Executable file
View File

@ -0,0 +1,35 @@
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "~/components/ui/toast"
import { useToast } from "~/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

194
src/components/ui/use-toast.ts Executable file
View File

@ -0,0 +1,194 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "~/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

0
src/env.js Normal file → Executable file
View File

0
src/lib/utils.ts Normal file → Executable file
View File

0
src/middleware.ts Normal file → Executable file
View File

0
src/scripts/generate_apple_secret.ts Normal file → Executable file
View File

0
src/server/db/index.ts Normal file → Executable file
View File

213
src/server/db/schema.ts Normal file → Executable file
View File

@ -1,29 +1,72 @@
//import { sql } from "drizzle-orm";
import {
boolean,
timestamp,
pgTable,
text,
pgEnum,
primaryKey,
integer,
numeric,
} from "drizzle-orm/pg-core"
import type { AdapterAccountType } from "next-auth/adapters"
import postgres from "postgres"
import { drizzle } from "drizzle-orm/postgres-js"
import type { AdapterAccountType } from "next-auth/adapters"
const connectionString = process.env.DATABASE_URL ?? "";
const pool = postgres(connectionString, { max: 1 })
export const db = drizzle(pool)
export const frequencyEnum = pgEnum("frequency", ["Monthly", "Bi-weekly", "Weekly"]);
export const workOrderStatusEnum = pgEnum("workOrderStatus", ["Pending", "Open", "Closed"]);
export const paymentTypeEnum = pgEnum("paymentType", ["Security Deposit", "Rent", "Late Fee", "Other"]);
export const paymentStatusEnum = pgEnum("paymentStatus", ["Pending", "Complete", "Late", "Refunded"]);
export const preferredDaysofWeekEnum = pgEnum("preferredDaysofWeek",
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
);
export const propertyTypeEnum = pgEnum("propertyType", ["Apartment", "Condominium",
"Mobile Home", "Multi-Unit Home", "Single-Family Residence", "Townhouse"]
);
export const workOrderPriorityEnum = pgEnum("workOrderPriority", ["Low", "High"]
);
export const workOrderTypeEnum = pgEnum("workOrderType",
["Appliance Repair", "Carbon Monoxide Detector Installation", "Ceiling Fan Repair",
"Carpentry Repair", "Door Installation/Repair", "Drywall Installation/Repair",
"Electrical Repair", "Extermination", "Fire Alarm Maintenance", "Fencing Repair",
"Gutter Cleaning", "HVAC Repair", "Light Fixture Repair", "Mold Remediation",
"Painting", "Pest Control", "Pool/Hot Tub Maintenance", "Plumbing Repair",
"Roof Repair/Maintenance", "Septic System Maintenance", "Smoke Detector Replacement",
"Tile Flooring", "Tree Trimming/Cutting", "Water Treatment", "Well/Water Testing",
"Window Repair/Installation"]
);
export const billStatusEnum = pgEnum("billStatus",
["Awaiting Payment", "Paid", "Scheduled", "Late", "Refunded"]
);
export const billTypeEnum = pgEnum("billType",
["Rent", "Power", "Internet", "Gas", "Water", "Phone Bill", "Cable",
"Security Deposit", "Other"]
);
export const billPaymentTypeEnum = pgEnum("billPaymentType",
["Paid Online", "Zelle", "Cash", "Cash App", "Apple Pay"]
);
export const billRecurrenceEnum = pgEnum("billRecurrence",
["Monthly", "Bi-weekly", "Weekly", "Annually"]
);
export const users = pgTable("user", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").unique(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
image: text("image"),
})
export const users = pgTable(
"user",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").unique().notNull(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
image: text("image"),
phoneNumber: text("phoneNumber"),
propertyID: text("propertyID").references(() => properties.id, { onDelete: "cascade" }),
stripeCustomerID: text("stripeCustomerID"),
}
)
export const accounts = pgTable(
"account",
@ -49,13 +92,16 @@ export const accounts = pgTable(
})
)
export const sessions = pgTable("session", {
sessionToken: text("sessionToken").primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
})
export const sessions = pgTable(
"session",
{
sessionToken: text("sessionToken").primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
}
)
export const verificationTokens = pgTable(
"verificationToken",
@ -91,3 +137,132 @@ export const authenticators = pgTable(
}),
})
)
export const admins = pgTable(
"admin",
{
id: text("id").primaryKey(),
userID: text("userID").notNull().references(() => users.id, { onDelete: "cascade" }),
}
)
export const properties = pgTable(
"property",
{
id: text("id").primaryKey(),
address1: text("address1").unique().notNull(),
address2: text("address2"),
city: text("city").notNull(),
state: text("state").notNull(),
zip: text("zip").notNull(),
monthlyRent: numeric("monthlyRent").notNull(),
securityDeposit: numeric("securityDeposit"),
leaseStartDate: timestamp("leaseStartDate").notNull(),
leaseEndDate: timestamp("leaseEndDate").notNull(),
propertyType: propertyTypeEnum("propertyType").notNull(),
}
)
export const payments = pgTable(
"payment",
{
id: text("id").primaryKey(),
userID: text("userID").notNull().references(() => users.id, { onDelete: "cascade" }),
stripePaymentID: text("stripeID"),
amount: numeric("amount").notNull(),
paymentDate: timestamp("paymentDate").notNull(),
paymentType: paymentTypeEnum("paymentType").notNull(),
paymentStatus: paymentStatusEnum("paymentStatus").notNull(),
}
)
export const autoPayments = pgTable(
"autoPayment",
{
id: text("id").primaryKey(),
userID: text("userID").notNull().references(() => users.id, { onDelete: "cascade" }),
amount: numeric("amount").notNull(),
frequency: frequencyEnum("frequency").notNull(),
preferredDayofWeek: preferredDaysofWeekEnum("preferredDayofWeek").notNull(),
startDate: timestamp("startDate").notNull(),
nextPaymentDate: timestamp("nextPaymentDate").notNull(),
stripePaymentMethodID: text("stripePaymentMethodID").notNull(),
}
)
export const workorders = pgTable(
"workorder",
{
id: text("id").primaryKey(),
userID: text("userID").notNull().references(() => users.id, { onDelete: "cascade" }),
date: timestamp("date").notNull(),
type: workOrderTypeEnum("type").notNull(),
status: workOrderStatusEnum("status").notNull().default("Pending"),
priority: workOrderPriorityEnum("priority").notNull().default("Low"),
title: text("title").notNull(),
description: text("description"),
}
)
export const documents = pgTable(
"document",
{
id: text("id").primaryKey(),
propertyID: text("propertyID").notNull().references(() => properties.id, { onDelete: "cascade" }),
name: text("name").notNull(),
type: text("type").notNull(),
file: text("file").notNull(),
}
)
export const emergencyContacts = pgTable(
"emergencyContact",
{
id: text("id").primaryKey(),
userID: text("userID").notNull().references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
mobilePhoneNumber: text("mobilePhoneNumber"),
workPhoneNumber: text("workPhoneNumber"),
email: text("email"),
}
)
export const bills = pgTable(
"bill",
{
id: text("id").primaryKey(),
billType: billTypeEnum("billType").notNull(),
billDescription: text("billDescription"),
createdBy: text("createdBy").notNull().references(() => users.id, { onDelete: "cascade" }),
createdAt: timestamp("createdAt").notNull().defaultNow(),
dueDate: timestamp("dueDate").notNull(),
amount: numeric("amount").notNull(),
recurrence: billRecurrenceEnum("recurrence"),
attachmentUrl: text("attachmentUrl"),
}
)
export const billsSplitBetween = pgTable(
"billSplitBetween",
{
id: text("id").primaryKey(),
billID: text("billID").notNull().references(() => bills.id, { onDelete: "cascade" }),
userID: text("userID").notNull().references(() => users.id, { onDelete: "cascade" }),
amount: numeric("amount").notNull(),
status: billStatusEnum("status").notNull(),
paymentType: billPaymentTypeEnum("paymentType"),
paidAt: timestamp("paidAt"),
attachmentUrl: text("attachmentUrl"),
}
)
export const billReminders = pgTable(
"billReminders",
{
id: text("id").primaryKey(),
billID: text("billID").notNull().references(() => bills.id, { onDelete: "cascade" }),
userID: text("userID").notNull().references(() => users.id, { onDelete: "cascade" }),
reminderDate: timestamp("reminderDate").notNull(),
reminderSent: boolean("reminderSent").notNull().default(false),
}
)

43
src/server/functions.ts Executable file
View File

@ -0,0 +1,43 @@
import "server-only"
import { db } from "~/server/db"
import * as schema from "~/server/db/schema"
import { eq } from "drizzle-orm"
//import { sql } from "drizzle-orm"
export const set_users_name_by_email = async (users_name: string, users_email: string) => {
try {
await db.update(schema.users)
.set({ name: users_name })
.where(eq(schema.users.email, users_email))
} catch (error) {
console.error('Error updating user name:', error);
throw error; // Ensure we rethrow to be caught by the calling function
}
};
export const get_users_name_by_email = async (users_email: string) => {
const result = await db.select({
users_name: schema.users.name,
}).from(schema.users)
.where(eq(schema.users.email, users_email))
return result;
}
export const set_users_pfp_by_email = async (users_pfp: string, users_email: string) => {
try {
await db.update(schema.users)
.set({ image: users_pfp })
.where(eq(schema.users.email, users_email))
} catch (error) {
console.error('Error updating user pfp:', error);
throw error;
}
};
export const get_users_pfp_by_email = async (users_email: string) => {
const result = await db.select({
users_pfp: schema.users.image,
}).from(schema.users)
.where(eq(schema.users.email, users_email))
return result;
}

0
src/styles/globals.css Normal file → Executable file
View File

1
tailwind.config.ts Normal file → Executable file
View File

@ -56,6 +56,7 @@ const config = {
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
outfit: ["var(--font-outfit)", ...fontFamily.sans],
},
borderRadius: {
lg: "var(--radius)",

0
tsconfig.json Normal file → Executable file
View File