diff --git a/platforms/eReputation/.gitignore b/platforms/eReputation/.gitignore new file mode 100644 index 00000000..f9ba7f8b --- /dev/null +++ b/platforms/eReputation/.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/README.md b/platforms/eReputation/README.md new file mode 100644 index 00000000..e8a5b634 --- /dev/null +++ b/platforms/eReputation/README.md @@ -0,0 +1,172 @@ +# eReputation - Professional Reputation Management Platform + +## Overview + +eReputation is a full-stack web application for professional reputation analysis and management. The platform allows users to calculate reputation scores for themselves and others (users, groups, platforms) using AI-powered analysis. It features a modern React frontend with shadcn/ui components and an Express.js backend with PostgreSQL database integration. + +## Recent Changes (August 11, 2025) + +### Production-Ready Email/Password Authentication System Completed +- Successfully replaced Replit auth with comprehensive email/password authentication +- Implemented bcrypt password hashing for secure credential storage +- Created session-based authentication with PostgreSQL session store +- Built QR code-style authentication interface with eReputation branding +- Updated all API routes and middleware to use new requireAuth system +- Created TypeORM User entity with email, password, firstName, lastName fields +- Cleaned authentication page design with W3DS branding and MetaState messaging + +### Database Migration to TypeORM Completed +- Successfully migrated from Drizzle ORM to TypeORM with PostgreSQL +- Created comprehensive TypeORM entities preserving all existing schema relationships +- Implemented automatic database schema creation and initialization +- Maintained backward compatibility through schema re-exports +- Database migration infrastructure ready for seamless data preservation + +### AI Functionality Removed - Simple Digital Signatures Added +- Removed all AI/OpenAI functionality from reference creation and reputation calculation +- Implemented simple digital signature system for references using basic cryptographic hashing +- References now include digital signature and timestamp for authenticity verification +- Reputation calculations use simplified random scoring instead of AI analysis +- System ready for future replacement with more sophisticated signature libraries + +### Unified Reference View Modal System Completed +- Created shared ReferenceViewModal component for consistent modal behavior across all pages +- Fixed dashboard reference view to display actual reference content from database instead of mock data +- Both dashboard and references pages now use identical modal component for viewing references +- Updated activity handler to access full reference data from activity.data property +- Eliminated duplicate modal code and ensured consistent user experience +- Mobile-first responsive design maintained across all reference viewing interfaces +- Maintained separate modal systems: ReferenceViewModal for references, Activity Details modal for eReputation calculations + +### Landing Page Redesign Completed +- Implemented clean gradient background (fig/30 to fig/10, bottom to top) +- Removed blur effects behind logo for cleaner appearance +- Updated modal background to fig-10 for consistency +- Applied branded fig background icons with swiss-cheese gold text throughout +- Updated feature text to "Calculate", "Reference", "Share" with proper spacing +- Optimized icon spacing for desktop viewing (gap-8, justify-center) + +## User Preferences + +Preferred communication style: Simple, everyday language. + +## System Architecture + +### Frontend Architecture +- **Framework**: React 18 with TypeScript +- **Build Tool**: Vite for fast development and optimized builds +- **UI Library**: shadcn/ui components built on Radix UI primitives +- **Styling**: Tailwind CSS with custom design tokens and CSS variables +- **State Management**: TanStack Query (React Query) for server state +- **Routing**: Wouter for lightweight client-side routing +- **Form Handling**: React Hook Form with Zod validation + +### Backend Architecture +- **Runtime**: Node.js with Express.js framework +- **Language**: TypeScript with ES modules +- **Database**: PostgreSQL with Neon serverless driver +- **ORM**: TypeORM for entity-based database operations +- **Authentication**: Email/password authentication with bcrypt hashing +- **Session Management**: Express sessions with PostgreSQL storage +- **File Uploads**: Multer for handling file attachments + +## Key Components + +### Authentication System +- **Provider**: Email/password authentication with bcrypt +- **Session Storage**: PostgreSQL-backed sessions with connect-pg-simple +- **Security**: HTTP-only secure cookies with session-based authentication +- **User Management**: Registration and login with secure password hashing +- **Interface**: QR code-style login page with eReputation and W3DS branding + +### Reputation Analysis Engine +- **AI Integration**: OpenAI GPT-4o for reputation analysis +- **Analysis Types**: Self-assessment, user evaluation, group/platform analysis +- **Variables**: Configurable analysis parameters (comment history, references, qualifications, etc.) +- **Scoring**: 1-10 reputation score with confidence metrics + +### Database Schema +- **Users**: Profile information and authentication data +- **Reputation Calculations**: Analysis results with scores and confidence +- **References**: Professional endorsements and testimonials +- **File Uploads**: Document attachments for evidence +- **Sessions**: Authentication session storage + +### File Management +- **Upload Handling**: Multer-based file processing +- **File Types**: Images (JPEG, PNG, GIF) and documents (PDF, DOC, DOCX) +- **Size Limits**: 10MB maximum file size +- **Storage**: Local filesystem storage with configurable paths + +## Data Flow + +1. **Authentication Flow**: + - User initiates login via Replit Auth + - OIDC provider validates credentials + - Session created and stored in PostgreSQL + - User profile created/updated in database + +2. **Reputation Calculation Flow**: + - User selects analysis type and variables + - Backend creates calculation record with "processing" status + - OpenAI API analyzes based on selected parameters + - Results stored with score, confidence, and detailed analysis + - Frontend updates to show completed analysis + +3. **Reference Management Flow**: + - Users can create references for others + - File uploads supported for evidence + - References linked to target users/groups/platforms + - Analysis engine can incorporate reference data + +## External Dependencies + +### Core Infrastructure +- **Database**: Neon PostgreSQL for serverless database hosting +- **Authentication**: Replit Auth service for OIDC authentication +- **AI Processing**: OpenAI API for reputation analysis + +### Development Tools +- **Package Manager**: npm with lockfile version 3 +- **TypeScript**: Type checking and compilation +- **ESBuild**: Production bundling for server code +- **Drizzle Kit**: Database migrations and schema management + +### UI/UX Libraries +- **Radix UI**: Accessible component primitives +- **Tailwind CSS**: Utility-first styling framework +- **Lucide React**: Icon library +- **React Hook Form**: Form state management +- **Zod**: Runtime type validation + +## Deployment Strategy + +### Development Environment +- **Dev Server**: Vite dev server with HMR for frontend +- **Backend**: tsx for TypeScript execution with hot reload +- **Database**: Drizzle push for schema synchronization +- **Environment**: NODE_ENV=development with debug logging + +### Production Build +- **Frontend**: Vite build to dist/public directory +- **Backend**: ESBuild bundle to dist/index.js +- **Static Serving**: Express serves built frontend assets +- **Process**: Single Node.js process serving both frontend and API + +### Environment Configuration +- **Database**: DATABASE_URL for PostgreSQL connection +- **Auth**: REPL_ID, SESSION_SECRET, ISSUER_URL for authentication +- **AI**: OPENAI_API_KEY for reputation analysis +- **Domains**: REPLIT_DOMAINS for CORS and auth configuration + +### File Structure +``` +├── client/ # React frontend application +├── server/ # Express.js backend API +├── shared/ # Shared TypeScript types and schemas +├── dist/ # Production build output +├── uploads/ # File upload storage +└── migrations/ # Database migration files +``` + +The application follows a monorepo structure with clear separation between frontend, backend, and shared code, making it easy to maintain and scale both components independently while sharing common types and utilities. \ No newline at end of file diff --git a/platforms/eReputation/client/index.html b/platforms/eReputation/client/index.html new file mode 100644 index 00000000..4b4d09e3 --- /dev/null +++ b/platforms/eReputation/client/index.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/platforms/eReputation/client/src/App.tsx b/platforms/eReputation/client/src/App.tsx new file mode 100644 index 00000000..2f8a1fcb --- /dev/null +++ b/platforms/eReputation/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/client/src/components/modals/other-calculation-modal.tsx b/platforms/eReputation/client/src/components/modals/other-calculation-modal.tsx new file mode 100644 index 00000000..2e2eacf2 --- /dev/null +++ b/platforms/eReputation/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/client/src/components/modals/reference-modal.tsx b/platforms/eReputation/client/src/components/modals/reference-modal.tsx new file mode 100644 index 00000000..d24bdcf5 --- /dev/null +++ b/platforms/eReputation/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 */} +
+ +