From 32b0005e5c0f44ccce3e45af706db41f0cac1874 Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Tue, 2 Sep 2025 10:51:22 +0530 Subject: [PATCH 1/3] feat: add erep --- platforms/eReputation-Basic/.gitignore | 6 + platforms/eReputation-Basic/client/index.html | 13 + .../eReputation-Basic/client/src/App.tsx | 40 + .../modals/other-calculation-modal.tsx | 528 + .../src/components/modals/reference-modal.tsx | 446 + .../modals/reference-view-modal.tsx | 91 + .../modals/self-calculation-modal.tsx | 254 + .../modals/view-reputation-modal.tsx | 111 + .../client/src/components/ui/accordion.tsx | 56 + .../client/src/components/ui/alert-dialog.tsx | 139 + .../client/src/components/ui/alert.tsx | 59 + .../client/src/components/ui/aspect-ratio.tsx | 5 + .../client/src/components/ui/avatar.tsx | 50 + .../client/src/components/ui/badge.tsx | 36 + .../client/src/components/ui/breadcrumb.tsx | 115 + .../client/src/components/ui/button.tsx | 56 + .../client/src/components/ui/calendar.tsx | 68 + .../client/src/components/ui/card.tsx | 79 + .../client/src/components/ui/carousel.tsx | 260 + .../client/src/components/ui/chart.tsx | 365 + .../client/src/components/ui/checkbox.tsx | 28 + .../client/src/components/ui/collapsible.tsx | 11 + .../client/src/components/ui/command.tsx | 151 + .../client/src/components/ui/context-menu.tsx | 198 + .../client/src/components/ui/dialog.tsx | 122 + .../client/src/components/ui/drawer.tsx | 118 + .../src/components/ui/dropdown-menu.tsx | 198 + .../client/src/components/ui/file-upload.tsx | 133 + .../client/src/components/ui/form.tsx | 178 + .../client/src/components/ui/hover-card.tsx | 29 + .../client/src/components/ui/input-otp.tsx | 69 + .../client/src/components/ui/input.tsx | 22 + .../client/src/components/ui/label.tsx | 24 + .../client/src/components/ui/menubar.tsx | 256 + .../src/components/ui/navigation-menu.tsx | 128 + .../client/src/components/ui/pagination.tsx | 117 + .../client/src/components/ui/popover.tsx | 29 + .../client/src/components/ui/progress.tsx | 28 + .../client/src/components/ui/radio-group.tsx | 42 + .../client/src/components/ui/resizable.tsx | 45 + .../client/src/components/ui/scroll-area.tsx | 46 + .../client/src/components/ui/select.tsx | 160 + .../client/src/components/ui/separator.tsx | 29 + .../client/src/components/ui/sheet.tsx | 140 + .../client/src/components/ui/sidebar.tsx | 771 ++ .../client/src/components/ui/skeleton.tsx | 15 + .../client/src/components/ui/slider.tsx | 26 + .../client/src/components/ui/switch.tsx | 27 + .../client/src/components/ui/table.tsx | 117 + .../client/src/components/ui/tabs.tsx | 53 + .../client/src/components/ui/textarea.tsx | 22 + .../client/src/components/ui/toast.tsx | 127 + .../client/src/components/ui/toaster.tsx | 33 + .../client/src/components/ui/toggle-group.tsx | 61 + .../client/src/components/ui/toggle.tsx | 43 + .../client/src/components/ui/tooltip.tsx | 30 + .../client/src/hooks/use-mobile.tsx | 19 + .../client/src/hooks/use-toast.ts | 191 + .../client/src/hooks/useAuth.ts | 97 + .../eReputation-Basic/client/src/index.css | 150 + .../client/src/lib/authUtils.ts | 3 + .../client/src/lib/queryClient.ts | 57 + .../eReputation-Basic/client/src/lib/utils.ts | 6 + .../eReputation-Basic/client/src/main.tsx | 5 + .../client/src/pages/auth-page.tsx | 213 + .../client/src/pages/dashboard.tsx | 878 ++ .../client/src/pages/landing.tsx | 89 + .../client/src/pages/not-found.tsx | 21 + .../client/src/pages/references.tsx | 492 + platforms/eReputation-Basic/components.json | 20 + platforms/eReputation-Basic/drizzle.config.ts | 14 + platforms/eReputation-Basic/package-lock.json | 10220 ++++++++++++++++ platforms/eReputation-Basic/package.json | 121 + platforms/eReputation-Basic/postcss.config.js | 6 + platforms/eReputation-Basic/replit.md | 172 + platforms/eReputation-Basic/server/auth.ts | 161 + platforms/eReputation-Basic/server/db.ts | 15 + platforms/eReputation-Basic/server/index.ts | 81 + .../eReputation-Basic/server/migrate-data.ts | 178 + .../eReputation-Basic/server/replitAuth.ts | 157 + platforms/eReputation-Basic/server/routes.ts | 460 + .../server/services/openai.ts | 138 + .../server/services/reputation.ts | 91 + platforms/eReputation-Basic/server/storage.ts | 154 + .../eReputation-Basic/server/typeorm-db.ts | 28 + .../server/typeorm.config.ts | 17 + platforms/eReputation-Basic/server/vite.ts | 85 + .../eReputation-Basic/shared/entities.ts | 205 + .../eReputation-Basic/shared/schema-backup.ts | 112 + platforms/eReputation-Basic/shared/schema.ts | 115 + .../eReputation-Basic/tailwind.config.ts | 98 + platforms/eReputation-Basic/tsconfig.json | 25 + platforms/eReputation-Basic/vite.config.ts | 37 + 93 files changed, 21334 insertions(+) create mode 100644 platforms/eReputation-Basic/.gitignore create mode 100644 platforms/eReputation-Basic/client/index.html create mode 100644 platforms/eReputation-Basic/client/src/App.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/modals/other-calculation-modal.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/modals/reference-modal.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/modals/reference-view-modal.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/modals/self-calculation-modal.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/modals/view-reputation-modal.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/accordion.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/alert-dialog.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/alert.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/aspect-ratio.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/avatar.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/badge.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/breadcrumb.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/button.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/calendar.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/card.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/carousel.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/chart.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/checkbox.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/collapsible.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/command.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/context-menu.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/dialog.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/drawer.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/dropdown-menu.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/file-upload.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/form.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/hover-card.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/input-otp.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/input.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/label.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/menubar.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/navigation-menu.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/pagination.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/popover.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/progress.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/radio-group.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/resizable.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/scroll-area.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/select.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/separator.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/sheet.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/sidebar.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/skeleton.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/slider.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/switch.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/table.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/tabs.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/textarea.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/toast.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/toaster.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/toggle-group.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/toggle.tsx create mode 100644 platforms/eReputation-Basic/client/src/components/ui/tooltip.tsx create mode 100644 platforms/eReputation-Basic/client/src/hooks/use-mobile.tsx create mode 100644 platforms/eReputation-Basic/client/src/hooks/use-toast.ts create mode 100644 platforms/eReputation-Basic/client/src/hooks/useAuth.ts create mode 100644 platforms/eReputation-Basic/client/src/index.css create mode 100644 platforms/eReputation-Basic/client/src/lib/authUtils.ts create mode 100644 platforms/eReputation-Basic/client/src/lib/queryClient.ts create mode 100644 platforms/eReputation-Basic/client/src/lib/utils.ts create mode 100644 platforms/eReputation-Basic/client/src/main.tsx create mode 100644 platforms/eReputation-Basic/client/src/pages/auth-page.tsx create mode 100644 platforms/eReputation-Basic/client/src/pages/dashboard.tsx create mode 100644 platforms/eReputation-Basic/client/src/pages/landing.tsx create mode 100644 platforms/eReputation-Basic/client/src/pages/not-found.tsx create mode 100644 platforms/eReputation-Basic/client/src/pages/references.tsx create mode 100644 platforms/eReputation-Basic/components.json create mode 100644 platforms/eReputation-Basic/drizzle.config.ts create mode 100644 platforms/eReputation-Basic/package-lock.json create mode 100644 platforms/eReputation-Basic/package.json create mode 100644 platforms/eReputation-Basic/postcss.config.js create mode 100644 platforms/eReputation-Basic/replit.md create mode 100644 platforms/eReputation-Basic/server/auth.ts create mode 100644 platforms/eReputation-Basic/server/db.ts create mode 100644 platforms/eReputation-Basic/server/index.ts create mode 100644 platforms/eReputation-Basic/server/migrate-data.ts create mode 100644 platforms/eReputation-Basic/server/replitAuth.ts create mode 100644 platforms/eReputation-Basic/server/routes.ts create mode 100644 platforms/eReputation-Basic/server/services/openai.ts create mode 100644 platforms/eReputation-Basic/server/services/reputation.ts create mode 100644 platforms/eReputation-Basic/server/storage.ts create mode 100644 platforms/eReputation-Basic/server/typeorm-db.ts create mode 100644 platforms/eReputation-Basic/server/typeorm.config.ts create mode 100644 platforms/eReputation-Basic/server/vite.ts create mode 100644 platforms/eReputation-Basic/shared/entities.ts create mode 100644 platforms/eReputation-Basic/shared/schema-backup.ts create mode 100644 platforms/eReputation-Basic/shared/schema.ts create mode 100644 platforms/eReputation-Basic/tailwind.config.ts create mode 100644 platforms/eReputation-Basic/tsconfig.json create mode 100644 platforms/eReputation-Basic/vite.config.ts diff --git a/platforms/eReputation-Basic/.gitignore b/platforms/eReputation-Basic/.gitignore new file mode 100644 index 00000000..f9ba7f8b --- /dev/null +++ b/platforms/eReputation-Basic/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.DS_Store +server/public +vite.config.ts.* +*.tar.gz \ No newline at end of file diff --git a/platforms/eReputation-Basic/client/index.html b/platforms/eReputation-Basic/client/index.html new file mode 100644 index 00000000..4b4d09e3 --- /dev/null +++ b/platforms/eReputation-Basic/client/index.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/platforms/eReputation-Basic/client/src/App.tsx b/platforms/eReputation-Basic/client/src/App.tsx new file mode 100644 index 00000000..2f8a1fcb --- /dev/null +++ b/platforms/eReputation-Basic/client/src/App.tsx @@ -0,0 +1,40 @@ +import { Switch, Route } from "wouter"; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { useAuth } from "@/hooks/useAuth"; +import AuthPage from "@/pages/auth-page"; +import Dashboard from "@/pages/dashboard"; +import References from "@/pages/references"; +import NotFound from "@/pages/not-found"; + +function Router() { + const { isAuthenticated, isLoading } = useAuth(); + + // Show auth page if loading or not authenticated + if (isLoading || !isAuthenticated) { + return ; + } + + return ( + + + + + + ); +} + +function App() { + return ( + + + + + + + ); +} + +export default App; diff --git a/platforms/eReputation-Basic/client/src/components/modals/other-calculation-modal.tsx b/platforms/eReputation-Basic/client/src/components/modals/other-calculation-modal.tsx new file mode 100644 index 00000000..2e2eacf2 --- /dev/null +++ b/platforms/eReputation-Basic/client/src/components/modals/other-calculation-modal.tsx @@ -0,0 +1,528 @@ +import { useState, useEffect } from "react"; +import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query"; +import { useDebouncedCallback } from 'use-debounce'; +import { apiRequest } from "@/lib/queryClient"; +import { useToast } from "@/hooks/use-toast"; +import { isUnauthorizedError } from "@/lib/authUtils"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import ViewReputationModal from "./view-reputation-modal"; + + +interface OtherCalculationModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const TARGET_TYPES = [ + { + value: "user", + label: "User", + icon: ( + + + + ) + }, + { + value: "group", + label: "Group", + icon: ( + + + + ) + }, + { + value: "platform", + label: "Platform", + icon: ( + + + + ) + } +]; + +const ANALYSIS_STEPS = [ + { label: "Analyzing post interactions", platform: "Pictique" }, + { label: "Evaluating social engagement", platform: "Blabsy" }, + { label: "Processing positive feedback", platform: "Comment Likes" }, + { label: "Processing negative feedback", platform: "Comment Dislikes" }, + { label: "Measuring social interactions", platform: "Social Interactions" }, + { label: "Finalizing eReputation score", platform: "Calculating" } +]; + +const ALL_VARIABLES = ["comment-history", "references", "qualifications", "profile-completeness", "engagement", "activity-frequency"]; + +const OTHER_VARIABLES = [ + { + id: "comment-history", + label: "Comment History", + description: "Analyze past interactions and feedback" + }, + { + id: "references", + label: "References", + description: "Professional endorsements received" + }, + { + id: "qualifications", + label: "Qualifications", + description: "Educational and professional credentials" + }, + { + id: "profile-completeness", + label: "Profile Completeness", + description: "How complete your profile information is" + }, + { + id: "engagement", + label: "Likes/Dislikes", + description: "Community engagement metrics" + }, + { + id: "activity-frequency", + label: "Activity Frequency", + description: "Consistency of platform participation" + } +]; + +export default function OtherCalculationModal({ open, onOpenChange }: OtherCalculationModalProps) { + const [targetType, setTargetType] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedTarget, setSelectedTarget] = useState(null); + const [isCalculating, setIsCalculating] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [progress, setProgress] = useState(0); + const [showViewModal, setShowViewModal] = useState(false); + const [reputationResult, setReputationResult] = useState(null); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // Progress simulation effect + useEffect(() => { + if (isCalculating && currentStep < ANALYSIS_STEPS.length) { + const timer = setTimeout(() => { + setCurrentStep(prev => prev + 1); + setProgress(prev => Math.min(100, prev + (100 / ANALYSIS_STEPS.length))); + }, 800 + Math.random() * 400); // Random delay between 800-1200ms for realism + + return () => clearTimeout(timer); + } + }, [isCalculating, currentStep]); + + const debouncedSearch = useDebouncedCallback((query: string) => { + if (query.length >= 2) { + refetch(); + } + }, 300); + + const { data: searchResults = [], refetch } = useQuery({ + queryKey: ['/api/search', targetType, searchQuery], + queryFn: () => { + if (!targetType || searchQuery.length < 2) return []; + const endpoint = `/api/search/${targetType}s?q=${encodeURIComponent(searchQuery)}`; + return fetch(endpoint, { credentials: "include" }).then(res => res.json()); + }, + enabled: false, + }); + + const calculateMutation = useMutation({ + mutationFn: async () => { + const response = await apiRequest("POST", "/api/reputation/calculate", { + targetType: targetType, + targetId: selectedTarget?.id || '', + targetName: selectedTarget?.name || selectedTarget?.title || 'Unknown', + variables: ALL_VARIABLES + }); + return response.json(); + }, + onError: (error) => { + setIsCalculating(false); + setCurrentStep(0); + setProgress(0); + + if (isUnauthorizedError(error)) { + toast({ + title: "Unauthorized", + description: "You are logged out. Logging in again...", + variant: "destructive", + }); + setTimeout(() => { + window.location.href = "/api/login"; + }, 500); + return; + } + toast({ + title: "Calculation Failed", + description: error instanceof Error ? error.message : "Failed to calculate reputation", + variant: "destructive", + }); + }, + }); + + // When progress completes, wait a bit then show results + useEffect(() => { + if (currentStep >= ANALYSIS_STEPS.length && isCalculating && calculateMutation.data) { + // Small delay to show 100% completion + setTimeout(() => { + setReputationResult(calculateMutation.data); + setIsCalculating(false); + setShowViewModal(true); + + // Update dashboard queries + queryClient.invalidateQueries({ queryKey: ["/api/dashboard/stats"] }); + queryClient.invalidateQueries({ queryKey: ["/api/dashboard/activities"] }); + }, 500); + } + }, [currentStep, isCalculating, calculateMutation.data, queryClient]); + + const resetForm = () => { + setTargetType(""); + setSearchQuery(""); + setSelectedTarget(null); + setIsCalculating(false); + setCurrentStep(0); + setProgress(0); + setReputationResult(null); + }; + + const handleSearchChange = (value: string) => { + setSearchQuery(value); + // Trigger search if query is long enough + if (value.length >= 2) { + debouncedSearch(value); + } + // Don't automatically set selected target, let user pick from results + if (!value.trim()) { + setSelectedTarget(null); + } + }; + + const handleSelectTarget = (target: any) => { + setSelectedTarget(target); + setSearchQuery(target.name); + }; + + const handleStartCalculation = () => { + if (!targetType) { + toast({ + title: "Invalid Selection", + description: "Please select a target type", + variant: "destructive", + }); + return; + } + + if (!selectedTarget) { + toast({ + title: "Invalid Selection", + description: "Please select a target to evaluate", + variant: "destructive", + }); + return; + } + + setIsCalculating(true); + setCurrentStep(0); + setProgress(0); + calculateMutation.mutate(); + }; + + const handleCloseModal = () => { + // Reset all states when closing + resetForm(); + onOpenChange(false); + }; + + return ( + + + +
+
+ + + +
+
+ Evaluate Others' eReputation + Calculate eReputation for users, groups, or platforms throughout the W3DS +
+
+
+ +
+
+ {/* Progress Bar or Ready State */} + {isCalculating ? ( + // Calculating state - show progress +
+
+

Calculating {selectedTarget?.name || 'Target'}'s eReputation

+

+ {currentStep < ANALYSIS_STEPS.length + ? ANALYSIS_STEPS[currentStep].label + : "Calculation complete!" + } +

+
+ + {/* Progress Bar */} +
+ +
+ Progress + {Math.round(progress)}% +
+
+ + {/* Current Platform */} + {currentStep < ANALYSIS_STEPS.length && ( +
+
+
+
+
+
+
+ {ANALYSIS_STEPS[currentStep].platform} +
+
+ {ANALYSIS_STEPS[currentStep].label} +
+
+
+
+ )} + + {/* Steps completed */} +
+ {ANALYSIS_STEPS.slice(0, currentStep).map((step, index) => ( +
+ + + + {step.label} +
+ ))} +
+
+ ) : selectedTarget ? ( + // Ready to calculate state +
+
+ + + +
+ +
+

Ready to Calculate

+

+ We'll analyze {selectedTarget.name}'s eReputation across multiple post-platforms including likes, dislikes, and engagement metrics. +

+
+ +
+
+ + + + Analysis includes all eReputation factors automatically +
+
+
+ ) : ( + // Default placeholder state when no target selected +
+
+ + + +
+ +
+

Select Target to Evaluate

+

+ Choose a target type below and search for a user, group, or platform to calculate their eReputation across multiple post-platforms. +

+
+ +
+
+ + + + Analysis includes all eReputation factors automatically +
+
+
+ )} + + {/* Target Selection */} +
+

Select Target Type

+ +
+ {TARGET_TYPES.map((type) => ( + + ))} +
+
+
+ + {/* Search Target */} +
+ +
+ handleSearchChange(e.target.value)} + className="pl-10 border-2 border-fig/20 focus:border-fig/40 focus:ring-fig/20 rounded-2xl" + disabled={!targetType} + /> + + + + + {/* Search Results Dropdown - Absolute positioned overlay */} + {searchQuery.length >= 2 && searchResults.length > 0 && !selectedTarget && ( +
+ {searchResults.map((result: any, index: number) => ( + + ))} +
+ )} + + {/* Manual Entry Option */} + {searchQuery.length >= 2 && !selectedTarget && ( +
+ +
+ )} +
+ + {/* Selected Target Display */} + {selectedTarget && ( +
+
+
+ + + + {selectedTarget.name} +
+ +
+
+ )} +
+
+
+ +
+
+ + +
+
+
+ + {/* View Reputation Modal */} + { + setShowViewModal(open); + if (!open) { + handleCloseModal(); + } + }} + reputationData={reputationResult} + /> +
+ ); +} diff --git a/platforms/eReputation-Basic/client/src/components/modals/reference-modal.tsx b/platforms/eReputation-Basic/client/src/components/modals/reference-modal.tsx new file mode 100644 index 00000000..d24bdcf5 --- /dev/null +++ b/platforms/eReputation-Basic/client/src/components/modals/reference-modal.tsx @@ -0,0 +1,446 @@ +import { useState } from "react"; +import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query"; +import { useToast } from "@/hooks/use-toast"; +import { isUnauthorizedError } from "@/lib/authUtils"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import FileUpload from "@/components/ui/file-upload"; +import { useDebouncedCallback } from "use-debounce"; + +interface ReferenceModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const TARGET_TYPES = [ + { + value: "user", + label: "User", + icon: ( + + + + ) + }, + { + value: "group", + label: "Group", + icon: ( + + + + ) + }, + { + value: "platform", + label: "Platform", + icon: ( + + + + ) + } +]; + +const REFERENCE_TYPES = [ + { value: "professional", label: "Professional Work" }, + { value: "academic", label: "Academic Achievement" }, + { value: "character", label: "Character Reference" }, + { value: "skill", label: "Skill Endorsement" }, + { value: "leadership", label: "Leadership Qualities" } +]; + +export default function ReferenceModal({ open, onOpenChange }: ReferenceModalProps) { + const [targetType, setTargetType] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedTarget, setSelectedTarget] = useState(null); + const [referenceText, setReferenceText] = useState(""); + const [referenceType, setReferenceType] = useState(""); + const [files, setFiles] = useState([]); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const debouncedSearch = useDebouncedCallback((query: string) => { + if (query.length >= 2) { + refetch(); + } + }, 300); + + const { data: searchResults = [], refetch } = useQuery({ + queryKey: ['/api/search', targetType, searchQuery], + queryFn: () => { + if (!targetType || searchQuery.length < 2) return []; + const endpoint = `/api/search/${targetType}s?q=${encodeURIComponent(searchQuery)}`; + return fetch(endpoint, { credentials: "include" }).then(res => res.json()); + }, + enabled: false, + }); + + const submitMutation = useMutation({ + mutationFn: async (data: any) => { + const formData = new FormData(); + + // Add text fields + Object.keys(data).forEach(key => { + if (key !== 'files') { + formData.append(key, data[key]); + } + }); + + // Add files + files.forEach(file => { + formData.append('files', file); + }); + + const response = await fetch('/api/references', { + method: 'POST', + body: formData, + credentials: 'include', + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`${response.status}: ${error}`); + } + + return response.json(); + }, + onSuccess: () => { + toast({ + title: "Reference Submitted", + description: "Your professional reference has been successfully submitted.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/dashboard/stats"] }); + queryClient.invalidateQueries({ queryKey: ["/api/dashboard/activities"] }); + onOpenChange(false); + resetForm(); + }, + onError: (error) => { + if (isUnauthorizedError(error)) { + toast({ + title: "Unauthorized", + description: "You are logged out. Logging in again...", + variant: "destructive", + }); + setTimeout(() => { + window.location.href = "/api/login"; + }, 500); + return; + } + toast({ + title: "Submission Failed", + description: error instanceof Error ? error.message : "Failed to submit reference", + variant: "destructive", + }); + }, + }); + + const resetForm = () => { + setTargetType(""); + setSearchQuery(""); + setSelectedTarget(null); + setReferenceText(""); + setReferenceType(""); + setFiles([]); + }; + + const handleSearchChange = (value: string) => { + setSearchQuery(value); + // Trigger search if query is long enough + if (value.length >= 2) { + debouncedSearch(value); + } + // Don't automatically set selected target, let user pick from results + if (!value.trim()) { + setSelectedTarget(null); + } + }; + + const handleSelectTarget = (target: any) => { + setSelectedTarget(target); + setSearchQuery(target.name); + }; + + const handleSubmit = () => { + if (!targetType) { + toast({ + title: "Invalid Selection", + description: "Please select a target type", + variant: "destructive", + }); + return; + } + + if (!selectedTarget) { + toast({ + title: "Invalid Selection", + description: "Please select who you want to reference", + variant: "destructive", + }); + return; + } + + if (!referenceText.trim()) { + toast({ + title: "Missing Information", + description: "Please write a reference text", + variant: "destructive", + }); + return; + } + + + + if (referenceText.length > 500) { + toast({ + title: "Text Too Long", + description: "Please keep your reference under 500 characters", + variant: "destructive", + }); + return; + } + + submitMutation.mutate({ + targetType, + targetId: selectedTarget.id, + targetName: selectedTarget.name, + content: referenceText, + referenceType: 'general' + }); + }; + + return ( + + + +
+
+ + + +
+
+ Send an eReference + Provide professional eReferences throughout the W3DS +
+
+
+ +
+
+ {/* Target Selection */} +
+

Select eReference Target

+ +
+ {TARGET_TYPES.map((type) => ( + + ))} +
+
+
+ + {/* Search Target */} +
+ +
+ handleSearchChange(e.target.value)} + className="pl-10 border-2 border-fig/20 focus:border-fig/40 focus:ring-fig/20 rounded-2xl" + disabled={!targetType} + /> + + + + + {/* Search Results Dropdown - Absolute positioned overlay */} + {searchQuery.length >= 2 && searchResults.length > 0 && !selectedTarget && ( +
+ {searchResults.map((result: any, index: number) => ( + + ))} +
+ )} + + {/* Manual Entry Option */} + {searchQuery.length >= 2 && !selectedTarget && ( +
+ +
+ )} +
+ + {/* Selected Target Display */} + {selectedTarget && ( +
+
+
+ + + + {selectedTarget.name} +
+ +
+
+ )} +
+ + {/* Reference Text */} +
+ +