Skip to content
2 changes: 1 addition & 1 deletion api/upcoming-projects.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function validateUpcomingProject($data, $isUpdate = false) {
}

// Validate status
$validStatuses = ['Upcoming', 'Under Development', 'Planning', 'Cancelled'];
$validStatuses = ['Upcoming', 'Under Development', 'Planning', 'Cancelled', 'Completed'];
if (!in_array($data['status'], $validStatuses)) {
return "Invalid status. Must be one of: " . implode(', ', $validStatuses);
}
Expand Down
2 changes: 1 addition & 1 deletion deploy/api/upcoming-projects.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function validateUpcomingProject($data, $isUpdate = false) {
}

// Validate status
$validStatuses = ['Upcoming', 'Under Development', 'Planning', 'Cancelled'];
$validStatuses = ['Upcoming', 'Under Development', 'Planning', 'Cancelled', 'Completed'];
if (!in_array($data['status'], $validStatuses)) {
return "Invalid status. Must be one of: " . implode(', ', $validStatuses);
}
Expand Down
8 changes: 4 additions & 4 deletions deploy/src-data/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@
"type": "Tours & Travel",
"techStack": "WordPress",
"handledBy": "Dibin",
"renewalDate": "Expired",
"status": "Awaiting Renewal"
"renewalDate": "Friday, September 18, 2026",
"status": "Active"
},
{
"name": "Andre Vaillancourt",
Expand Down Expand Up @@ -324,8 +324,8 @@
},
{
"name": "leaftravelsandtour.com",
"renewalDate": "Expired",
"status": "Awaiting Renewal"
"renewalDate": "Friday, September 18, 2026",
"status": "Active"
},
{
"name": "andrevaillancourt.com",
Expand Down
14 changes: 10 additions & 4 deletions src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ interface ProtectedRouteProps {
}

export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated } = useApp();
const { isAuthenticated, loading } = useApp();
const location = useLocation();

if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}

if (!isAuthenticated) {
// Redirect to login page with the attempted location
return <Navigate to="/" state={{ from: location }} replace />;
}

return <>{children}</>;
}

}
138 changes: 60 additions & 78 deletions src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useApp } from '../../contexts/AppContext';
import { useNavigate } from 'react-router-dom';
import { Search, Moon, Sun, LogOut, User, Bell, AlertTriangle, Clock, X } from 'lucide-react';
import { Input } from '../ui/input';
import { LogOut, User, Bell, AlertTriangle, Clock, X } from 'lucide-react';
import { Button } from '../ui/button';
import {
DropdownMenu,
Expand All @@ -13,21 +12,28 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import { Badge } from '../ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { getRenewalCount, getExpiredItems, getDaysUntil, projects, domains } from '../../utils/dataLoader';

export default function Navbar() {
const { user, logout, darkMode, toggleDarkMode, searchQuery, setSearchQuery } = useApp();
const { user, logout } = useApp();
const navigate = useNavigate();
const [showSearch, setShowSearch] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);

const renewalCount = getRenewalCount();
const expiredItems = getExpiredItems();
const totalAlerts = expiredItems.length > 0 ? expiredItems.length : renewalCount;

// Get urgent renewals (within 7 days)
const getUrgentRenewals = () => {
const urgentItems: Array<{
name: string;
Expand Down Expand Up @@ -72,74 +78,29 @@ export default function Navbar() {

const urgentRenewals = getUrgentRenewals();

const handleLogout = () => {
const handleLogoutConfirm = () => {
logout();
navigate('/');
setShowLogoutDialog(false);
};

const handleLogoutCancel = () => {
setShowLogoutDialog(false);
};

return (
<motion.nav
initial={{ y: -80 }}
animate={{ y: 0 }}
transition={{ duration: 0.3 }}
className="fixed top-0 right-0 left-64 h-16 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 z-30 flex items-center justify-between px-6"
className="fixed top-0 right-0 left-64 h-16 bg-white/80 backdrop-blur-md border-b border-slate-200 z-30 flex items-center justify-end px-6"
>
<div className="flex items-center gap-4 flex-1">
<motion.div
className="relative flex-1 max-w-xl"
initial={false}
animate={{ width: showSearch ? '100%' : 'auto' }}
>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
type="text"
placeholder="Search projects, clients, domains, servers..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setShowSearch(true)}
onBlur={() => setShowSearch(false)}
className="pl-10 h-10 bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700"
/>
</motion.div>
</div>

<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={toggleDarkMode}
className="relative w-10 h-10 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
>
<AnimatePresence mode="wait">
{darkMode ? (
<motion.div
key="sun"
initial={{ rotate: -90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: 90, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Sun className="w-5 h-5" />
</motion.div>
) : (
<motion.div
key="moon"
initial={{ rotate: 90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: -90, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<Moon className="w-5 h-5" />
</motion.div>
)}
</AnimatePresence>
</Button>

<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={() => setShowNotifications(!showNotifications)}
className="relative w-10 h-10 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
className="relative w-10 h-10 rounded-lg hover:bg-slate-100"
>
<Bell className="w-5 h-5" />
{totalAlerts > 0 && (
Expand All @@ -153,7 +114,7 @@ export default function Navbar() {
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex items-center gap-2 h-10 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
className="flex items-center gap-2 h-10 px-3 rounded-lg hover:bg-slate-100"
>
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center">
<User className="w-4 h-4 text-white" />
Expand All @@ -169,23 +130,25 @@ export default function Navbar() {
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} className="text-red-600 cursor-pointer">
<DropdownMenuItem
onClick={() => setShowLogoutDialog(true)}
className="text-red-600 cursor-pointer"
>
<LogOut className="w-4 h-4 mr-2" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

{/* Notification Drawer */}
<AnimatePresence>
{showNotifications && (
<motion.div
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.2 }}
className="fixed top-16 right-6 w-96 max-h-96 overflow-y-auto bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg shadow-xl z-40"
className="fixed top-16 right-6 w-96 max-h-96 overflow-y-auto bg-white border border-slate-200 rounded-lg shadow-xl z-40"
>
<Card className="border-0">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
Expand All @@ -202,18 +165,18 @@ export default function Navbar() {
<CardContent className="space-y-3">
{expiredItems.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
<div className="flex items-center gap-2 text-red-600">
<AlertTriangle className="w-4 h-4" />
<span className="font-medium">Expired Items</span>
</div>
{expiredItems.map((item, index) => (
<div key={index} className="p-3 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg">
<div key={index} className="p-3 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start justify-between">
<div>
<p className="font-medium text-red-800 dark:text-red-200">{item.name}</p>
<p className="text-sm text-red-600 dark:text-red-400">{item.type}</p>
<p className="font-medium text-red-800">{item.name}</p>
<p className="text-sm text-red-600">{item.type}</p>
</div>
<Badge className="bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300">
<Badge className="bg-red-100 text-red-700">
Expired
</Badge>
</div>
Expand All @@ -224,19 +187,19 @@ export default function Navbar() {

{urgentRenewals.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
<div className="flex items-center gap-2 text-orange-600">
<Clock className="w-4 h-4" />
<span className="font-medium">Urgent Renewals</span>
</div>
{urgentRenewals.map((item, index) => (
<div key={index} className="p-3 bg-orange-50 dark:bg-orange-950/20 border border-orange-200 dark:border-orange-800 rounded-lg">
<div key={index} className="p-3 bg-orange-50 border border-orange-200 rounded-lg">
<div className="flex items-start justify-between">
<div>
<p className="font-medium text-orange-800 dark:text-orange-200">{item.name}</p>
<p className="text-sm text-orange-600 dark:text-orange-400">{item.type}</p>
<p className="text-xs text-orange-500 dark:text-orange-500">{item.renewalDate}</p>
<p className="font-medium text-orange-800">{item.name}</p>
<p className="text-sm text-orange-600">{item.type}</p>
<p className="text-xs text-orange-500">{item.renewalDate}</p>
</div>
<Badge className="bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300">
<Badge className="bg-orange-100 text-orange-700">
{item.daysUntil} days
</Badge>
</div>
Expand All @@ -247,16 +210,35 @@ export default function Navbar() {

{expiredItems.length === 0 && urgentRenewals.length === 0 && (
<div className="text-center py-8">
<Bell className="w-12 h-12 mx-auto text-slate-300 dark:text-slate-600 mb-3" />
<p className="text-slate-500 dark:text-slate-400">No notifications</p>
<p className="text-sm text-slate-400 dark:text-slate-500">All renewals are up to date</p>
<Bell className="w-12 h-12 mx-auto text-slate-300 mb-3" />
<p className="text-slate-500">No notifications</p>
<p className="text-sm text-slate-400">All renewals are up to date</p>
</div>
)}
</CardContent>
</Card>
</motion.div>
)}
</AnimatePresence>

<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Logout</DialogTitle>
<DialogDescription>
Are you sure you want to log out? You'll need to sign in again to access your account.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleLogoutCancel}>
Cancel
</Button>
<Button variant="destructive" onClick={handleLogoutConfirm}>
Log Out
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</motion.nav>
);
}
}
17 changes: 14 additions & 3 deletions src/components/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { Cross2Icon } from '@radix-ui/react-icons';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva, type VariantProps } from 'class-variance-authority';
import { Loader2 } from 'lucide-react'; // Assuming you have lucide-react for spinner; install if needed

import { cn } from '../../lib/utils';

Expand All @@ -14,7 +15,7 @@ const ToastViewport = React.forwardRef<
<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]',
'fixed top-0 z-[100] flex max-h-screen w-full flex-col p-4 sm:top-0 sm:right-0 sm:bottom-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
Expand All @@ -23,13 +24,14 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;

const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 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',
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 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',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
loading: 'border bg-background text-foreground', // Simple loading variant; customize as needed
},
},
defaultVariants: {
Expand Down Expand Up @@ -110,6 +112,14 @@ const ToastDescription = React.forwardRef<
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;

// Loading-specific content (e.g., spinner)
const ToastLoadingContent = ({ className }: { className?: string }) => (
<div className={cn('flex items-center gap-2', className)}>
<Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span>
</div>
);

type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;

type ToastActionElement = React.ReactElement<typeof ToastAction>;
Expand All @@ -124,4 +134,5 @@ export {
ToastDescription,
ToastClose,
ToastAction,
};
ToastLoadingContent,
};
Loading