Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions frontend/app/dashboard/invoices/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
import { ArrowLeft, Download } from 'lucide-react';
import Link from 'next/link';
import { Skeleton } from '@/components/ui/skeleton';
import { PageBreadcrumb } from '@/components/layout/PageBreadcrumb';
import {
formatDateInTimeZone,
formatDateTimeInTimeZone,
formatTimeInTimeZone,
} from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';

export default function InvoiceDetailPage() {
const params = useParams();
const rawId = params.id as string;
const projectId = rawId.startsWith('INV-') ? rawId.replace('INV-', '') : rawId;
const timezone = useAuthStore((state) => state.timezone);

const { useProjectDetail } = useAgenticPay();
const { project, loading } = useProjectDetail(projectId);
Expand Down Expand Up @@ -53,7 +59,7 @@

return (
<div className="space-y-6 invoice-print-page">
<PageBreadcrumb

Check failure on line 62 in frontend/app/dashboard/invoices/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Node.js 22)

'PageBreadcrumb' is not defined
items={[
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Invoices', href: '/dashboard/invoices' },
Expand Down Expand Up @@ -95,9 +101,9 @@
Generated
</p>
<p className="mt-2 font-medium text-slate-900">
{generatedAt.toLocaleDateString()}
{formatDateInTimeZone(generatedAt, timezone)}
</p>
<p className="text-xs text-slate-500">{generatedAt.toLocaleTimeString()}</p>
<p className="text-xs text-slate-500">{formatTimeInTimeZone(generatedAt, timezone)}</p>
</div>
<div className="print-break-inside-avoid rounded-xl border border-slate-200 bg-white p-4">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
Expand Down Expand Up @@ -166,7 +172,7 @@
<div className="flex items-center justify-between gap-4 px-5 py-4 text-sm">
<span className="text-slate-600">Generated</span>
<span className="text-right font-medium text-slate-900">
{generatedAt.toLocaleString()}
{formatDateTimeInTimeZone(generatedAt, timezone)}
</span>
</div>
<div className="flex items-center justify-between gap-4 px-5 py-4 text-sm">
Expand Down
48 changes: 46 additions & 2 deletions frontend/app/dashboard/invoices/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { motion } from 'framer-motion';
import Link from 'next/link';
import { InvoiceCardSkeleton } from '@/components/ui/loading-skeletons';
import { EmptyState } from '@/components/empty/EmptyState';
import { useRouter } from 'next/navigation';
import { formatDateInTimeZone } from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';

export default function InvoicesPage() {
const router = useRouter();
const { invoices, loading } = useDashboardData();
const timezone = useAuthStore((state) => state.timezone);
const [filter, setFilter] = useState<'all' | 'paid' | 'pending' | 'overdue'>('all');

const filteredInvoices =
Expand Down Expand Up @@ -89,7 +91,49 @@ export default function InvoicesPage() {
</div>
</div>

{filteredInvoices.length === 0 ? (
<div className="grid grid-cols-1 gap-4">
{filteredInvoices.map((invoice, index) => (
<motion.div
key={invoice.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Link href={`/dashboard/projects/${invoice.projectId}`}>
<Card className="hover:shadow-lg transition-all cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
{getStatusIcon(invoice.status)}
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{invoice.projectTitle}</h3>
<p className="text-sm text-gray-600">{invoice.milestoneTitle}</p>
<p className="text-xs text-gray-500 mt-1">
Ref #{invoice.id} • {formatDateInTimeZone(invoice.generatedAt, timezone)}
</p>
</div>
</div>
<div className="text-right">
<p className="text-xl font-bold text-gray-900">
{invoice.amount} {invoice.currency}
</p>
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-medium border mt-2 ${getStatusColor(
invoice.status
)}`}
>
{invoice.status}
</span>
</div>
</div>
</CardContent>
</Card>
</Link>
</motion.div>
))}
</div>

{filteredInvoices.length === 0 && (
<Card>
<CardContent>
<EmptyState
Expand Down
64 changes: 58 additions & 6 deletions frontend/app/dashboard/payments/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ import {
import { motion } from 'framer-motion';
import { PaymentCardSkeleton } from '@/components/ui/loading-skeletons';
import { EmptyState } from '@/components/empty/EmptyState';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { PaymentQRModal } from '@/components/payment/QRCode';
import { formatDateTimeInTimeZone } from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';

export default function PaymentsPage() {
const router = useRouter();
const { payments, loading } = useDashboardData();
const { address } = useAuthStore();
const [isQrModalOpen, setIsQrModalOpen] = useState(false);
const timezone = useAuthStore((state) => state.timezone);

const getStatusIcon = (status: string) => {
switch (status) {
Expand Down Expand Up @@ -75,7 +73,61 @@ export default function PaymentsPage() {
)}
</div>

{payments.length === 0 ? (
{/* --- PAYMENT LIST --- */}
<div className="space-y-4">
{payments.map((payment, index) => (
<motion.div
key={payment.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Card className="hover:shadow-lg transition-all">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
{getStatusIcon(payment.status)}
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{payment.projectTitle}</h3>
<p className="text-sm text-gray-600">
{payment.type === 'milestone_payment' ? 'Milestone Payment' : 'Full Payment'}
</p>
<p className="text-xs text-gray-500 mt-1">
{formatDateTimeInTimeZone(payment.timestamp, timezone)}
</p>
</div>
</div>
<div className="text-right">
<p className="text-xl font-bold text-gray-900">
{payment.amount} {payment.currency}
</p>
{payment.transactionHash && (
<a
href={`https://testnet.cronoscan.com/tx/${payment.transactionHash}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-blue-600 hover:underline mt-2 justify-end"
>
View on Explorer
<ExternalLink className="h-3 w-3" />
</a>
)}
</div>
</div>
{payment.transactionHash && (
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-gray-500 font-mono break-all">
{payment.transactionHash}
</p>
</div>
)}
</CardContent>
</Card>
</motion.div>
))}
</div>

{payments.length === 0 && (
<Card>
<CardContent>
<EmptyState
Expand Down
24 changes: 19 additions & 5 deletions frontend/app/dashboard/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
import { useAccount } from 'wagmi';
import { toast } from 'sonner';
import { api } from '@/lib/api';
import { PageBreadcrumb } from '@/components/layout/PageBreadcrumb';
import { OfflineActionQueuedError } from '@/lib/offline';
import { formatDateInTimeZone } from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';

export default function ProjectDetailPage() {
const params = useParams();
const projectId = params.id as string;
const { address } = useAccount();
const timezone = useAuthStore((state) => state.timezone);

const { useProjectDetail, fundProject, submitWork, approveWork, isPending, isConfirming, isConfirmed, error, arbitrator } = useAgenticPay();
const { project, loading, refetch } = useProjectDetail(projectId);
Expand Down Expand Up @@ -92,7 +95,7 @@

return (
<div className="space-y-6">
<PageBreadcrumb

Check failure on line 98 in frontend/app/dashboard/projects/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Node.js 22)

'PageBreadcrumb' is not defined
items={[
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Projects', href: '/dashboard/projects' },
Expand Down Expand Up @@ -141,7 +144,7 @@
<div>
<p className="text-sm text-gray-600">Created</p>
<p className="text-lg font-medium">
{new Date(project.createdAt).toLocaleDateString()}
{formatDateInTimeZone(project.createdAt, timezone)}
</p>
</div>
</div>
Expand Down Expand Up @@ -213,13 +216,21 @@
toast.success("Invoice Generated");
refetch();
} catch (invError) {
toast.error("Invoice error: " + (invError as Error).message);
if (invError instanceof OfflineActionQueuedError) {
toast.info(invError.message);
} else {
toast.error("Invoice error: " + (invError as Error).message);
}
}
} else {
toast.error("Verification failed: " + verification.summary);
}
} catch (e) {
toast.error((e as Error).message);
if (e instanceof OfflineActionQueuedError) {
toast.info(e.message);
} else {
toast.error((e as Error).message);
}
}
}}>
Request AI Verification
Expand All @@ -240,6 +251,9 @@
<div className="flex gap-2">
<Button onClick={async () => {
try {
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
throw new Error('You are offline. Reconnect before submitting an on-chain transaction.');
}
if (!repoLink) throw new Error("No repo link");
toast.info('Submitting work to blockchain...');
await submitWork(project.id, repoLink);
Expand Down Expand Up @@ -290,7 +304,7 @@
</p>
{milestone.dueDate && (
<p className="text-xs text-gray-500">
Due: {new Date(milestone.dueDate).toLocaleDateString()}
Due: {formatDateInTimeZone(milestone.dueDate, timezone)}
</p>
)}
</div>
Expand Down
76 changes: 69 additions & 7 deletions frontend/app/dashboard/projects/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,64 @@ import { toast } from 'sonner';
import { useAgenticPay } from '@/lib/hooks/useAgenticPay';
import { useAccount } from 'wagmi';

const walletAddressSchema = z
.string()
.trim()
.regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address');

const tokenAddressSchema = z
.string()
.trim()
.regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address');

const isFutureDate = (value: string) => {
const selectedDate = new Date(`${value}T00:00:00`);

if (Number.isNaN(selectedDate.getTime())) {
return false;
}

const today = new Date();
today.setHours(0, 0, 0, 0);

return selectedDate > today;
};

const getMinDeadlineDate = () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

return tomorrow.toISOString().split('T')[0];
};

const projectSchema = z.object({
title: z.string().min(1, 'Title is required'),
clientAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address').optional(), // Optional if user is client
freelancerAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid wallet address'),
totalAmount: z.string().min(1, 'Amount is required'),
title: z.string().trim().min(1, 'Title is required'),
clientAddress: walletAddressSchema.optional(), // Optional if user is client
freelancerAddress: walletAddressSchema,
totalAmount: z
.string()
.trim()
.min(1, 'Amount is required')
.refine((value) => {
const amount = Number(value);
return Number.isFinite(amount) && amount > 0;
}, 'Amount must be a positive number'),
currency: z.string().min(1, 'Currency is required'),
tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address').optional(),
deadline: z.string().min(1, 'Deadline is required'),
tokenAddress: z.union([tokenAddressSchema, z.literal('')]).optional(),
deadline: z
.string()
.min(1, 'Deadline is required')
.refine(isFutureDate, 'Deadline must be a future date'),
githubRepo: z.string().url('Invalid URL').optional().or(z.literal('')),
description: z.string().optional(),
}).superRefine((data, ctx) => {
if (data.currency === 'ERC20' && !data.tokenAddress?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['tokenAddress'],
message: 'Token address is required for ERC20 payments',
});
}
});

type ProjectFormData = z.infer<typeof projectSchema>;
Expand Down Expand Up @@ -78,6 +126,11 @@ export default function CreateProjectPage() {
return;
}

if (typeof navigator !== 'undefined' && navigator.onLine === false) {
toast.error('You are offline. Reconnect before creating an on-chain project.');
return;
}

try {
const paymentType = data.currency === 'ETH' ? 0 : 1;
const tokenAddr = data.currency === 'ETH' ? '0x0000000000000000000000000000000000000000' : data.tokenAddress!;
Expand Down Expand Up @@ -175,7 +228,15 @@ export default function CreateProjectPage() {
</div>
<div>
<Label htmlFor="currency">Currency</Label>
<Select onValueChange={(val) => setValue('currency', val)} defaultValue="ETH">
<Select
onValueChange={(val) =>
setValue('currency', val, {
shouldDirty: true,
shouldValidate: true,
})
}
defaultValue="ETH"
>
<SelectTrigger>
<SelectValue placeholder="Select Currency" />
</SelectTrigger>
Expand Down Expand Up @@ -209,6 +270,7 @@ export default function CreateProjectPage() {
<Input
id="deadline"
type="date"
min={getMinDeadlineDate()}
{...register('deadline')}
/>
{errors.deadline && (
Expand Down
5 changes: 4 additions & 1 deletion frontend/app/dashboard/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import { EmptyState } from '@/components/empty/EmptyState';
import { useRouter } from 'next/navigation';
import { useAgenticPay } from '@/lib/hooks/useAgenticPay';
import { useAccount } from 'wagmi';
import { formatDateInTimeZone } from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';

export default function ProjectsPage() {
const router = useRouter();
const { isConnected } = useAccount();
const { useUserProjects } = useAgenticPay();
const { projects, loading } = useUserProjects();
const timezone = useAuthStore((state) => state.timezone);

if (loading) {
return (
Expand Down Expand Up @@ -138,7 +141,7 @@ export default function ProjectsPage() {

<div className="flex items-center gap-2 text-xs text-gray-500">
<Clock className="h-3 w-3" />
<span>Created {new Date(project.createdAt).toLocaleDateString()}</span>
<span>Created {formatDateInTimeZone(project.createdAt, timezone)}</span>
</div>

<Link href={`/dashboard/projects/${project.id}`}>
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "AgenticPay - Get Paid Instantly for Your Work",
description: "Secure, fast, and transparent payments for freelancers powered by blockchain technology.",
// manifest: "/manifest.webmanifest",
manifest: "/manifest.webmanifest",
keywords: ["freelancer", "payments", "blockchain", "crypto", "web3", "escrow", "milestones"],
authors: [{ name: "AgenticPay" }],
openGraph: {
Expand Down
Loading
Loading