@@ -10,7 +10,7 @@ import { Input } from '@comp/ui/input';
1010import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '@comp/ui/select' ;
1111import { Switch } from '@comp/ui/switch' ;
1212import { zodResolver } from '@hookform/resolvers/zod' ;
13- import { ExternalLink , FileText , Upload } from 'lucide-react' ;
13+ import { ExternalLink , FileText , Upload , Download , Eye , FileCheck2 } from 'lucide-react' ;
1414import { useAction } from 'next-safe-action/hooks' ;
1515import Link from 'next/link' ;
1616import { useCallback , useEffect , useRef , useState } from 'react' ;
@@ -865,7 +865,75 @@ function ComplianceFramework({
865865 orgId : string ;
866866} ) {
867867 const [ isUploading , setIsUploading ] = useState ( false ) ;
868+ const [ isDragging , setIsDragging ] = useState ( false ) ;
868869 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
870+ const dragCounterRef = useRef ( 0 ) ;
871+
872+ const processFile = async ( file : File ) => {
873+ if ( file . type !== 'application/pdf' && ! file . name . toLowerCase ( ) . endsWith ( '.pdf' ) ) {
874+ toast . error ( 'Please upload a PDF file' ) ;
875+ return ;
876+ }
877+
878+ const MAX_FILE_SIZE = 10 * 1024 * 1024 ;
879+ if ( file . size > MAX_FILE_SIZE ) {
880+ toast . error ( 'File size must be less than 10MB' ) ;
881+ return ;
882+ }
883+
884+ if ( onFileUpload ) {
885+ setIsUploading ( true ) ;
886+ try {
887+ await onFileUpload ( file , frameworkKey ) ;
888+ toast . success ( 'Certificate uploaded successfully' ) ;
889+ if ( fileInputRef . current ) {
890+ fileInputRef . current . value = '' ;
891+ }
892+ } catch ( error ) {
893+ const message = error instanceof Error ? error . message : 'Failed to upload certificate' ;
894+ toast . error ( message ) ;
895+ console . error ( 'File upload error:' , error ) ;
896+ } finally {
897+ setIsUploading ( false ) ;
898+ }
899+ }
900+ } ;
901+
902+ const handleDragEnter = ( e : React . DragEvent ) => {
903+ e . preventDefault ( ) ;
904+ e . stopPropagation ( ) ;
905+ dragCounterRef . current ++ ;
906+ if ( e . dataTransfer . items && e . dataTransfer . items . length > 0 ) {
907+ setIsDragging ( true ) ;
908+ }
909+ } ;
910+
911+ const handleDragLeave = ( e : React . DragEvent ) => {
912+ e . preventDefault ( ) ;
913+ e . stopPropagation ( ) ;
914+ dragCounterRef . current -- ;
915+ if ( dragCounterRef . current === 0 ) {
916+ setIsDragging ( false ) ;
917+ }
918+ } ;
919+
920+ const handleDragOver = ( e : React . DragEvent ) => {
921+ e . preventDefault ( ) ;
922+ e . stopPropagation ( ) ;
923+ } ;
924+
925+ const handleDrop = async ( e : React . DragEvent ) => {
926+ e . preventDefault ( ) ;
927+ e . stopPropagation ( ) ;
928+ setIsDragging ( false ) ;
929+ dragCounterRef . current = 0 ;
930+
931+ const files = e . dataTransfer . files ;
932+ if ( files && files . length > 0 ) {
933+ await processFile ( files [ 0 ] ) ;
934+ }
935+ } ;
936+
869937 const logo =
870938 title === 'ISO 27001' ? (
871939 < div className = "h-16 w-16 flex items-center justify-center" >
@@ -962,102 +1030,169 @@ function ComplianceFramework({
9621030
9631031 { /* File Upload Section - Only show when status is "compliant" */ }
9641032 { isEnabled && status === 'compliant' && (
965- < div className = "space-y-2 border-t pt-4" >
1033+ < div className = "mt-4 border-t pt-4" >
9661034 < input
9671035 ref = { fileInputRef }
9681036 type = "file"
9691037 accept = ".pdf,application/pdf"
9701038 className = "hidden"
9711039 onChange = { async ( e ) => {
9721040 const file = e . target . files ?. [ 0 ] ;
973- if ( ! file ) return ;
974-
975- if ( file . type !== 'application/pdf' && ! file . name . toLowerCase ( ) . endsWith ( '.pdf' ) ) {
976- toast . error ( 'Please upload a PDF file' ) ;
977- return ;
978- }
979-
980- const MAX_FILE_SIZE = 10 * 1024 * 1024 ;
981- if ( file . size > MAX_FILE_SIZE ) {
982- toast . error ( 'File size must be less than 10MB' ) ;
983- return ;
984- }
985-
986- if ( onFileUpload ) {
987- setIsUploading ( true ) ;
988- try {
989- await onFileUpload ( file , frameworkKey ) ;
990- toast . success ( 'File uploaded successfully' ) ;
991- if ( fileInputRef . current ) {
992- fileInputRef . current . value = '' ;
993- }
994- } catch ( error ) {
995- const message = error instanceof Error ? error . message : 'Failed to upload file' ;
996- toast . error ( message ) ;
997- console . error ( 'File upload error:' , error ) ;
998- } finally {
999- setIsUploading ( false ) ;
1000- }
1041+ if ( file ) {
1042+ await processFile ( file ) ;
10011043 }
10021044 } }
10031045 disabled = { isUploading }
10041046 />
1005- < TooltipProvider delayDuration = { 100 } >
1006- < div className = "flex w-full flex-wrap items-center justify-between gap-3" >
1007- < span className = "text-sm font-medium text-muted-foreground" >
1008- Compliance Certificate
1009- </ span >
1010- < div className = "flex flex-wrap items-center gap-2" >
1011- < Tooltip >
1012- < TooltipTrigger asChild >
1013- < Button
1014- type = "button"
1015- variant = "ghost"
1016- size = "icon"
1017- onClick = { ( ) => fileInputRef . current ?. click ( ) }
1018- disabled = { isUploading }
1019- className = "flex items-center justify-center"
1020- aria-label = { fileName ? 'Change certificate' : 'Upload certificate' }
1021- >
1022- < Upload className = "h-4 w-4" />
1023- </ Button >
1024- </ TooltipTrigger >
1025- < TooltipContent >
1026- { fileName ? 'Change certificate (PDF)' : 'Upload certificate (PDF)' }
1027- </ TooltipContent >
1028- </ Tooltip >
1029- { fileName && onFilePreview && (
1030- < >
1031- < span className = "text-muted-foreground/50 text-sm" > |</ span >
1047+
1048+ { /* Section Header */ }
1049+ < h4 className = "text-sm font-semibold text-foreground mb-3" >
1050+ Compliance Certificate
1051+ </ h4 >
1052+
1053+ { /* Certificate Content */ }
1054+ { fileName ? (
1055+ /* File Uploaded State */
1056+ < div className = "rounded-lg bg-muted/40 border border-border/50 p-4 space-y-3" >
1057+ < div className = "flex items-center gap-3 animate-in fade-in-0 slide-in-from-top-1 duration-200" >
1058+ < div className = "flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10" >
1059+ < FileCheck2 className = "h-5 w-5 text-primary" />
1060+ </ div >
1061+ < div className = "flex-1 min-w-0" >
1062+ < p className = "text-sm font-medium text-foreground truncate" >
1063+ { fileName }
1064+ </ p >
1065+ < p className = "text-xs text-muted-foreground" >
1066+ Certificate uploaded
1067+ </ p >
1068+ </ div >
1069+ { onFilePreview && (
1070+ < TooltipProvider delayDuration = { 100 } >
1071+ < Tooltip >
1072+ < TooltipTrigger asChild >
1073+ < button
1074+ type = "button"
1075+ onClick = { async ( ) => {
1076+ try {
1077+ await onFilePreview ( frameworkKey ) ;
1078+ } catch ( error ) {
1079+ const message =
1080+ error instanceof Error ? error . message : 'Failed to preview certificate' ;
1081+ toast . error ( message ) ;
1082+ }
1083+ } }
1084+ className = "text-xs font-medium text-primary hover:text-primary/80 hover:underline transition-colors flex items-center gap-1"
1085+ >
1086+ < Eye className = "h-3.5 w-3.5" />
1087+ View
1088+ </ button >
1089+ </ TooltipTrigger >
1090+ < TooltipContent > Open certificate in new tab</ TooltipContent >
1091+ </ Tooltip >
1092+ </ TooltipProvider >
1093+ ) }
1094+ </ div >
1095+
1096+ { /* Action Bar */ }
1097+ < div className = "flex items-center gap-2 pt-1" >
1098+ < TooltipProvider delayDuration = { 100 } >
1099+ < Tooltip >
1100+ < TooltipTrigger asChild >
1101+ < Button
1102+ type = "button"
1103+ variant = "outline"
1104+ size = "sm"
1105+ onClick = { ( ) => fileInputRef . current ?. click ( ) }
1106+ disabled = { isUploading }
1107+ className = "h-8 gap-1.5 text-xs font-medium hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors"
1108+ >
1109+ < Upload className = "h-3.5 w-3.5" />
1110+ Replace
1111+ </ Button >
1112+ </ TooltipTrigger >
1113+ < TooltipContent > Replace current certificate (PDF)</ TooltipContent >
1114+ </ Tooltip >
1115+ </ TooltipProvider >
1116+
1117+ { onFilePreview && (
1118+ < TooltipProvider delayDuration = { 100 } >
10321119 < Tooltip >
10331120 < TooltipTrigger asChild >
10341121 < Button
10351122 type = "button"
1036- variant = "ghost "
1037- size = "icon "
1123+ variant = "outline "
1124+ size = "sm "
10381125 onClick = { async ( ) => {
10391126 try {
10401127 await onFilePreview ( frameworkKey ) ;
10411128 } catch ( error ) {
10421129 const message =
1043- error instanceof Error ? error . message : 'Failed to preview file ' ;
1130+ error instanceof Error ? error . message : 'Failed to download certificate ' ;
10441131 toast . error ( message ) ;
1045- console . error ( 'File preview error:' , error ) ;
10461132 }
10471133 } }
1048- className = "flex items-center justify-center"
1049- aria-label = "Preview certificate"
1134+ className = "h-8 gap-1.5 text-xs font-medium hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors"
10501135 >
1051- < FileText className = "h-4 w-4" />
1136+ < Download className = "h-3.5 w-3.5" />
1137+ Download
10521138 </ Button >
10531139 </ TooltipTrigger >
1054- < TooltipContent > Preview certificate</ TooltipContent >
1140+ < TooltipContent > Download certificate</ TooltipContent >
10551141 </ Tooltip >
1056- </ >
1142+ </ TooltipProvider >
10571143 ) }
10581144 </ div >
10591145 </ div >
1060- </ TooltipProvider >
1146+ ) : (
1147+ /* Empty State - Drop zone matching uploaded state height (122px) */
1148+ < div
1149+ onDragEnter = { handleDragEnter }
1150+ onDragLeave = { handleDragLeave }
1151+ onDragOver = { handleDragOver }
1152+ onDrop = { handleDrop }
1153+ onClick = { ( ) => ! isUploading && fileInputRef . current ?. click ( ) }
1154+ className = { `
1155+ relative rounded-lg bg-muted/40 border border-border/50 p-4 cursor-pointer
1156+ h-[122px] flex items-center
1157+ transition-all duration-200 ease-in-out
1158+ ${ isDragging ? 'border-primary bg-primary/5' : '' }
1159+ ${ isUploading ? 'opacity-50 cursor-not-allowed' : '' }
1160+ ` }
1161+ >
1162+ < div className = "flex items-center gap-3" >
1163+ < div className = { `
1164+ flex h-10 w-10 shrink-0 items-center justify-center rounded-lg
1165+ transition-all duration-200
1166+ ${ isDragging ? 'bg-primary/10' : 'bg-background' }
1167+ ` } >
1168+ < Upload className = { `
1169+ h-5 w-5 transition-all duration-200
1170+ ${ isDragging ? 'text-primary scale-110' : 'text-muted-foreground' }
1171+ ` } />
1172+ </ div >
1173+ < div className = "flex-1 min-w-0" >
1174+ < p className = { `
1175+ text-sm font-medium transition-colors duration-200
1176+ ${ isDragging ? 'text-primary' : 'text-foreground' }
1177+ ` } >
1178+ { isDragging ? 'Drop your certificate here' : 'Drag & drop certificate' }
1179+ </ p >
1180+ < p className = "text-xs text-muted-foreground" >
1181+ or click to browse • PDF only, max 10MB
1182+ </ p >
1183+ </ div >
1184+ </ div >
1185+
1186+ { isUploading && (
1187+ < div className = "absolute inset-0 flex items-center justify-center rounded-lg bg-background/80" >
1188+ < div className = "flex items-center gap-2 text-sm text-muted-foreground" >
1189+ < div className = "h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
1190+ Uploading...
1191+ </ div >
1192+ </ div >
1193+ ) }
1194+ </ div >
1195+ ) }
10611196 </ div >
10621197 ) }
10631198 </ CardContent >
0 commit comments