Initial Commit

This commit is contained in:
2025-11-16 16:00:51 +00:00
commit ba324b370e
52 changed files with 8014 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { useAuth } from '@clerk/clerk-react';
import { Navigate, useLocation } from 'react-router-dom';
import { ReactNode } from 'react';
interface ProtectedRouteProps {
children: ReactNode;
redirectTo?: string;
}
/**
* Protected route wrapper that requires authentication
* Redirects to sign-in page if user is not authenticated
*/
export function ProtectedRoute({
children,
redirectTo = '/sign-in'
}: ProtectedRouteProps) {
const { isLoaded, isSignedIn } = useAuth();
const location = useLocation();
// Wait for Clerk to load
if (!isLoaded) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
// Redirect to sign-in if not authenticated
if (!isSignedIn) {
return <Navigate to={redirectTo} state={{ from: location }} replace />;
}
return <>{children}</>;
}

View File

@@ -0,0 +1,43 @@
import { SignIn } from '@clerk/clerk-react';
import { useNavigate, useLocation } from 'react-router-dom';
/**
* Sign-in page with Clerk authentication
*/
export function SignInPage() {
const navigate = useNavigate();
const location = useLocation();
// Get the redirect path from location state, default to home
const from = (location.state as any)?.from?.pathname || '/';
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to Beach Litter Mapping
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Join the community in tracking and cleaning coastal pollution
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<SignIn
routing="path"
path="/sign-in"
signUpUrl="/sign-up"
afterSignInUrl={from}
appearance={{
elements: {
rootBox: 'w-full',
card: 'shadow-none',
},
}}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { SignUp } from '@clerk/clerk-react';
/**
* Sign-up page with Clerk authentication
*/
export function SignUpPage() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Start reporting and tracking beach litter in your area
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<SignUp
routing="path"
path="/sign-up"
signInUrl="/sign-in"
afterSignUpUrl="/"
appearance={{
elements: {
rootBox: 'w-full',
card: 'shadow-none',
},
}}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { useUser, useClerk } from '@clerk/clerk-react';
import { useNavigate } from 'react-router-dom';
/**
* User profile display component showing user info and sign-out option
*/
export function UserProfile() {
const { user, isLoaded } = useUser();
const { signOut } = useClerk();
const navigate = useNavigate();
const handleSignOut = async () => {
await signOut();
navigate('/');
};
if (!isLoaded) {
return (
<div className="flex items-center space-x-3">
<div className="animate-pulse">
<div className="h-10 w-10 bg-gray-300 rounded-full"></div>
</div>
</div>
);
}
if (!user) {
return null;
}
return (
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-3">
{user.imageUrl && (
<img
src={user.imageUrl}
alt={user.fullName || user.username || 'User'}
className="h-10 w-10 rounded-full object-cover border-2 border-gray-200"
/>
)}
<div className="hidden md:block">
<p className="text-sm font-medium text-gray-900">
{user.fullName || user.username || 'User'}
</p>
<p className="text-xs text-gray-500">
{user.primaryEmailAddress?.emailAddress}
</p>
</div>
</div>
<button
onClick={handleSignOut}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Sign Out
</button>
</div>
);
}

View File

@@ -0,0 +1,4 @@
export { ProtectedRoute } from './ProtectedRoute';
export { UserProfile } from './UserProfile';
export { SignInPage } from './SignInPage';
export { SignUpPage } from './SignUpPage';