diff --git a/Backend/add_availability_feature_COMPLETE.sql b/Backend/add_availability_feature_COMPLETE.sql new file mode 100644 index 0000000..3dc3a41 --- /dev/null +++ b/Backend/add_availability_feature_COMPLETE.sql @@ -0,0 +1,55 @@ +-- ================================================ +-- AVAILABILITY STATUS FEATURE - DATABASE SETUP +-- ================================================ +-- Run this ENTIRE script in your Supabase SQL Editor +-- Go to: https://supabase.com/dashboard → Your Project → SQL Editor → New Query +-- Copy and paste this entire file, then click "Run" +-- ================================================ + +-- Step 1: Add the new columns to users table +ALTER TABLE public.users +ADD COLUMN IF NOT EXISTS availability_status TEXT DEFAULT 'available', +ADD COLUMN IF NOT EXISTS availability_message TEXT DEFAULT NULL; + +-- Step 2: Add validation constraint +-- Drop existing constraint if it exists (in case you're re-running this) +ALTER TABLE public.users DROP CONSTRAINT IF EXISTS check_availability_status; + +-- Add the constraint with valid values +ALTER TABLE public.users +ADD CONSTRAINT check_availability_status +CHECK (availability_status IN ('available', 'busy', 'not_looking')); + +-- Step 3: Create index for faster filtering +CREATE INDEX IF NOT EXISTS idx_users_availability +ON public.users(availability_status); + +-- Step 4: Add helpful comments for documentation +COMMENT ON COLUMN public.users.availability_status IS + 'Creator availability status: available (open for work), busy (booked but visible), not_looking (hidden from search)'; + +COMMENT ON COLUMN public.users.availability_message IS + 'Optional custom message shown to brands (max 150 chars). Example: "Available starting January 2026"'; + +-- Step 5: Update existing users to have default availability +-- This ensures all existing creators show as "available" by default +UPDATE public.users +SET availability_status = 'available' +WHERE availability_status IS NULL; + +-- ================================================ +-- VERIFICATION: Check if it worked +-- ================================================ +-- After running the above, run this query to verify: +-- SELECT id, username, availability_status, availability_message FROM public.users LIMIT 5; + +-- ================================================ +-- SUCCESS! +-- ================================================ +-- You should see: +-- ✓ availability_status column added +-- ✓ availability_message column added +-- ✓ All existing users set to 'available' +-- +-- Now refresh your frontend and the feature will work! +-- ================================================ diff --git a/Backend/add_availability_status.sql b/Backend/add_availability_status.sql new file mode 100644 index 0000000..fbafa9f --- /dev/null +++ b/Backend/add_availability_status.sql @@ -0,0 +1,16 @@ +-- Add availability status to users table +ALTER TABLE public.users +ADD COLUMN IF NOT EXISTS availability_status TEXT DEFAULT 'available', +ADD COLUMN IF NOT EXISTS availability_message TEXT DEFAULT NULL; + +-- Add check constraint to ensure valid status values +ALTER TABLE public.users +ADD CONSTRAINT check_availability_status +CHECK (availability_status IN ('available', 'busy', 'not_looking')); + +-- Add index for faster filtering +CREATE INDEX IF NOT EXISTS idx_users_availability ON public.users(availability_status); + +-- Add comment for documentation +COMMENT ON COLUMN public.users.availability_status IS 'Creator availability: available, busy, or not_looking'; +COMMENT ON COLUMN public.users.availability_message IS 'Optional custom message shown to brands (e.g., "Available starting Jan 2026")'; diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 60f7ecd..55e581a 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -20,6 +20,8 @@ import PublicRoute from "./components/PublicRoute"; import Dashboard from "./pages/Brand/Dashboard"; import BasicDetails from "./pages/BasicDetails"; import Onboarding from "./components/Onboarding"; +import ScrollToTop from "./components/ui/scroll-to-top"; +import CreatorSearch from "./pages/Brand/CreatorSearch"; function App() { const [isLoading, setIsLoading] = useState(true); @@ -45,6 +47,7 @@ function App() { return ( + {/* Public Routes */} } /> @@ -68,6 +71,11 @@ function App() { } /> + + + + } /> } /> } /> { + switch (status) { + case 'available': + return { + icon: CheckCircle, + label: 'Available', + bgColor: 'bg-green-100', + textColor: 'text-green-700', + borderColor: 'border-green-300', + dotColor: 'bg-green-500' + }; + case 'busy': + return { + icon: Clock, + label: 'Busy', + bgColor: 'bg-yellow-100', + textColor: 'text-yellow-700', + borderColor: 'border-yellow-300', + dotColor: 'bg-yellow-500' + }; + case 'not_looking': + return { + icon: XCircle, + label: 'Not Available', + bgColor: 'bg-gray-100', + textColor: 'text-gray-600', + borderColor: 'border-gray-300', + dotColor: 'bg-gray-500' + }; + default: + return { + icon: CheckCircle, + label: 'Available', + bgColor: 'bg-green-100', + textColor: 'text-green-700', + borderColor: 'border-green-300', + dotColor: 'bg-green-500' + }; + } + }; + + /** + * Returns Tailwind CSS classes for the specified badge size. + */ + const getSizeClasses = () => { + switch (size) { + case 'sm': + return { + badge: 'px-2 py-0.5 text-xs', + icon: 'h-3 w-3', + dot: 'h-2 w-2' + }; + case 'lg': + return { + badge: 'px-4 py-2 text-base', + icon: 'h-5 w-5', + dot: 'h-3 w-3' + }; + default: // md + return { + badge: 'px-3 py-1 text-sm', + icon: 'h-4 w-4', + dot: 'h-2.5 w-2.5' + }; + } + }; + + const config = getConfig(); + const sizeClasses = getSizeClasses(); + const Icon = config.icon; + + return ( +
+ + + + {config.label} + + {showMessage && message && ( + + "{message}" + + )} +
+ ); +} diff --git a/Frontend/src/components/AvailabilityToggle.tsx b/Frontend/src/components/AvailabilityToggle.tsx new file mode 100644 index 0000000..eb729c5 --- /dev/null +++ b/Frontend/src/components/AvailabilityToggle.tsx @@ -0,0 +1,246 @@ +import { useState, useEffect } from 'react'; +import { useAuth } from '../context/AuthContext'; +import { supabase } from '../utils/supabase'; +import { CheckCircle, XCircle, Clock } from 'lucide-react'; + +type AvailabilityStatus = 'available' | 'busy' | 'not_looking'; + +interface AvailabilityData { + status: AvailabilityStatus; + message: string; +} + +/** + * AvailabilityToggle component allows creators to set their availability status + * and optional custom message visible to brands. + */ +export default function AvailabilityToggle() { + const { user } = useAuth(); + const [availability, setAvailability] = useState({ + status: 'available', + message: '' + }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [showMessage, setShowMessage] = useState(false); + + useEffect(() => { + /** + * Loads the user's current availability status from the database. + * Falls back to default 'available' status if columns don't exist yet. + */ + const loadAvailability = async () => { + try { + const { data, error } = await supabase + .from('users') + .select('availability_status, availability_message') + .eq('id', user?.id) + .single(); + + if (error) { + console.error('Error loading availability:', error); + // If columns don't exist, just use defaults + setAvailability({ status: 'available', message: '' }); + return; + } + + if (data) { + setAvailability({ + status: data.availability_status || 'available', + message: data.availability_message || '' + }); + setShowMessage(!!data.availability_message); + } + } catch (error) { + console.error('Error loading availability:', error); + setAvailability({ status: 'available', message: '' }); + } finally { + setLoading(false); + } + }; + + if (user) { + void loadAvailability(); + } + }, [user]); + + /** + * Updates the user's availability status in the database. + * @param newStatus - The new availability status to set + * @param newMessage - Optional custom message to display to brands + */ + const updateAvailability = async (newStatus: AvailabilityStatus, newMessage?: string) => { + setSaving(true); + try { + const { error } = await supabase + .from('users') + .update({ + availability_status: newStatus, + availability_message: newMessage !== undefined ? (newMessage || null) : (availability.message || null) + }) + .eq('id', user?.id); + + if (error) { + console.error('Error updating availability:', error); + alert(`Failed to update availability status.\n\nError: ${error.message}\n\nPlease run the SQL migration in Supabase first. Check AVAILABILITY_STATUS_GUIDE.md for instructions.`); + setSaving(false); + return; + } + + setAvailability({ + status: newStatus, + message: newMessage !== undefined ? newMessage : availability.message + }); + } catch (error) { + console.error('Error updating availability:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + alert(`Failed to update availability status.\n\nError: ${errorMessage}\n\nPlease run the SQL migration in Supabase first.`); + } finally { + setSaving(false); + } + }; + + /** + * Returns configuration object for display styling based on status. + * @param status - The availability status + * @returns Configuration with icon, colors, label, and description + */ + const getStatusConfig = (status: AvailabilityStatus) => { + switch (status) { + case 'available': + return { + icon: CheckCircle, + color: 'bg-green-500', + textColor: 'text-green-600', + bgColor: 'bg-green-50', + borderColor: 'border-green-200', + label: 'Available for Work', + description: 'Brands can see you\'re actively looking for collaborations' + }; + case 'busy': + return { + icon: Clock, + color: 'bg-yellow-500', + textColor: 'text-yellow-600', + bgColor: 'bg-yellow-50', + borderColor: 'border-yellow-200', + label: 'Currently Busy', + description: 'You\'ll still appear in searches but marked as busy' + }; + case 'not_looking': + return { + icon: XCircle, + color: 'bg-gray-500', + textColor: 'text-gray-600', + bgColor: 'bg-gray-50', + borderColor: 'border-gray-200', + label: 'Not Looking', + description: 'You won\'t appear in brand searches' + }; + } + }; + + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + const currentConfig = getStatusConfig(availability.status); + const Icon = currentConfig.icon; + + return ( +
+
+
+

Availability Status

+

Let brands know if you're open to collaborations

+
+
+ +
+
+ + {/* Status Options */} +
+ {(['available', 'busy', 'not_looking'] as AvailabilityStatus[]).map((status) => { + const config = getStatusConfig(status); + const StatusIcon = config.icon; + const isSelected = availability.status === status; + + return ( + + ); + })} +
+ + {/* Optional Custom Message */} +
+ + + {showMessage && ( +
+