miranshala 7 months ago
parent d1bc2139b4
commit 63a753403a

@ -8,15 +8,32 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
try {
const pageParam = request.nextUrl.searchParams.get("page");
const pageSizeParam = request.nextUrl.searchParams.get("pageSize");
const page = Math.max(1, Number(pageParam || 1));
const pageSize = Math.max(1, Math.min(50, Number(pageSizeParam || 5)));
const offset = (page - 1) * pageSize;
const pool = getPool();
const countRes = await pool.query(
`SELECT COUNT(*)::int AS count FROM uploaded_files WHERE user_id = $1`,
[userId]
);
const total = countRes.rows[0]?.count || 0;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const result = await pool.query(
`SELECT id, original_name, file_path, upload_status, created_at
FROM uploaded_files
WHERE user_id = $1
ORDER BY created_at DESC`,
[userId]
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`,
[userId, pageSize, offset]
);
return NextResponse.json({ files: result.rows });
return NextResponse.json({
files: result.rows,
pagination: { page, pageSize, total, totalPages },
});
} catch (err: any) {
return NextResponse.json(
{ error: err.message || "Server error" },

@ -0,0 +1,72 @@
"use client";
import Link from "next/link";
import ContactForm from "@/components/ContactForm";
export default function ContactPage() {
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-12 text-center">
<Link
href="/"
className="inline-flex items-center text-[#FF7518] hover:text-[#ff4f00] mb-6 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2"
>
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>
Back To Home
</Link>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
Contact Us
</h1>
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
We look forward to hearing from you and helping you find the perfect
cloud solution for your needs. Our team of experts is ready to
assist you with any questions or concerns you may have.
</p>
</div>
{/* Contact Form Component */}
<div className="mb-12">
<ContactForm />
</div>
{/* Call to Action */}
<div className="text-center bg-white rounded-xl border border-[#FF7518] shadow p-8">
<p className="text-lg text-gray-700 mb-4">
Looking to start a project with us?
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link
href="/signup"
className="px-6 py-3 bg-[#FF7518] text-white rounded-md font-semibold hover:bg-[#ff4f00] transition-colors"
>
Create an account
</Link>
<span className="text-gray-500">or</span>
<Link
href="/"
className="px-6 py-3 border border-[#FF7518] text-[#FF7518] rounded-md font-semibold hover:bg-[#FF7518] hover:text-white transition-colors"
>
Schedule a consultation
</Link>
</div>
</div>
</div>
</div>
);
}

@ -166,8 +166,15 @@ export default function Home() {
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
href="/pricing"
className="px-4 py-2 text-gray-700 hover:text-gray-900"
href="#pricing"
onClick={(e) => {
e.preventDefault();
const pricingSection = document.getElementById("pricing");
if (pricingSection) {
pricingSection.scrollIntoView({ behavior: "smooth" });
}
}}
className="px-4 py-2 text-gray-700 hover:text-gray-900 cursor-pointer"
>
Pricing
</NavigationMenuLink>
@ -270,12 +277,18 @@ export default function Home() {
power so you can focus on the creative side of your work.
</p>
<div className="flex gap-4 mt-6">
<button className="bg-white text-orange-500 px-6 py-3 rounded-md font-medium hover:bg-gray-100 cursor-pointer transition-colors">
<a
href="/contact"
className="bg-white text-orange-500 px-6 py-3 rounded-md font-medium hover:bg-gray-100 cursor-pointer transition-colors"
>
Contact Us
</button>
<button className="bg-transparent border-2 border-white text-white px-6 py-3 rounded-md font-medium hover:bg-white hover:text-orange-500 cursor-pointer transition-colors">
</a>
<a
href="/signup"
className="bg-transparent border-2 border-white text-white px-6 py-3 rounded-md font-medium hover:bg-white hover:text-orange-500 cursor-pointer transition-colors"
>
Get Started
</button>
</a>
</div>
</div>
<div className="absolute bottom-4 right-4">
@ -318,7 +331,7 @@ export default function Home() {
)}
{/* Pricing Section */}
<div className="flex justify-center mt-16 mb-16">
<div id="pricing" className="flex justify-center mt-16 mb-16">
<div className="w-[90%]">
<h2 className="text-4xl font-bold text-center mb-12 text-gray-800">
Available Plans

@ -3,6 +3,14 @@
import { useEffect, useState } from "react";
import { useAuth } from "@/lib/auth-context";
import { useRouter } from "next/navigation";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationPrevious,
PaginationNext,
} from "@/components/ui/pagination";
interface UploadedFile {
id: string;
@ -19,6 +27,9 @@ export default function RendersPage() {
const [loading, setLoading] = useState(true);
const [authInitialized, setAuthInitialized] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [page, setPage] = useState(1);
const pageSize = 5;
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
const timer = setTimeout(() => setAuthInitialized(true), 100);
@ -31,19 +42,25 @@ export default function RendersPage() {
router.push("/login?redirect=/renders");
return;
}
fetchFiles();
fetchFiles(page);
// eslint-disable-next-line
}, [authInitialized, isAuthenticated, user]);
}, [authInitialized, isAuthenticated, user, page]);
async function fetchFiles() {
async function fetchFiles(pageNum: number) {
setLoading(true);
try {
const res = await fetch("/api/renders/in", {
headers: { "user-id": user?.id },
});
const res = await fetch(
`/api/renders/in?page=${pageNum}&pageSize=${pageSize}`,
{
headers: { "user-id": user?.id },
}
);
const data = await res.json();
setFiles(data.files || []);
setTotalPages(data.pagination?.totalPages || 1);
} catch {
setFiles([]);
setTotalPages(1);
} finally {
setLoading(false);
}
@ -65,7 +82,7 @@ export default function RendersPage() {
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Delete failed");
await fetchFiles();
await fetchFiles(page);
} catch (e) {
alert((e as any).message || "Delete failed");
} finally {
@ -81,6 +98,9 @@ export default function RendersPage() {
);
}
const canPrev = page > 1;
const canNext = page < totalPages;
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-5xl mx-auto px-4">
@ -95,62 +115,112 @@ export default function RendersPage() {
) : files.length === 0 ? (
<div className="text-gray-500">No files uploaded yet.</div>
) : (
<table className="w-full bg-white rounded-lg shadow-md">
<thead>
<tr className="bg-orange-50">
<th className="p-3 text-left font-semibold">Filename</th>
<th className="p-3 text-left font-semibold">Status</th>
<th className="p-3 text-left font-semibold">Uploaded At</th>
<th className="p-3 text-left font-semibold">Download</th>
<th className="p-3 text-left font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{files.map((f) => (
<tr key={f.id} className="border-t">
<td className="p-3">{f.original_name}</td>
<td className="p-3 capitalize">
<span
className={`px-2 py-1 rounded-full text-xs font-bold ${
f.upload_status === "COMPLETE"
? "bg-green-100 text-green-800"
: "bg-gray-200 text-gray-600"
}`}
>
{f.upload_status}
</span>
</td>
<td className="p-3">
{new Date(f.created_at).toLocaleString()}
</td>
<td className="p-3">
<a
href={
f.file_path
? `/uploads/${f.file_path.split("/").pop()}`
: "#"
}
className="underline text-blue-700"
target="_blank"
rel="noopener noreferrer"
download={f.original_name}
>
Download
</a>
</td>
<td className="p-3">
<button
className="px-3 py-1 text-sm rounded-md bg-red-500 text-white hover:bg-red-600 disabled:bg-red-300"
disabled={deletingId === f.id}
onClick={() => handleDelete(f.id)}
>
{deletingId === f.id ? "Deleting..." : "Delete"}
</button>
</td>
<>
<table className="w-full bg-white rounded-lg shadow-md">
<thead>
<tr className="bg-orange-50">
<th className="p-3 text-left font-semibold">Filename</th>
<th className="p-3 text-left font-semibold">Status</th>
<th className="p-3 text-left font-semibold">Uploaded At</th>
<th className="p-3 text-left font-semibold">Download</th>
<th className="p-3 text-left font-semibold">Actions</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{files.map((f) => (
<tr key={f.id} className="border-t">
<td className="p-3">{f.original_name}</td>
<td className="p-3 capitalize">
<span
className={`px-2 py-1 rounded-full text-xs font-bold ${
f.upload_status === "COMPLETE"
? "bg-green-100 text-green-800"
: "bg-gray-200 text-gray-600"
}`}
>
{f.upload_status}
</span>
</td>
<td className="p-3">
{new Date(f.created_at).toLocaleString()}
</td>
<td className="p-3">
<a
href={
f.file_path
? `/uploads/${f.file_path.split("/").pop()}`
: "#"
}
className="underline text-blue-700"
target="_blank"
rel="noopener noreferrer"
download={f.original_name}
>
Download
</a>
</td>
<td className="p-3">
<button
className="px-3 py-1 text-sm rounded-md bg-red-500 text-white hover:bg-red-600 disabled:bg-red-300"
disabled={deletingId === f.id}
onClick={() => handleDelete(f.id)}
>
{deletingId === f.id ? "Deleting..." : "Delete"}
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="mt-4 flex justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (canPrev) setPage(page - 1);
}}
aria-disabled={!canPrev}
className={
!canPrev ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
(p) => (
<PaginationItem key={p}>
<PaginationLink
href="#"
isActive={p === page}
onClick={(e) => {
e.preventDefault();
setPage(p);
}}
>
{p}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (canNext) setPage(page + 1);
}}
aria-disabled={!canNext}
className={
!canNext ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</>
)}
</section>

@ -0,0 +1,115 @@
"use client";
import React from "react";
export default function ContactForm() {
return (
<div className="grid gap-6 md:grid-cols-2">
{/* Get in Touch Card */}
<div className="rounded-xl border border-[#FF7518] shadow bg-white text-black">
<div className="p-6">
<h2 className="text-2xl font-semibold mb-6">Get in Touch</h2>
<div className="space-y-6">
<div className="flex items-start space-x-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-6 h-6 text-[#ff4f00] mt-1"
>
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
</svg>
<div>
<p className="font-medium">Phone Support</p>
<p className="text-gray-400">+1 (555) 123-4567</p>
<p className="text-sm text-gray-500 mt-1">
Available 24/7 for urgent matters
</p>
</div>
</div>
<div className="flex items-start space-x-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-6 h-6 text-[#ff4f00] mt-1"
>
<rect width="20" height="16" x="2" y="4" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
<div className="space-y-4">
<div>
<p className="font-medium">Sales Inquiries</p>
<a
href="mailto:sales@reya.cloud"
className="text-[#ff4f00] hover:underline"
>
sales@reya.cloud
</a>
</div>
<div>
<p className="font-medium">Technical Support</p>
<a
href="mailto:support@reya.cloud"
className="text-[#ff4f00] hover:underline"
>
support@reya.cloud
</a>
</div>
<div>
<p className="font-medium">Report Abuse</p>
<a
href="mailto:abuse@reya.cloud"
className="text-[#ff4f00] hover:underline"
>
abuse@reya.cloud
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Business Hours Card */}
<div className="rounded-xl border border-[#FF7518] shadow bg-white text-black">
<div className="p-6">
<h2 className="text-2xl font-semibold mb-6">Business Hours</h2>
<div className="space-y-4">
<div>
<p className="font-medium">Sales Department</p>
<p className="text-gray-400">
Monday - Friday: 9:00 AM - 6:00 PM EST
</p>
</div>
<div>
<p className="font-medium">Technical Support</p>
<p className="text-gray-400">24/7/365</p>
<p className="text-sm text-gray-500 mt-1">
Priority support for enterprise customers
</p>
</div>
<div className="pt-4">
<p className="text-gray-400">
For urgent matters outside of business hours, please use our
24/7 phone support or submit a ticket through your customer
portal.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Loading…
Cancel
Save