this repo has no description

Added generic LocalStorageInput island

+268 -334
+2
fresh.gen.ts
··· 10 10 import * as $index from "./routes/index.tsx"; 11 11 import * as $EventFormatter from "./islands/EventFormatter.tsx"; 12 12 import * as $FormContainer from "./islands/FormContainer.tsx"; 13 + import * as $LocalStorageInput from "./islands/LocalStorageInput.tsx"; 13 14 import * as $ResultsDisplay from "./islands/ResultsDisplay.tsx"; 14 15 import * as $URLInput from "./islands/URLInput.tsx"; 15 16 import * as $WeekPicker from "./islands/WeekPicker.tsx"; ··· 27 28 islands: { 28 29 "./islands/EventFormatter.tsx": $EventFormatter, 29 30 "./islands/FormContainer.tsx": $FormContainer, 31 + "./islands/LocalStorageInput.tsx": $LocalStorageInput, 30 32 "./islands/ResultsDisplay.tsx": $ResultsDisplay, 31 33 "./islands/URLInput.tsx": $URLInput, 32 34 "./islands/WeekPicker.tsx": $WeekPicker,
+15 -202
islands/EventFormatter.tsx
··· 1 - import { useEffect, useRef, useState } from "preact/hooks"; 2 - import { IS_BROWSER } from "$fresh/runtime.ts"; 1 + import { useEffect, useState } from "preact/hooks"; 2 + import LocalStorageInput from "./LocalStorageInput.tsx"; 3 3 4 4 interface EventFormatterProps { 5 5 events: ICalEvent[]; ··· 11 11 ) { 12 12 const defaultFormat = "- {timestamp} | **{summary}**{description}"; 13 13 const [formatString, setFormatString] = useState(defaultFormat); 14 - const [formatHistory, setFormatHistory] = useState<string[]>([]); 15 - const [showFormatHistory, setShowFormatHistory] = useState<boolean>(false); 16 - const formatInputRef = useRef<HTMLInputElement>(null); 17 - const formatDropdownRef = useRef<HTMLDivElement>(null); 18 - 19 - // Load format history from localStorage on component mount 20 - useEffect(() => { 21 - if (IS_BROWSER) { 22 - // Get saved format from localStorage or use default 23 - const savedFormat = localStorage.getItem("calendarFormatCurrent"); 24 - if (savedFormat) { 25 - setFormatString(savedFormat); 26 - } 27 - 28 - // Get format history from localStorage 29 - const storedFormats = localStorage.getItem("calendarFormatHistory"); 30 - if (storedFormats) { 31 - setFormatHistory(JSON.parse(storedFormats)); 32 - } 33 - } 34 - }, []); 35 - 36 - // Add click outside listener to close dropdown 37 - useEffect(() => { 38 - if (IS_BROWSER && showFormatHistory) { 39 - const handleClickOutside = (event: MouseEvent) => { 40 - if ( 41 - formatDropdownRef.current && 42 - !formatDropdownRef.current.contains(event.target as Node) && 43 - formatInputRef.current && 44 - !formatInputRef.current.contains(event.target as Node) 45 - ) { 46 - setShowFormatHistory(false); 47 - } 48 - }; 49 - 50 - document.addEventListener("mousedown", handleClickOutside); 51 - return () => { 52 - document.removeEventListener("mousedown", handleClickOutside); 53 - }; 54 - } 55 - }, [showFormatHistory]); 56 14 57 15 // Format events whenever the format string or events change 58 16 useEffect(() => { ··· 82 40 onFormattedTextChange(formatted); 83 41 }, [formatString, events]); 84 42 85 - // Save format string to localStorage when it changes 86 - useEffect(() => { 87 - if (IS_BROWSER && formatString) { 88 - localStorage.setItem("calendarFormatCurrent", formatString); 89 - } 90 - }, [formatString]); 91 - 92 - // Handle format input changes 93 - const handleFormatChange = (e: Event) => { 94 - const target = e.target as HTMLInputElement; 95 - setFormatString(target.value); 96 - }; 97 - 98 - // Handle format input focus 99 - const handleFormatFocus = () => { 100 - setShowFormatHistory(true); 101 - }; 102 - 103 - // Handle format submission 104 - const handleFormatSubmit = (e: Event) => { 105 - e.preventDefault(); 106 - 107 - if (!formatString || formatString === defaultFormat) return; 108 - 109 - // Add to history if not already present 110 - if (!formatHistory.includes(formatString)) { 111 - const newHistory = [formatString, ...formatHistory].slice(0, 10); // Keep last 10 formats 112 - setFormatHistory(newHistory); 113 - 114 - // Save to localStorage 115 - if (IS_BROWSER) { 116 - localStorage.setItem( 117 - "calendarFormatHistory", 118 - JSON.stringify(newHistory), 119 - ); 120 - } 121 - } 122 - 123 - setShowFormatHistory(false); 124 - }; 125 - 126 - // Handle format selection from history 127 - const selectFormat = (selectedFormat: string) => { 128 - setFormatString(selectedFormat); 129 - setShowFormatHistory(false); 130 - // Focus the input after selection 131 - if (formatInputRef.current) { 132 - formatInputRef.current.focus(); 133 - } 134 - }; 135 - 136 43 return ( 137 44 <div className="event-formatter"> 138 - <form 139 - onSubmit={handleFormatSubmit} 140 - className="format-input-container" 141 - > 142 - <label htmlFor="format-input">Custom Format Pattern:</label> 143 - <div className="input-with-button"> 144 - <input 145 - id="format-input" 146 - type="text" 147 - value={formatString} 148 - onInput={handleFormatChange} 149 - onFocus={handleFormatFocus} 150 - className="format-input" 151 - placeholder="Enter format pattern" 152 - ref={formatInputRef} 153 - /> 154 - <button type="submit" className="save-format-button"> 155 - Save 156 - </button> 157 - </div> 158 - 159 - <p className="format-help"> 160 - Available keywords: {"{timestamp}"}, {"{summary}"},{" "} 161 - {"{description}"}, {"{location}"} 162 - </p> 163 - 164 - {showFormatHistory && formatHistory.length > 0 && ( 165 - <div 166 - className="format-history-dropdown" 167 - ref={formatDropdownRef} 168 - > 169 - {formatHistory.map((historyFormat, index) => ( 170 - <div 171 - key={index} 172 - className="format-history-item" 173 - onClick={() => selectFormat(historyFormat)} 174 - > 175 - {historyFormat} 176 - </div> 177 - ))} 178 - </div> 179 - )} 180 - </form> 45 + <LocalStorageInput 46 + id="format-input" 47 + label="Custom Format Pattern:" 48 + value={formatString} 49 + onChange={setFormatString} 50 + placeholder="Enter format pattern" 51 + storageKey="calendarFormatCurrent" 52 + historyStorageKey="calendarFormatHistory" 53 + maxHistoryItems={10} 54 + helpText="Available keywords: {timestamp}, {summary}, {description}, {location}" 55 + className="format-input-wrapper" 56 + /> 181 57 182 58 <style> 183 59 {` ··· 185 61 margin-bottom: 15px; 186 62 } 187 63 188 - .format-input-container { 189 - position: relative; 190 - margin-bottom: 10px; 191 - } 192 - 193 - .input-with-button { 194 - display: flex; 195 - gap: 8px; 196 - margin-top: 5px; 197 - } 198 - 199 - .format-input { 200 - flex: 1; 201 - padding: 8px; 202 - border: 1px solid #ddd; 203 - border-radius: 4px; 64 + .format-input-wrapper :global(.storage-input) { 204 65 font-family: monospace; 205 - } 206 - 207 - .save-format-button { 208 - padding: 8px 12px; 209 - background-color: #1976d2; 210 - color: white; 211 - border: none; 212 - border-radius: 4px; 213 - cursor: pointer; 214 - font-size: 14px; 215 - transition: background-color 0.2s; 216 - } 217 - 218 - .save-format-button:hover { 219 - background-color: #1565c0; 220 - } 221 - 222 - .format-help { 223 - margin-top: 5px; 224 - font-size: 12px; 225 - color: #666; 226 - } 227 - 228 - .format-history-dropdown { 229 - position: absolute; 230 - top: 100%; 231 - left: 0; 232 - width: 100%; 233 - max-height: 200px; 234 - overflow-y: auto; 235 - background-color: white; 236 - border: 1px solid #ccc; 237 - border-top: none; 238 - border-radius: 0 0 4px 4px; 239 - z-index: 10; 240 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 241 - } 242 - 243 - .format-history-item { 244 - padding: 8px 12px; 245 - cursor: pointer; 246 - white-space: nowrap; 247 - overflow: hidden; 248 - text-overflow: ellipsis; 249 - } 250 - 251 - .format-history-item:hover { 252 - background-color: #f5f5f5; 253 66 } 254 67 `} 255 68 </style>
+235
islands/LocalStorageInput.tsx
··· 1 + import { useEffect, useRef, useState } from "preact/hooks"; 2 + import { IS_BROWSER } from "$fresh/runtime.ts"; 3 + 4 + export interface LocalStorageInputProps { 5 + id: string; 6 + label: string; 7 + value: string; 8 + onChange: (value: string) => void; 9 + onSubmit?: (value: string) => void; 10 + placeholder?: string; 11 + storageKey: string; 12 + historyStorageKey: string; 13 + maxHistoryItems?: number; 14 + helpText?: string; 15 + className?: string; 16 + } 17 + 18 + export default function LocalStorageInput({ 19 + id, 20 + label, 21 + value, 22 + onChange, 23 + onSubmit, 24 + placeholder = "", 25 + storageKey, 26 + historyStorageKey, 27 + maxHistoryItems = 10, 28 + helpText, 29 + className = "", 30 + }: LocalStorageInputProps) { 31 + const [history, setHistory] = useState<string[]>([]); 32 + const [showHistory, setShowHistory] = useState<boolean>(false); 33 + const inputRef = useRef<HTMLInputElement>(null); 34 + const dropdownRef = useRef<HTMLDivElement>(null); 35 + 36 + // Load history from localStorage on component mount 37 + useEffect(() => { 38 + if (IS_BROWSER) { 39 + // Get saved value from localStorage 40 + const savedValue = localStorage.getItem(storageKey); 41 + if (savedValue && savedValue !== value) { 42 + onChange(savedValue); 43 + } 44 + 45 + // Get history from localStorage 46 + const storedHistory = localStorage.getItem(historyStorageKey); 47 + if (storedHistory) { 48 + setHistory(JSON.parse(storedHistory)); 49 + } 50 + } 51 + }, []); 52 + 53 + // Add click outside listener to close dropdown 54 + useEffect(() => { 55 + if (IS_BROWSER && showHistory) { 56 + const handleClickOutside = (event: MouseEvent) => { 57 + if ( 58 + dropdownRef.current && 59 + !dropdownRef.current.contains(event.target as Node) && 60 + inputRef.current && 61 + !inputRef.current.contains(event.target as Node) 62 + ) { 63 + setShowHistory(false); 64 + } 65 + }; 66 + 67 + document.addEventListener("mousedown", handleClickOutside); 68 + return () => { 69 + document.removeEventListener("mousedown", handleClickOutside); 70 + }; 71 + } 72 + }, [showHistory]); 73 + 74 + // Save value to localStorage when it changes 75 + useEffect(() => { 76 + if (IS_BROWSER && value) { 77 + localStorage.setItem(storageKey, value); 78 + } 79 + }, [value, storageKey]); 80 + 81 + // Handle input changes 82 + const handleChange = (e: Event) => { 83 + const target = e.target as HTMLInputElement; 84 + onChange(target.value); 85 + }; 86 + 87 + // Handle input focus 88 + const handleFocus = () => { 89 + setShowHistory(true); 90 + }; 91 + 92 + // Handle form submission 93 + const handleSubmit = (e: Event) => { 94 + e.preventDefault(); 95 + 96 + if (!value) return; 97 + 98 + // Add to history if not already present 99 + if (!history.includes(value)) { 100 + const newHistory = [value, ...history].slice(0, maxHistoryItems); 101 + setHistory(newHistory); 102 + 103 + // Save to localStorage 104 + if (IS_BROWSER) { 105 + localStorage.setItem( 106 + historyStorageKey, 107 + JSON.stringify(newHistory), 108 + ); 109 + } 110 + } 111 + 112 + setShowHistory(false); 113 + 114 + if (onSubmit) { 115 + onSubmit(value); 116 + } 117 + }; 118 + 119 + // Handle selection from history 120 + const selectItem = (selectedItem: string) => { 121 + onChange(selectedItem); 122 + setShowHistory(false); 123 + // Focus the input after selection 124 + if (inputRef.current) { 125 + inputRef.current.focus(); 126 + } 127 + }; 128 + 129 + return ( 130 + <div className={`local-storage-input-container ${className}`}> 131 + <form onSubmit={handleSubmit}> 132 + <label htmlFor={id}>{label}</label> 133 + <div className="input-with-button"> 134 + <input 135 + id={id} 136 + type="text" 137 + value={value} 138 + onInput={handleChange} 139 + onFocus={handleFocus} 140 + placeholder={placeholder} 141 + className="storage-input" 142 + ref={inputRef} 143 + /> 144 + <button type="submit" className="save-button">Save</button> 145 + </div> 146 + 147 + {helpText && <p className="help-text">{helpText}</p>} 148 + 149 + {showHistory && history.length > 0 && ( 150 + <div className="history-dropdown" ref={dropdownRef}> 151 + {history.map((historyItem, index) => ( 152 + <div 153 + key={index} 154 + className="history-item" 155 + onClick={() => selectItem(historyItem)} 156 + > 157 + {historyItem} 158 + </div> 159 + ))} 160 + </div> 161 + )} 162 + </form> 163 + 164 + <style> 165 + {` 166 + .local-storage-input-container { 167 + position: relative; 168 + margin-bottom: 10px; 169 + } 170 + 171 + .input-with-button { 172 + display: flex; 173 + gap: 8px; 174 + margin-top: 5px; 175 + } 176 + 177 + .storage-input { 178 + flex: 1; 179 + padding: 8px; 180 + border: 1px solid #ddd; 181 + border-radius: 4px; 182 + } 183 + 184 + .save-button { 185 + padding: 8px 12px; 186 + background-color: #1976d2; 187 + color: white; 188 + border: none; 189 + border-radius: 4px; 190 + cursor: pointer; 191 + font-size: 14px; 192 + transition: background-color 0.2s; 193 + } 194 + 195 + .save-button:hover { 196 + background-color: #1565c0; 197 + } 198 + 199 + .help-text { 200 + margin-top: 5px; 201 + font-size: 12px; 202 + color: #666; 203 + } 204 + 205 + .history-dropdown { 206 + position: absolute; 207 + top: 100%; 208 + left: 0; 209 + width: 100%; 210 + max-height: 200px; 211 + overflow-y: auto; 212 + background-color: white; 213 + border: 1px solid #ccc; 214 + border-top: none; 215 + border-radius: 0 0 4px 4px; 216 + z-index: 10; 217 + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 218 + } 219 + 220 + .history-item { 221 + padding: 8px 12px; 222 + cursor: pointer; 223 + white-space: nowrap; 224 + overflow: hidden; 225 + text-overflow: ellipsis; 226 + } 227 + 228 + .history-item:hover { 229 + background-color: #f5f5f5; 230 + } 231 + `} 232 + </style> 233 + </div> 234 + ); 235 + }
+16 -132
islands/URLInput.tsx
··· 1 - import { useEffect, useRef, useState } from "preact/hooks"; 2 - import { IS_BROWSER } from "$fresh/runtime.ts"; 1 + import { useEffect } from "preact/hooks"; 2 + import LocalStorageInput from "./LocalStorageInput.tsx"; 3 3 4 4 interface URLInputProps { 5 5 value: string; ··· 8 8 } 9 9 10 10 export default function URLInput({ value, onChange, onError }: URLInputProps) { 11 - const [urlHistory, setUrlHistory] = useState<string[]>([]); 12 - const [showUrlHistory, setShowUrlHistory] = useState<boolean>(false); 13 - const urlInputRef = useRef<HTMLInputElement>(null); 14 - const urlDropdownRef = useRef<HTMLDivElement>(null); 15 - 16 - // Load URL history from localStorage on component mount 11 + // Handle URL validation 17 12 useEffect(() => { 18 - if (IS_BROWSER) { 19 - const storedUrls = localStorage.getItem("calendarUrlHistory"); 20 - if (storedUrls) { 21 - setUrlHistory(JSON.parse(storedUrls)); 22 - } 23 - } 24 - }, []); 25 - 26 - // Add click outside listener to close dropdown 27 - useEffect(() => { 28 - if (IS_BROWSER && showUrlHistory) { 29 - const handleClickOutside = (event: MouseEvent) => { 30 - if ( 31 - urlDropdownRef.current && 32 - !urlDropdownRef.current.contains(event.target as Node) && 33 - urlInputRef.current && 34 - !urlInputRef.current.contains(event.target as Node) 35 - ) { 36 - setShowUrlHistory(false); 37 - } 38 - }; 39 - 40 - document.addEventListener("mousedown", handleClickOutside); 41 - return () => { 42 - document.removeEventListener("mousedown", handleClickOutside); 43 - }; 44 - } 45 - }, [showUrlHistory]); 46 - 47 - // Handle URL input changes 48 - const handleUrlChange = (e: Event) => { 49 - const target = e.target as HTMLInputElement; 50 - const newValue = target.value; 51 - 52 - onChange(newValue); 53 - 54 - // Validate URL format 55 13 if (onError) { 56 - if (newValue && !newValue.match(/^(https?:\/\/|webcal:\/\/)/i)) { 14 + if (value && !value.match(/^(https?:\/\/|webcal:\/\/)/i)) { 57 15 onError("URL must begin with https://, http://, or webcal://"); 58 16 } else { 59 17 onError(null); 60 18 } 61 19 } 62 - }; 63 - 64 - // Handle URL input focus 65 - const handleUrlFocus = () => { 66 - setShowUrlHistory(true); 67 - }; 68 - 69 - // Handle URL selection from history 70 - const selectUrl = (selectedUrl: string) => { 71 - onChange(selectedUrl); 72 - setShowUrlHistory(false); 73 - // Focus the input after selection 74 - if (urlInputRef.current) { 75 - urlInputRef.current.focus(); 76 - } 77 - }; 20 + }, [value, onError]); 78 21 79 22 return ( 80 - <div className="url-input-container"> 81 - <label htmlFor="url-input">Enter iCalendar URL:</label> 82 - <input 83 - id="url-input" 84 - type="text" 85 - value={value} 86 - onInput={handleUrlChange} 87 - onFocus={handleUrlFocus} 88 - placeholder="https://, http://, or webcal://" 89 - className="url-input" 90 - ref={urlInputRef} 91 - /> 92 - 93 - {showUrlHistory && urlHistory.length > 0 && ( 94 - <div className="url-history-dropdown" ref={urlDropdownRef}> 95 - {urlHistory.map((historyUrl, index) => ( 96 - <div 97 - key={index} 98 - className="url-history-item" 99 - onClick={() => selectUrl(historyUrl)} 100 - > 101 - {historyUrl} 102 - </div> 103 - ))} 104 - </div> 105 - )} 106 - 107 - <style> 108 - {` 109 - .url-input-container { 110 - position: relative; 111 - } 112 - 113 - .url-input { 114 - width: 100%; 115 - padding: 8px; 116 - font-size: 16px; 117 - border: 1px solid #ccc; 118 - border-radius: 4px; 119 - } 120 - 121 - .url-history-dropdown { 122 - position: absolute; 123 - top: 100%; 124 - left: 0; 125 - width: 100%; 126 - max-height: 200px; 127 - overflow-y: auto; 128 - background-color: white; 129 - border: 1px solid #ccc; 130 - border-top: none; 131 - border-radius: 0 0 4px 4px; 132 - z-index: 10; 133 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 134 - } 135 - 136 - .url-history-item { 137 - padding: 8px 12px; 138 - cursor: pointer; 139 - white-space: nowrap; 140 - overflow: hidden; 141 - text-overflow: ellipsis; 142 - } 143 - 144 - .url-history-item:hover { 145 - background-color: #f5f5f5; 146 - } 147 - `} 148 - </style> 149 - </div> 23 + <LocalStorageInput 24 + id="url-input" 25 + label="Enter iCalendar URL:" 26 + value={value} 27 + onChange={onChange} 28 + placeholder="https://, http://, or webcal://" 29 + storageKey="calendarUrlCurrent" 30 + historyStorageKey="calendarUrlHistory" 31 + maxHistoryItems={10} 32 + className="url-input-wrapper" 33 + /> 150 34 ); 151 35 }