@@ -14,6 +14,7 @@ import {
1414 AlertCircle ,
1515 Loader2 ,
1616 Database ,
17+ Globe ,
1718} from 'lucide-react' ;
1819import type { SocialPlatform , UserProfile , BlockData } from '../types' ;
1920import { AVATAR_PLACEHOLDER } from '../constants' ;
@@ -37,7 +38,7 @@ type SettingsModalProps = {
3738 onBlocksChange ?: ( blocks : BlockData [ ] ) => void ;
3839} ;
3940
40- type TabType = 'general' | 'social' | 'analytics' | 'json' ;
41+ type TabType = 'general' | 'social' | 'seo' | ' analytics' | 'json' ;
4142
4243const SettingsModal : React . FC < SettingsModalProps > = ( {
4344 isOpen,
@@ -256,6 +257,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
256257 const tabs : { id : TabType ; label : string ; icon : React . ReactNode } [ ] = [
257258 { id : 'general' , label : 'General' , icon : < User size = { 16 } /> } ,
258259 { id : 'social' , label : 'Social' , icon : < Share2 size = { 16 } /> } ,
260+ { id : 'seo' , label : 'SEO & Social Sharing' , icon : < Globe size = { 16 } /> } ,
259261 { id : 'analytics' , label : 'Analytics' , icon : < BarChart3 size = { 16 } /> } ,
260262 { id : 'json' , label : 'Raw JSON' , icon : < Code size = { 16 } /> } ,
261263 ] ;
@@ -902,6 +904,284 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
902904 </ section >
903905 ) }
904906
907+ { /* SEO TAB */ }
908+ { activeTab === 'seo' && (
909+ < section className = "space-y-6" >
910+ < div >
911+ < h3 className = "text-xs font-bold text-gray-400 uppercase tracking-wider mb-3" >
912+ OpenGraph & Social Sharing
913+ </ h3 >
914+ < p className = "text-sm text-gray-500 mb-4" >
915+ Configure how your page appears when shared on social media platforms.
916+ </ p >
917+ </ div >
918+
919+ { /* OG Title */ }
920+ < div className = "space-y-2" >
921+ < label htmlFor = "og-title" className = "block text-sm font-medium text-gray-700" >
922+ Title
923+ </ label >
924+ < input
925+ id = "og-title"
926+ type = "text"
927+ value = { profile . openGraph ?. title || '' }
928+ onChange = { ( e ) =>
929+ setProfile ( {
930+ ...profile ,
931+ openGraph : { ...profile . openGraph , title : e . target . value } ,
932+ } )
933+ }
934+ placeholder = { profile . name || 'Your page title' }
935+ className = "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
936+ />
937+ < p className = "text-xs text-gray-400" > Leave empty to use your profile name</ p >
938+ </ div >
939+
940+ { /* OG Description */ }
941+ < div className = "space-y-2" >
942+ < label
943+ htmlFor = "og-description"
944+ className = "block text-sm font-medium text-gray-700"
945+ >
946+ Description
947+ </ label >
948+ < textarea
949+ id = "og-description"
950+ value = { profile . openGraph ?. description || '' }
951+ onChange = { ( e ) => {
952+ const value = e . target . value . slice ( 0 , 200 ) ;
953+ setProfile ( {
954+ ...profile ,
955+ openGraph : { ...profile . openGraph , description : value } ,
956+ } ) ;
957+ } }
958+ placeholder = "A brief description of your page..."
959+ rows = { 3 }
960+ className = "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
961+ />
962+ < p className = "text-xs text-gray-400" >
963+ { profile . openGraph ?. description ?. length || 0 } /200 characters
964+ </ p >
965+ </ div >
966+
967+ { /* OG Image */ }
968+ < div className = "space-y-2" >
969+ < label htmlFor = "og-image" className = "block text-sm font-medium text-gray-700" >
970+ Image
971+ </ label >
972+ < div className = "flex gap-2" >
973+ < input
974+ id = "og-image"
975+ type = "url"
976+ value = {
977+ profile . openGraph ?. image ?. startsWith ( 'data:' )
978+ ? ''
979+ : profile . openGraph ?. image || ''
980+ }
981+ onChange = { ( e ) =>
982+ setProfile ( {
983+ ...profile ,
984+ openGraph : { ...profile . openGraph , image : e . target . value } ,
985+ } )
986+ }
987+ placeholder = "https://example.com/image.png"
988+ className = "flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
989+ />
990+ < label
991+ className = "px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg cursor-pointer transition-colors flex items-center gap-2 text-sm font-medium text-gray-700"
992+ title = "Upload image"
993+ >
994+ < Upload size = { 16 } />
995+ < span > Upload</ span >
996+ < input
997+ type = "file"
998+ accept = "image/*"
999+ aria-label = "Upload OpenGraph image"
1000+ className = "hidden"
1001+ onChange = { ( e ) => {
1002+ const file = e . target . files ?. [ 0 ] ;
1003+ if ( file ) {
1004+ const reader = new FileReader ( ) ;
1005+ reader . onload = ( ) => {
1006+ setProfile ( {
1007+ ...profile ,
1008+ openGraph : {
1009+ ...profile . openGraph ,
1010+ image : reader . result as string ,
1011+ } ,
1012+ } ) ;
1013+ } ;
1014+ reader . readAsDataURL ( file ) ;
1015+ }
1016+ } }
1017+ />
1018+ </ label >
1019+ { profile . openGraph ?. image && (
1020+ < button
1021+ type = "button"
1022+ aria-label = "Remove image"
1023+ onClick = { ( ) =>
1024+ setProfile ( {
1025+ ...profile ,
1026+ openGraph : { ...profile . openGraph , image : undefined } ,
1027+ } )
1028+ }
1029+ className = "px-3 py-2 bg-red-50 hover:bg-red-100 rounded-lg text-red-600 transition-colors"
1030+ title = "Remove image"
1031+ >
1032+ < X size = { 16 } />
1033+ </ button >
1034+ ) }
1035+ </ div >
1036+ < p className = "text-xs text-gray-400" > Recommended size: 1200x630 pixels</ p >
1037+ { profile . openGraph ?. image && (
1038+ < div className = "mt-2 p-2 bg-gray-50 rounded-lg" >
1039+ < img
1040+ src = { profile . openGraph . image }
1041+ alt = "OG Preview"
1042+ className = "w-full max-w-xs h-auto rounded border border-gray-200"
1043+ onError = { ( e ) => {
1044+ ( e . target as HTMLImageElement ) . style . display = 'none' ;
1045+ } }
1046+ />
1047+ </ div >
1048+ ) }
1049+ </ div >
1050+
1051+ { /* Site Name */ }
1052+ < div className = "space-y-2" >
1053+ < label
1054+ htmlFor = "og-site-name"
1055+ className = "block text-sm font-medium text-gray-700"
1056+ >
1057+ Site Name
1058+ </ label >
1059+ < input
1060+ id = "og-site-name"
1061+ type = "text"
1062+ value = { profile . openGraph ?. siteName || '' }
1063+ onChange = { ( e ) =>
1064+ setProfile ( {
1065+ ...profile ,
1066+ openGraph : { ...profile . openGraph , siteName : e . target . value } ,
1067+ } )
1068+ }
1069+ placeholder = "My Bento"
1070+ className = "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
1071+ />
1072+ </ div >
1073+
1074+ { /* Twitter Section */ }
1075+ < div className = "pt-4 border-t border-gray-100" >
1076+ < h4 className = "text-xs font-bold text-gray-400 uppercase tracking-wider mb-3" >
1077+ Twitter / X
1078+ </ h4 >
1079+
1080+ { /* Twitter Handle */ }
1081+ < div className = "space-y-2 mb-4" >
1082+ < label
1083+ htmlFor = "twitter-handle"
1084+ className = "block text-sm font-medium text-gray-700"
1085+ >
1086+ Twitter Handle
1087+ </ label >
1088+ < div className = "flex" >
1089+ < span className = "inline-flex items-center px-3 border border-r-0 border-gray-200 rounded-l-lg bg-gray-50 text-gray-500 text-sm" >
1090+ @
1091+ </ span >
1092+ < input
1093+ id = "twitter-handle"
1094+ type = "text"
1095+ value = { profile . openGraph ?. twitterHandle || '' }
1096+ onChange = { ( e ) => {
1097+ const value = e . target . value . replace ( / ^ @ / , '' ) ;
1098+ setProfile ( {
1099+ ...profile ,
1100+ openGraph : { ...profile . openGraph , twitterHandle : value } ,
1101+ } ) ;
1102+ } }
1103+ placeholder = "username"
1104+ className = "flex-1 px-3 py-2 border border-gray-200 rounded-r-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
1105+ />
1106+ </ div >
1107+ </ div >
1108+
1109+ { /* Twitter Card Type */ }
1110+ < div className = "space-y-2" >
1111+ < label
1112+ htmlFor = "twitter-card-type"
1113+ className = "block text-sm font-medium text-gray-700"
1114+ >
1115+ Card Type
1116+ </ label >
1117+ < select
1118+ id = "twitter-card-type"
1119+ value = { profile . openGraph ?. twitterCardType || 'summary_large_image' }
1120+ onChange = { ( e ) =>
1121+ setProfile ( {
1122+ ...profile ,
1123+ openGraph : {
1124+ ...profile . openGraph ,
1125+ twitterCardType : e . target . value as 'summary' | 'summary_large_image' ,
1126+ } ,
1127+ } )
1128+ }
1129+ className = "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white"
1130+ >
1131+ < option value = "summary_large_image" > Large Image Card</ option >
1132+ < option value = "summary" > Summary Card</ option >
1133+ </ select >
1134+ < p className = "text-xs text-gray-400" >
1135+ Large image cards show a bigger preview image
1136+ </ p >
1137+ </ div >
1138+ </ div >
1139+
1140+ { /* Live Preview */ }
1141+ < div className = "pt-4 border-t border-gray-100" >
1142+ < h4 className = "text-xs font-bold text-gray-400 uppercase tracking-wider mb-3" >
1143+ Preview
1144+ </ h4 >
1145+ < p className = "text-sm text-gray-500 mb-4" >
1146+ How your link will appear when shared on social media
1147+ </ p >
1148+ < div className = "border border-gray-200 rounded-xl overflow-hidden bg-white max-w-md" >
1149+ { /* Image Preview */ }
1150+ < div
1151+ className = "w-full bg-gray-100 flex items-center justify-center"
1152+ style = { { aspectRatio : '1200/630' } }
1153+ >
1154+ { profile . openGraph ?. image ? (
1155+ < img
1156+ src = { profile . openGraph . image }
1157+ alt = "Preview"
1158+ className = "w-full h-full object-cover"
1159+ onError = { ( e ) => {
1160+ ( e . target as HTMLImageElement ) . style . display = 'none' ;
1161+ } }
1162+ />
1163+ ) : (
1164+ < div className = "text-gray-400 text-sm" > No image set</ div >
1165+ ) }
1166+ </ div >
1167+ { /* Content Preview */ }
1168+ < div className = "p-3" >
1169+ < p className = "text-xs text-gray-500 mb-1" >
1170+ { profile . openGraph ?. siteName || 'yourdomain.com' }
1171+ </ p >
1172+ < p className = "font-semibold text-gray-900 text-sm leading-tight mb-1 line-clamp-1" >
1173+ { profile . openGraph ?. title || profile . name || 'Page Title' }
1174+ </ p >
1175+ < p className = "text-xs text-gray-500 line-clamp-2" >
1176+ { profile . openGraph ?. description ||
1177+ 'Your page description will appear here...' }
1178+ </ p >
1179+ </ div >
1180+ </ div >
1181+ </ div >
1182+ </ section >
1183+ ) }
1184+
9051185 { /* ANALYTICS TAB */ }
9061186 { activeTab === 'analytics' && (
9071187 < section className = "space-y-6" >
0 commit comments