767 lines
29 KiB
TypeScript
767 lines
29 KiB
TypeScript
import { define } from "../../../utils.ts";
|
||
import { page } from "fresh";
|
||
import type { SessionCookie } from "../../auth/callback.ts";
|
||
import {
|
||
markRoleOnboarded,
|
||
startGuestOnboarding,
|
||
} from "../../../services/onboardingService.ts";
|
||
import { getPropertyById } from "../../../services/propertyService.ts";
|
||
import { getPhotosByProperty } from "../../../services/propertyMediaService.ts";
|
||
import {
|
||
getTenantInviteById,
|
||
removeTenantInviteEmail,
|
||
} from "../../../services/tenantInviteService.ts";
|
||
import { getUserById } from "../../../services/userService.ts";
|
||
import { getLeaseById } from "../../../services/leaseService.ts";
|
||
import { createMessage } from "../../../services/messageService.ts";
|
||
import LoginPurposeButton from "../../../islands/LoginPurposeButton.tsx";
|
||
import type {
|
||
Lease,
|
||
Property,
|
||
PropertyMedia,
|
||
TenantInvite,
|
||
User,
|
||
} from "../../../db/interfaces.ts";
|
||
|
||
type InvitePreviewData = {
|
||
invite: TenantInvite | null;
|
||
property: Property | null;
|
||
photos: PropertyMedia[];
|
||
user: User | null;
|
||
inviteeEmail: string;
|
||
status?: string;
|
||
error?: string;
|
||
lease?: Lease | null;
|
||
};
|
||
|
||
function buildPropertyLabel(property: Property | null) {
|
||
return property?.title?.trim() || property?.address_1?.trim() ||
|
||
"Rental Property";
|
||
}
|
||
|
||
function buildPropertyAddress(property: Property | null) {
|
||
if (!property) return "";
|
||
return [
|
||
property.address_1,
|
||
property.address_2,
|
||
property.city,
|
||
property.state,
|
||
property.zip,
|
||
].filter(Boolean).join(", ");
|
||
}
|
||
|
||
function formatCurrency(amount?: number | null) {
|
||
if (amount === null || amount === undefined) return "N/A";
|
||
return new Intl.NumberFormat("en-US", {
|
||
style: "currency",
|
||
currency: "USD",
|
||
maximumFractionDigits: 0,
|
||
}).format(amount);
|
||
}
|
||
|
||
function formatNumber(value?: number | null) {
|
||
if (value === null || value === undefined) return "N/A";
|
||
return new Intl.NumberFormat("en-US").format(value);
|
||
}
|
||
|
||
async function buildInvitePreview(
|
||
ctx: {
|
||
params: Record<string, string>;
|
||
req: Request;
|
||
state: { data: SessionCookie };
|
||
},
|
||
options: { error?: string } = {},
|
||
) {
|
||
const inviteId = decodeURIComponent(ctx.params.invite_id || "");
|
||
const url = new URL(ctx.req.url);
|
||
const status = url.searchParams.get("status") || undefined;
|
||
const stateData = ctx.state.data as SessionCookie;
|
||
const userId = stateData?.user_id;
|
||
const user = userId ? await getUserById(userId) : null;
|
||
const invite = inviteId ? await getTenantInviteById(inviteId) : null;
|
||
const property = invite?.property_id
|
||
? await getPropertyById(invite.property_id)
|
||
: null;
|
||
const photos = property?.id ? await getPhotosByProperty(property.id) : [];
|
||
const inviteeEmail = user?.email?.trim() || "";
|
||
const lease = invite?.lease_id ? await getLeaseById(invite.lease_id) : null;
|
||
|
||
return page({
|
||
invite,
|
||
property,
|
||
photos,
|
||
user,
|
||
inviteeEmail,
|
||
status,
|
||
lease,
|
||
error: options.error,
|
||
});
|
||
}
|
||
|
||
export const handler = define.handlers({
|
||
async GET(ctx) {
|
||
return await buildInvitePreview(ctx);
|
||
},
|
||
|
||
async POST(ctx) {
|
||
const inviteId = decodeURIComponent(ctx.params.invite_id || "");
|
||
const formData = await ctx.req.formData();
|
||
const intent = (formData.get("intent") || "").toString();
|
||
const stateData = ctx.state.data as SessionCookie;
|
||
const userId = stateData?.user_id;
|
||
const user = userId ? await getUserById(userId) : null;
|
||
const invite = inviteId ? await getTenantInviteById(inviteId) : null;
|
||
|
||
if (!invite) {
|
||
return await buildInvitePreview(ctx, {
|
||
error: "We could not find that invite.",
|
||
});
|
||
}
|
||
|
||
if (intent === "start") {
|
||
if (!userId) {
|
||
return new Response("", {
|
||
status: 302,
|
||
headers: { Location: "/auth/login" },
|
||
});
|
||
}
|
||
if (!invite.property_id) {
|
||
return await buildInvitePreview(ctx, {
|
||
error: "This invite is missing the property details.",
|
||
});
|
||
}
|
||
|
||
await startGuestOnboarding(userId, "tenant");
|
||
|
||
const cookieHeader = ctx.req.headers.get("Cookie") ?? "";
|
||
const requestUrl = new URL(ctx.req.url);
|
||
const apiUrl = `${requestUrl.origin}/api/lease/request-approval`;
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"Cookie": cookieHeader,
|
||
},
|
||
body: JSON.stringify({
|
||
invite_id: invite.id,
|
||
property_id: invite.property_id,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({
|
||
error: "Failed to request lease approval",
|
||
}));
|
||
return await buildInvitePreview(ctx, {
|
||
error: errorData.error || "Failed to request lease approval",
|
||
});
|
||
}
|
||
|
||
const responseData = await response.json().catch(() => ({}));
|
||
const lease = responseData?.lease;
|
||
const leaseId = lease?.id ?? null;
|
||
|
||
if (invite.landlord_id && leaseId) {
|
||
await createMessage(
|
||
leaseId,
|
||
invite.landlord_id,
|
||
userId,
|
||
"notice",
|
||
"Lease documents – next steps",
|
||
"The landlord was notified of your lease request. A follow-up message will contain more information about your lease documents.",
|
||
null,
|
||
null,
|
||
);
|
||
}
|
||
|
||
await markRoleOnboarded(userId, "tenant");
|
||
|
||
return new Response("", {
|
||
status: 302,
|
||
headers: {
|
||
Location: `/tenant/invite/${
|
||
encodeURIComponent(invite.id)
|
||
}?status=pending`,
|
||
},
|
||
});
|
||
}
|
||
|
||
if (intent === "decline") {
|
||
const inviteeEmail = (user?.email || formData.get("invitee_email") || "")
|
||
.toString()
|
||
.trim();
|
||
if (!inviteeEmail) {
|
||
return await buildInvitePreview(ctx, {
|
||
error: "Enter the email tied to the invite to decline it.",
|
||
});
|
||
}
|
||
const removal = await removeTenantInviteEmail(invite.id, inviteeEmail);
|
||
if (!removal.removed) {
|
||
return await buildInvitePreview(ctx, {
|
||
error: "We could not match that email to this invite.",
|
||
});
|
||
}
|
||
return new Response("", {
|
||
status: 302,
|
||
headers: {
|
||
Location: `/tenant/invite/${
|
||
encodeURIComponent(invite.id)
|
||
}?status=declined`,
|
||
},
|
||
});
|
||
}
|
||
|
||
return new Response("", {
|
||
status: 302,
|
||
headers: { Location: `/tenant/invite/${encodeURIComponent(inviteId)}` },
|
||
});
|
||
},
|
||
});
|
||
|
||
export default define.page(function TenantInvitePreview({ data }) {
|
||
const {
|
||
invite,
|
||
property,
|
||
photos,
|
||
user,
|
||
inviteeEmail,
|
||
status,
|
||
lease,
|
||
error,
|
||
} = data as InvitePreviewData;
|
||
|
||
const approvalStatus = lease?.approval_status;
|
||
const isPending = approvalStatus === "pending_approval";
|
||
const isApproved = approvalStatus === "approved";
|
||
const isRejected = approvalStatus === "rejected";
|
||
const hasLease = Boolean(lease);
|
||
// Allow "Start Lease" when no lease or when previous request was rejected (re-invite same property)
|
||
const canStartLease = !hasLease || isRejected;
|
||
|
||
if (!invite || !property) {
|
||
return (
|
||
<div class="min-h-screen bg-gray-50 tenant-route flex items-center justify-center px-6">
|
||
<div class="max-w-lg text-center bg-white shadow rounded-lg p-8">
|
||
<p class="text-sm uppercase tracking-[0.3em] text-gray-400">
|
||
Invite Preview
|
||
</p>
|
||
<h1 class="mt-4 text-2xl font-semibold text-gray-900">
|
||
{error || "We could not load this invite."}
|
||
</h1>
|
||
<p class="mt-3 text-sm text-gray-500">
|
||
Ask your landlord to resend the invite or verify the link.
|
||
</p>
|
||
<a
|
||
href="/"
|
||
class="mt-6 inline-flex items-center justify-center rounded-full bg-gray-900 px-4 py-2 text-sm font-semibold text-white hover:bg-gray-800"
|
||
>
|
||
Back to Home
|
||
</a>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const propertyLabel = buildPropertyLabel(property);
|
||
const propertyAddress = buildPropertyAddress(property);
|
||
const photoUrls = photos
|
||
.map((photo) => photo.file_url)
|
||
.filter((url): url is string => Boolean(url && url.trim().length > 0));
|
||
const fallbackPhoto = property.image_url?.trim() || "";
|
||
const displayedPhotos = photoUrls.length > 0
|
||
? photoUrls
|
||
: fallbackPhoto
|
||
? [fallbackPhoto]
|
||
: [];
|
||
const availableOn = invite.available_on
|
||
? new Date(invite.available_on).toLocaleDateString()
|
||
: null;
|
||
const cancelHref = user?.id ? "/tenant/dashboard" : "/";
|
||
const isLoggedIn = Boolean(user?.id);
|
||
|
||
const approvalSteps = [
|
||
{ label: "Request Lease", completed: hasLease && !isRejected },
|
||
{ label: "Landlord Review", completed: isApproved },
|
||
{ label: "Lease Active", completed: isApproved },
|
||
];
|
||
|
||
const getStepStatus = (stepIndex: number) => {
|
||
if (!hasLease) return stepIndex === 0 ? "current" : "pending";
|
||
if (isRejected) {
|
||
return stepIndex === 0
|
||
? "current"
|
||
: stepIndex === 1
|
||
? "error"
|
||
: "pending";
|
||
}
|
||
if (isPending) return stepIndex <= 1 ? "current" : "pending";
|
||
if (isApproved) return stepIndex <= 2 ? "current" : "completed";
|
||
return "pending";
|
||
};
|
||
|
||
return (
|
||
<div class="min-h-screen bg-gray-50 tenant-route">
|
||
<div class="max-w-6xl mx-auto px-6 py-10">
|
||
<header class="mb-8">
|
||
<p class="text-sm uppercase tracking-[0.4em] text-gray-400">
|
||
Tenant Invitation
|
||
</p>
|
||
<h1 class="mt-3 text-3xl font-semibold text-gray-900">
|
||
{propertyLabel}
|
||
</h1>
|
||
{propertyAddress && (
|
||
<p class="mt-2 text-sm text-gray-500">
|
||
{propertyAddress}
|
||
</p>
|
||
)}
|
||
{inviteeEmail && (
|
||
<p class="mt-4 inline-flex items-center rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">
|
||
Invite sent to {inviteeEmail}
|
||
</p>
|
||
)}
|
||
{status === "declined" && (
|
||
<div class="mt-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
|
||
Invite declined. If this was a mistake, reach out to your landlord
|
||
for a fresh link.
|
||
</div>
|
||
)}
|
||
{error && (
|
||
<div class="mt-4 rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||
{error}
|
||
</div>
|
||
)}
|
||
</header>
|
||
|
||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||
<div class="lg:col-span-2 space-y-6">
|
||
<div class="bg-white shadow rounded-lg p-4">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-lg font-semibold text-gray-900">
|
||
Property Photos
|
||
</h2>
|
||
<span class="text-xs text-gray-500">
|
||
{displayedPhotos.length} photo
|
||
{displayedPhotos.length === 1 ? "" : "s"}
|
||
</span>
|
||
</div>
|
||
{displayedPhotos.length > 0
|
||
? (
|
||
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<img
|
||
src={displayedPhotos[0]}
|
||
alt={`${propertyLabel} photo`}
|
||
class="h-64 w-full rounded-lg object-cover sm:col-span-2"
|
||
/>
|
||
{displayedPhotos.slice(1, 3).map((photo, index) => (
|
||
<img
|
||
key={`${photo}-${index}`}
|
||
src={photo}
|
||
alt={`${propertyLabel} secondary photo ${index + 1}`}
|
||
class="h-40 w-full rounded-lg object-cover"
|
||
/>
|
||
))}
|
||
</div>
|
||
)
|
||
: (
|
||
<div class="mt-4 rounded-lg border border-dashed border-gray-200 bg-gray-50 px-4 py-12 text-center text-sm text-gray-500">
|
||
No photos yet. Your landlord can add them later.
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div class="bg-white shadow rounded-lg p-6 space-y-4">
|
||
<div>
|
||
<h2 class="text-lg font-semibold text-gray-900">
|
||
Property Overview
|
||
</h2>
|
||
<p class="mt-2 text-sm text-gray-500">
|
||
{property.description || "No description has been added yet."}
|
||
</p>
|
||
</div>
|
||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<div class="rounded-lg bg-gray-50 p-4">
|
||
<p class="text-xs uppercase tracking-[0.3em] text-gray-400">
|
||
Bedrooms
|
||
</p>
|
||
<p class="mt-2 text-lg font-semibold text-gray-900">
|
||
{formatNumber(property.bedrooms)}
|
||
</p>
|
||
</div>
|
||
<div class="rounded-lg bg-gray-50 p-4">
|
||
<p class="text-xs uppercase tracking-[0.3em] text-gray-400">
|
||
Bathrooms
|
||
</p>
|
||
<p class="mt-2 text-lg font-semibold text-gray-900">
|
||
{formatNumber(property.bathrooms)}
|
||
</p>
|
||
</div>
|
||
<div class="rounded-lg bg-gray-50 p-4">
|
||
<p class="text-xs uppercase tracking-[0.3em] text-gray-400">
|
||
Square Feet
|
||
</p>
|
||
<p class="mt-2 text-lg font-semibold text-gray-900">
|
||
{formatNumber(property.square_footage)}
|
||
</p>
|
||
</div>
|
||
<div class="rounded-lg bg-gray-50 p-4">
|
||
<p class="text-xs uppercase tracking-[0.3em] text-gray-400">
|
||
Property Type
|
||
</p>
|
||
<p class="mt-2 text-lg font-semibold text-gray-900 capitalize">
|
||
{property.property_type || "N/A"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{property.amenities && (
|
||
<div class="rounded-lg border border-gray-200 p-4">
|
||
<p class="text-xs uppercase tracking-[0.3em] text-gray-400">
|
||
Amenities
|
||
</p>
|
||
<p class="mt-2 text-sm text-gray-600">
|
||
{property.amenities}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="space-y-6">
|
||
<div class="bg-white shadow rounded-lg p-6">
|
||
<h2 class="text-lg font-semibold text-gray-900">
|
||
Lease Snapshot
|
||
</h2>
|
||
<dl class="mt-4 space-y-3 text-sm text-gray-600">
|
||
<div class="flex items-center justify-between">
|
||
<dt>Monthly rent</dt>
|
||
<dd class="font-semibold text-gray-900">
|
||
{formatCurrency(property.monthly_rent)}
|
||
</dd>
|
||
</div>
|
||
<div class="flex items-center justify-between">
|
||
<dt>Security deposit</dt>
|
||
<dd class="font-semibold text-gray-900">
|
||
{formatCurrency(property.security_deposit)}
|
||
</dd>
|
||
</div>
|
||
<div class="flex items-center justify-between">
|
||
<dt>Lease length</dt>
|
||
<dd class="font-semibold text-gray-900">
|
||
{property.lease_duration_months
|
||
? `${property.lease_duration_months} months`
|
||
: "N/A"}
|
||
</dd>
|
||
</div>
|
||
{availableOn && (
|
||
<div class="flex items-center justify-between">
|
||
<dt>Available on</dt>
|
||
<dd class="font-semibold text-gray-900">
|
||
{availableOn}
|
||
</dd>
|
||
</div>
|
||
)}
|
||
<div class="flex items-center justify-between">
|
||
<dt>Status</dt>
|
||
<dd class="font-semibold text-gray-900 capitalize">
|
||
{property.rental_status || "N/A"}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
|
||
<div class="bg-white shadow rounded-lg p-6">
|
||
<h2 class="text-lg font-semibold text-gray-900">
|
||
Approval Status
|
||
</h2>
|
||
{hasLease && (
|
||
<div class="mt-4">
|
||
<div class="flex items-center space-x-2">
|
||
{approvalSteps.map((_step, index) => (
|
||
<div
|
||
key={index}
|
||
class={`flex items-center ${
|
||
index < approvalSteps.length - 1 ? "" : "hidden"
|
||
}`}
|
||
>
|
||
<div
|
||
class={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold ${
|
||
getStepStatus(index) === "current"
|
||
? "bg-blue-600 text-white"
|
||
: getStepStatus(index) === "completed"
|
||
? "bg-emerald-600 text-white"
|
||
: getStepStatus(index) === "error"
|
||
? "bg-rose-600 text-white"
|
||
: "bg-gray-200 text-gray-500"
|
||
}`}
|
||
>
|
||
{index + 1}
|
||
</div>
|
||
{index < approvalSteps.length - 1 && (
|
||
<div
|
||
class={`h-1 w-12 ${
|
||
getStepStatus(index + 1) === "completed" ||
|
||
getStepStatus(index + 1) === "current"
|
||
? "bg-emerald-600"
|
||
: "bg-gray-200"
|
||
}`}
|
||
/>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div class="mt-4">
|
||
{isPending && (
|
||
<div class="rounded-lg bg-amber-50 border border-amber-200 px-4 py-3 text-sm text-amber-700">
|
||
<div class="flex items-center">
|
||
<svg
|
||
class="h-5 w-5 mr-2"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M12 8v4l3 3m0 0l-3 3m3 3V8m0 0l-3 3m3 3V8"
|
||
/>
|
||
</svg>
|
||
<span class="font-semibold">
|
||
Pending Landlord Approval
|
||
</span>
|
||
</div>
|
||
<p class="mt-2 text-amber-600">
|
||
Your landlord has been notified and will review your
|
||
lease request. You will receive an email once a
|
||
decision is made.
|
||
</p>
|
||
<p class="mt-3 text-amber-700 font-medium">
|
||
The landlord will send you the lease documents.
|
||
</p>
|
||
<p class="mt-2 text-sm text-amber-600">
|
||
A confirmation has been added to your{" "}
|
||
<a
|
||
href="/tenant/messages"
|
||
class="underline font-medium hover:text-amber-800"
|
||
>
|
||
Messages
|
||
</a>{" "}
|
||
– the landlord was notified and a follow-up message
|
||
will contain more information.
|
||
</p>
|
||
</div>
|
||
)}
|
||
{isApproved && (
|
||
<div class="rounded-lg bg-emerald-50 border border-emerald-200 px-4 py-3 text-sm text-emerald-700">
|
||
<div class="flex items-center">
|
||
<svg
|
||
class="h-5 w-5 mr-2"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M5 13l4 4L19 7"
|
||
/>
|
||
</svg>
|
||
<span class="font-semibold">Lease Approved</span>
|
||
</div>
|
||
<p class="mt-2 text-emerald-600">
|
||
Congratulations! Your lease has been approved. You can
|
||
now access your tenant portal and view your lease
|
||
details.
|
||
</p>
|
||
</div>
|
||
)}
|
||
{isRejected && (
|
||
<div class="rounded-lg bg-rose-50 border border-rose-200 px-4 py-3 text-sm text-rose-700">
|
||
<div class="flex items-center">
|
||
<svg
|
||
class="h-5 w-5 mr-2"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M6 18L18 6M6 6l12 12"
|
||
/>
|
||
</svg>
|
||
<span class="font-semibold">Lease Rejected</span>
|
||
</div>
|
||
<p class="mt-2 text-rose-600">
|
||
Your landlord has declined this lease request. If you
|
||
have questions or would like to discuss this decision,
|
||
please contact your landlord directly.
|
||
</p>
|
||
{lease?.rejection_reason && (
|
||
<div class="mt-3 p-3 bg-white rounded border border-rose-200">
|
||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||
Reason for rejection
|
||
</p>
|
||
<p class="text-sm text-gray-700 mt-1">
|
||
{lease.rejection_reason}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
{canStartLease && (
|
||
<div class="mt-4">
|
||
<p class="text-sm text-gray-500">
|
||
Click "Start Lease" to request landlord approval for this
|
||
property.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div class="bg-white shadow rounded-lg p-6">
|
||
<h2 class="text-lg font-semibold text-gray-900">
|
||
What happens next
|
||
</h2>
|
||
<ul class="mt-4 space-y-2 text-sm text-gray-500">
|
||
{canStartLease && (
|
||
<>
|
||
<li>- Click "Start Lease" to request landlord approval.</li>
|
||
<li>- Your landlord will review your request.</li>
|
||
<li>- Track your approval status on this page.</li>
|
||
</>
|
||
)}
|
||
{isPending && (
|
||
<>
|
||
<li>- Your landlord has been notified of your request.</li>
|
||
<li>- The landlord will send you the lease documents.</li>
|
||
<li>- Wait for landlord to review and approve/reject.</li>
|
||
<li>
|
||
- You will receive an email notification with a decision.
|
||
</li>
|
||
<li>
|
||
- Check your Messages for a confirmation and follow-up
|
||
details.
|
||
</li>
|
||
</>
|
||
)}
|
||
{isApproved && (
|
||
<>
|
||
<li>- Your lease has been approved and activated.</li>
|
||
<li>- Access your tenant portal to view lease details.</li>
|
||
<li>- Set up payment methods and start paying rent.</li>
|
||
</>
|
||
)}
|
||
{isRejected && (
|
||
<>
|
||
<li>- Your lease request was declined.</li>
|
||
<li>- Contact your landlord if you have questions.</li>
|
||
{canStartLease
|
||
? (
|
||
<li>
|
||
- You can submit a new request using the Start Lease
|
||
button above.
|
||
</li>
|
||
)
|
||
: (
|
||
<li>
|
||
- You can request a new invite from your landlord.
|
||
</li>
|
||
)}
|
||
</>
|
||
)}
|
||
</ul>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<div class="mt-8 bg-white shadow rounded-lg p-6">
|
||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||
<div>
|
||
<h2 class="text-lg font-semibold text-gray-900">
|
||
Ready to proceed?
|
||
</h2>
|
||
<p class="text-sm text-gray-500">
|
||
{canStartLease &&
|
||
"Start your lease request, decline the invite, or come back later."}
|
||
{isPending &&
|
||
"Your request is pending landlord review. Check back here for updates."}
|
||
{isApproved &&
|
||
"Your lease is approved! Continue to your tenant portal."}
|
||
{isRejected && !canStartLease &&
|
||
"Your lease request was declined. Contact your landlord for more information."}
|
||
</p>
|
||
</div>
|
||
<div class="flex flex-wrap gap-3">
|
||
{canStartLease && isLoggedIn && (
|
||
<form method="POST">
|
||
<input type="hidden" name="intent" value="start" />
|
||
<button
|
||
type="submit"
|
||
class="inline-flex items-center justify-center rounded-full bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700"
|
||
>
|
||
Start Lease
|
||
</button>
|
||
</form>
|
||
)}
|
||
{isApproved && isLoggedIn && (
|
||
<a
|
||
href="/tenant/dashboard"
|
||
class="inline-flex items-center justify-center rounded-full bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700"
|
||
>
|
||
Continue to Tenant Portal
|
||
</a>
|
||
)}
|
||
{!isLoggedIn && (
|
||
<LoginPurposeButton
|
||
purpose="tenant"
|
||
lockRoute={`/tenant/invite/${encodeURIComponent(invite.id)}`}
|
||
className="inline-flex items-center justify-center rounded-full bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700"
|
||
>
|
||
Sign In to Start Lease
|
||
</LoginPurposeButton>
|
||
)}
|
||
|
||
<form method="POST" class="flex items-center gap-2">
|
||
<input type="hidden" name="intent" value="decline" />
|
||
{!inviteeEmail && (
|
||
<input
|
||
type="email"
|
||
name="invitee_email"
|
||
aria-label="Invitee email"
|
||
placeholder="your@email.com"
|
||
class="w-48 rounded-full border border-gray-300 px-3 py-2 text-xs text-gray-700 focus:border-gray-400 focus:outline-none"
|
||
/>
|
||
)}
|
||
<button
|
||
type="submit"
|
||
class="inline-flex items-center justify-center rounded-full border border-rose-200 px-5 py-2 text-sm font-semibold text-rose-600 hover:border-rose-300 hover:text-rose-700"
|
||
>
|
||
Decline
|
||
</button>
|
||
</form>
|
||
|
||
<a
|
||
href={cancelHref}
|
||
class="inline-flex items-center justify-center rounded-full border border-gray-200 px-5 py-2 text-sm font-semibold text-gray-600 hover:border-gray-300 hover:text-gray-800"
|
||
>
|
||
Cancel
|
||
</a>
|
||
</div>
|
||
</div>
|
||
{!isLoggedIn && (
|
||
<p class="mt-3 text-xs text-gray-400">
|
||
Sign in to save your decision and view the full tenant portal.
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
});
|