11import { useState , useEffect , useRef } from 'react' ;
2+ import type { FormEvent } from 'react' ;
23import { setGitHubToken , clearGitHubToken , hasGitHubToken } from '../utils/github-api' ;
34import { useToast } from '../hooks/useToast' ;
45import { useSoundStore } from '../store/soundStore' ;
6+ import { useStrudelJson } from '../hooks/useStrudelJson' ;
7+ import type { CustomUrlRepository } from '../types/strudel' ;
58
69interface SettingsProps {
710 onClose : ( ) => void ;
@@ -15,16 +18,22 @@ export function Settings({ onClose }: SettingsProps) {
1518 const [ showClearBlocklistConfirm , setShowClearBlocklistConfirm ] = useState ( false ) ;
1619 const [ hasTokenState , setHasTokenState ] = useState ( false ) ;
1720 const [ blockedReposList , setBlockedReposList ] = useState < string [ ] > ( [ ] ) ;
21+ const [ customUrlsList , setCustomUrlsList ] = useState < CustomUrlRepository [ ] > ( [ ] ) ;
22+ const [ newCustomUrl , setNewCustomUrl ] = useState ( '' ) ;
23+ const [ newCustomUrlName , setNewCustomUrlName ] = useState ( '' ) ;
24+ const [ isLoadingCustomUrl , setIsLoadingCustomUrl ] = useState ( false ) ;
25+ const [ customUrlError , setCustomUrlError ] = useState < string | null > ( null ) ;
1826
1927 // Refs to store functions without causing re-renders
2028 const storeRef = useRef ( {
21- unblockRepository : ( key : string ) => {
22- console . info ( 'Placeholder unblockRepository called with key:' , key ) ;
23- } ,
24- clearBlocklist : ( ) => { }
29+ unblockRepository : ( _key : string ) => { } ,
30+ clearBlocklist : ( ) => { } ,
31+ removeCustomUrl : ( _url : string ) => { } ,
32+ getCustomUrls : ( ) => [ ] as CustomUrlRepository [ ]
2533 } ) ;
2634
2735 const toast = useToast ( ) ;
36+ const { loadCustomUrlRepository } = useStrudelJson ( ) ;
2837
2938 // Initialize state on mount only
3039 useEffect ( ( ) => {
@@ -35,17 +44,23 @@ export function Settings({ onClose }: SettingsProps) {
3544 const store = useSoundStore . getState ( ) ;
3645 storeRef . current = {
3746 unblockRepository : store . unblockRepository ,
38- clearBlocklist : store . clearBlocklist
47+ clearBlocklist : store . clearBlocklist ,
48+ removeCustomUrl : store . removeCustomUrl ,
49+ getCustomUrls : store . getCustomUrls
3950 } ;
4051
4152 // Get blocked repos
4253 setBlockedReposList ( store . getBlockedRepos ( ) ) ;
4354
55+ // Get custom URLs
56+ setCustomUrlsList ( store . getCustomUrls ( ) ) ;
57+
4458 // Subscribe to blockedRepos changes
4559 const unsubscribe = useSoundStore . subscribe (
4660 ( state ) => {
47- // When blockedRepos changes , update our local state
61+ // When blockedRepos or customUrls change , update our local state
4862 setBlockedReposList ( state . getBlockedRepos ( ) ) ;
63+ setCustomUrlsList ( state . getCustomUrls ( ) ) ;
4964 }
5065 ) ;
5166
@@ -78,6 +93,46 @@ export function Settings({ onClose }: SettingsProps) {
7893 toast . success ( 'Blocklist cleared' ) ;
7994 setShowClearBlocklistConfirm ( false ) ;
8095 } ;
96+
97+ const handleAddCustomUrl = async ( e : FormEvent ) => {
98+ e . preventDefault ( ) ;
99+
100+ if ( ! newCustomUrl . trim ( ) || ! newCustomUrlName . trim ( ) ) {
101+ toast . error ( 'Please enter both URL and name' ) ;
102+ return ;
103+ }
104+
105+ // Validate URL format
106+ try {
107+ new URL ( newCustomUrl ) ;
108+ } catch ( _err ) {
109+ toast . error ( 'Please enter a valid URL' ) ;
110+ return ;
111+ }
112+
113+ setIsLoadingCustomUrl ( true ) ;
114+ setCustomUrlError ( null ) ;
115+
116+ try {
117+ // Load the repository (it will be automatically saved)
118+ await loadCustomUrlRepository ( newCustomUrl , newCustomUrlName ) ;
119+
120+ toast . success ( `Added custom URL: ${ newCustomUrlName } ` ) ;
121+ setNewCustomUrl ( '' ) ;
122+ setNewCustomUrlName ( '' ) ;
123+ } catch ( err ) {
124+ const message = err instanceof Error ? err . message : 'Failed to load custom URL' ;
125+ setCustomUrlError ( message ) ;
126+ toast . error ( message ) ;
127+ } finally {
128+ setIsLoadingCustomUrl ( false ) ;
129+ }
130+ } ;
131+
132+ const handleRemoveCustomUrl = ( url : string ) => {
133+ storeRef . current . removeCustomUrl ( url ) ;
134+ toast . info ( 'Custom URL removed' ) ;
135+ } ;
81136
82137 return (
83138 < div className = "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 overflow-y-auto" >
@@ -206,6 +261,92 @@ export function Settings({ onClose }: SettingsProps) {
206261 </ >
207262 ) }
208263 </ div >
264+
265+ { /* Custom URLs Section */ }
266+ < div className = "border-t pt-4" >
267+ < h3 className = "font-semibold text-gray-800 mb-3" >
268+ 🔗 Custom URLs ({ customUrlsList . length } )
269+ </ h3 >
270+ < p className = "text-sm text-gray-600 mb-3" >
271+ Add custom strudel.json URLs from any source, including localhost for development.
272+ </ p >
273+
274+ < form onSubmit = { handleAddCustomUrl } className = "mb-4 space-y-3" >
275+ < div >
276+ < label htmlFor = "customUrl" className = "block text-sm font-medium text-gray-700 mb-1" >
277+ Strudel JSON URL
278+ </ label >
279+ < input
280+ id = "customUrl"
281+ type = "text"
282+ value = { newCustomUrl }
283+ onChange = { ( e ) => setNewCustomUrl ( e . target . value ) }
284+ placeholder = "https://example.com/strudel.json"
285+ className = "w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent"
286+ disabled = { isLoadingCustomUrl }
287+ />
288+ </ div >
289+
290+ < div >
291+ < label htmlFor = "customUrlName" className = "block text-sm font-medium text-gray-700 mb-1" >
292+ Display Name
293+ </ label >
294+ < input
295+ id = "customUrlName"
296+ type = "text"
297+ value = { newCustomUrlName }
298+ onChange = { ( e ) => setNewCustomUrlName ( e . target . value ) }
299+ placeholder = "My Custom Strudel"
300+ className = "w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-transparent"
301+ disabled = { isLoadingCustomUrl }
302+ />
303+ </ div >
304+
305+ { customUrlError && (
306+ < div className = "text-sm text-red-600" >
307+ Error: { customUrlError }
308+ </ div >
309+ ) }
310+
311+ < button
312+ type = "submit"
313+ disabled = { isLoadingCustomUrl || ! newCustomUrl . trim ( ) || ! newCustomUrlName . trim ( ) }
314+ className = "px-3 py-1.5 bg-purple-600 text-white text-sm rounded hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
315+ >
316+ { isLoadingCustomUrl ? 'Loading...' : '➕ Add Custom URL' }
317+ </ button >
318+ </ form >
319+
320+ { customUrlsList . length === 0 ? (
321+ < div className = "bg-gray-50 border border-gray-200 rounded p-3 text-sm text-gray-600 text-center" >
322+ No custom URLs added
323+ </ div >
324+ ) : (
325+ < div className = "bg-gray-50 border border-gray-200 rounded max-h-40 overflow-y-auto" >
326+ { customUrlsList . map ( ( item ) => (
327+ < div
328+ key = { item . url }
329+ className = "flex items-center justify-between p-2 border-b border-gray-200 last:border-b-0 hover:bg-gray-100"
330+ >
331+ < div className = "flex-1 overflow-hidden" >
332+ < div className = "text-sm font-medium text-gray-800 truncate" >
333+ { item . name }
334+ </ div >
335+ < div className = "text-xs text-gray-500 truncate font-mono" >
336+ { item . url }
337+ </ div >
338+ </ div >
339+ < button
340+ onClick = { ( ) => handleRemoveCustomUrl ( item . url ) }
341+ className = "ml-2 px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700 transition-colors flex-shrink-0"
342+ >
343+ Remove
344+ </ button >
345+ </ div >
346+ ) ) }
347+ </ div >
348+ ) }
349+ </ div >
209350
210351 < div className = "flex gap-3 pt-4 border-t" >
211352 < button
0 commit comments