this repo has no description

Implemented custom text formatting, changed and renamed route, renamed Event to ICalEvent

+147 -55
+3 -1
fresh.gen.ts
··· 5 5 import * as $_404 from "./routes/_404.tsx"; 6 6 import * as $_app from "./routes/_app.tsx"; 7 7 import * as $api_fetch_calendar_svg from "./routes/api/fetch-calendar-svg.ts"; 8 - import * as $api_fetch_calendar_text from "./routes/api/fetch-calendar-text.ts"; 8 + import * as $api_fetch_calendar_text from "./routes/api/fetch-calendar-events.ts"; 9 9 import * as $api_svg_to_png from "./routes/api/svg-to-png.ts"; 10 10 import * as $index from "./routes/index.tsx"; 11 + import * as $EventFormatter from "./islands/EventFormatter.tsx"; 11 12 import * as $FormContainer from "./islands/FormContainer.tsx"; 12 13 import * as $ResultsDisplay from "./islands/ResultsDisplay.tsx"; 13 14 import * as $URLInput from "./islands/URLInput.tsx"; ··· 24 25 "./routes/index.tsx": $index, 25 26 }, 26 27 islands: { 28 + "./islands/EventFormatter.tsx": $EventFormatter, 27 29 "./islands/FormContainer.tsx": $FormContainer, 28 30 "./islands/ResultsDisplay.tsx": $ResultsDisplay, 29 31 "./islands/URLInput.tsx": $URLInput,
+91
islands/EventFormatter.tsx
··· 1 + import { useEffect, useState } from "preact/hooks"; 2 + 3 + interface EventFormatterProps { 4 + events: ICalEvent[]; 5 + onFormattedTextChange: (formattedText: string) => void; 6 + } 7 + 8 + export default function EventFormatter( 9 + { events, onFormattedTextChange }: EventFormatterProps, 10 + ) { 11 + const defaultFormat = "- {timestamp} | **{summary}**{description}"; 12 + const [formatString, setFormatString] = useState(defaultFormat); 13 + const [formattedText, setFormattedText] = useState(""); 14 + 15 + // Format events whenever the format string or events change 16 + useEffect(() => { 17 + if (!events || events.length === 0) return; 18 + 19 + const formatted = events.map((event) => { 20 + let line = formatString; 21 + 22 + // Replace keywords with actual values 23 + const unixTimestamp = Math.floor( 24 + new Date(event.start).getTime() / 1000, 25 + ); 26 + line = line.replace(/{timestamp}/g, `<t:${unixTimestamp}:F>`); 27 + line = line.replace(/{summary}/g, event.summary || ""); 28 + line = line.replace( 29 + /{description}/g, 30 + event.description ? ` | ${event.description}` : "", 31 + ); 32 + line = line.replace( 33 + /{location}/g, 34 + event.location ? ` | ${event.location}` : "", 35 + ); 36 + 37 + return line; 38 + }).join("\n"); 39 + 40 + setFormattedText(formatted); 41 + onFormattedTextChange(formatted); 42 + }, [formatString, events]); 43 + 44 + return ( 45 + <div className="event-formatter"> 46 + <div className="format-input-container"> 47 + <label htmlFor="format-input">Custom Format Pattern:</label> 48 + <input 49 + id="format-input" 50 + type="text" 51 + value={formatString} 52 + onChange={(e) => 53 + setFormatString((e.target as HTMLInputElement).value)} 54 + className="format-input" 55 + placeholder="Enter format pattern" 56 + /> 57 + <p className="format-help"> 58 + Available keywords: {"{timestamp}"}, {"{summary}"},{" "} 59 + {"{description}"}, {"{location}"} 60 + </p> 61 + </div> 62 + 63 + <style> 64 + {` 65 + .event-formatter { 66 + margin-bottom: 15px; 67 + } 68 + 69 + .format-input-container { 70 + margin-bottom: 10px; 71 + } 72 + 73 + .format-input { 74 + width: 100%; 75 + padding: 8px; 76 + margin-top: 5px; 77 + border: 1px solid #ddd; 78 + border-radius: 4px; 79 + font-family: monospace; 80 + } 81 + 82 + .format-help { 83 + margin-top: 5px; 84 + font-size: 12px; 85 + color: #666; 86 + } 87 + `} 88 + </style> 89 + </div> 90 + ); 91 + }
+22 -21
islands/FormContainer.tsx
··· 16 16 const [isSubmitting, setIsSubmitting] = useState<boolean>(false); 17 17 const [formError, setFormError] = useState<string | null>(null); 18 18 const [svgData, setSvgData] = useState<string | null>(null); 19 - const [textData, setTextData] = useState<string | null>(null); 19 + const [events, setEvents] = useState<ICalEvent[] | null>(null); 20 20 const [urlHistory, setUrlHistory] = useState<string[]>([]); 21 21 22 22 // Initialize with default week if dates are null ··· 67 67 68 68 setIsSubmitting(true); 69 69 setSvgData(null); 70 - setTextData(null); 70 + setEvents(null); 71 71 72 72 // Save the valid URL to history 73 73 const updatedHistory = saveUrlToHistory(url, urlHistory); 74 74 setUrlHistory(updatedHistory); 75 75 76 76 try { 77 - // Fetch SVG data 78 - const svgParams = new URLSearchParams({ 77 + // Fetch events data 78 + const eventsParams = new URLSearchParams({ 79 79 url: url, 80 80 startDate: startDate.toISOString(), 81 81 endDate: endDate.toISOString(), 82 82 }); 83 - const svgResponse = await fetch( 84 - `/api/fetch-calendar-svg?${svgParams.toString()}`, 83 + 84 + const eventsResponse = await fetch( 85 + `/api/fetch-calendar-events?${eventsParams.toString()}`, 85 86 ); 86 87 87 - if (!svgResponse.ok) { 88 - throw new Error(`SVG fetch failed: ${svgResponse.statusText}`); 88 + if (!eventsResponse.ok) { 89 + throw new Error( 90 + `Events fetch failed: ${eventsResponse.statusText}`, 91 + ); 89 92 } 90 93 91 - const svgContent = await svgResponse.text(); 92 - setSvgData(svgContent); 94 + const jsonData = await eventsResponse.json(); 95 + setEvents(jsonData.events); 93 96 94 - // Fetch text data 95 - const textParams = new URLSearchParams({ 97 + // Fetch SVG data 98 + const svgParams = new URLSearchParams({ 96 99 url: url, 97 100 startDate: startDate.toISOString(), 98 101 endDate: endDate.toISOString(), 99 102 }); 100 - const textResponse = await fetch( 101 - `/api/fetch-calendar-text?${textParams.toString()}`, 103 + const svgResponse = await fetch( 104 + `/api/fetch-calendar-svg?${svgParams.toString()}`, 102 105 ); 103 106 104 - if (!textResponse.ok) { 105 - throw new Error( 106 - `Text fetch failed: ${textResponse.statusText}`, 107 - ); 107 + if (!svgResponse.ok) { 108 + throw new Error(`SVG fetch failed: ${svgResponse.statusText}`); 108 109 } 109 110 110 - const jsonData = await textResponse.json(); 111 - setTextData(jsonData.result); 111 + const svgContent = await svgResponse.text(); 112 + setSvgData(svgContent); 112 113 } catch (error) { 113 114 console.error("Error fetching data:", error); 114 115 setFormError( ··· 154 155 </form> 155 156 156 157 {/* Results Section */} 157 - <ResultsDisplay svgData={svgData} textData={textData} /> 158 + <ResultsDisplay svgData={svgData} events={events} /> 158 159 159 160 <style> 160 161 {`
+15 -7
islands/ResultsDisplay.tsx
··· 1 1 import { useState } from "preact/hooks"; 2 + import EventFormatter from "./EventFormatter.tsx"; 2 3 3 4 interface ResultsDisplayProps { 4 5 svgData: string | null; 5 - textData: string | null; 6 + events: ICalEvent[] | null; 6 7 } 7 8 8 9 export default function ResultsDisplay( 9 - { svgData, textData }: ResultsDisplayProps, 10 + { svgData, events }: ResultsDisplayProps, 10 11 ) { 11 12 const [copyMessage, setCopyMessage] = useState<string | null>(null); 12 13 const [downloadStatus, setDownloadStatus] = useState<string | null>(null); 14 + const [formattedText, setFormattedText] = useState<string>(""); 13 15 14 16 // Copy text to clipboard 15 17 const copyTextToClipboard = async () => { 16 - if (!textData) return; 18 + if (!formattedText) return; 17 19 18 20 try { 19 - await navigator.clipboard.writeText(textData); 21 + await navigator.clipboard.writeText(formattedText); 20 22 setCopyMessage("Copied to clipboard!"); 21 23 setTimeout(() => setCopyMessage(null), 2000); 22 24 } catch (_err) { ··· 69 71 } 70 72 }; 71 73 72 - if (!svgData && !textData) return null; 74 + if (!svgData && !events) return null; 73 75 74 76 return ( 75 77 <div className="results-container"> 76 78 <h2>Calendar Results</h2> 77 79 78 - {textData && ( 80 + {events && events.length > 0 && ( 79 81 <div className="text-container"> 80 82 <div className="result-header"> 81 83 <h3>Event Listing</h3> ··· 87 89 {copyMessage || "Copy to Clipboard"} 88 90 </button> 89 91 </div> 90 - <pre className="text-output">{textData}</pre> 92 + 93 + <EventFormatter 94 + events={events} 95 + onFormattedTextChange={setFormattedText} 96 + /> 97 + 98 + <pre className="text-output">{formattedText}</pre> 91 99 </div> 92 100 )} 93 101
+2 -11
routes/api/fetch-calendar-text.ts routes/api/fetch-calendar-events.ts
··· 8 8 try { 9 9 const { events } = await processCalendarRequest(url); 10 10 11 - let formattedText = ""; 12 - for (const event of events) { 13 - const unixTimestamp = Math.floor(event.start.getTime() / 1000); 14 - formattedText += 15 - `\n- <t:${unixTimestamp}:F> | **${event.summary}**`; 16 - if (event.description) { 17 - formattedText += ` | ${event.description}`; 18 - } 19 - } 20 - 11 + // Return the raw events array as JSON 21 12 return new Response( 22 - JSON.stringify({ result: formattedText.trim() }), 13 + JSON.stringify({ events }), 23 14 { 24 15 headers: { "Content-Type": "application/json" }, 25 16 },
+6 -15
utils/calendarUtils.ts
··· 6 6 import startOfDay from "https://deno.land/x/date_fns@v2.22.1/startOfDay/index.ts"; 7 7 import endOfDay from "https://deno.land/x/date_fns@v2.22.1/endOfDay/index.ts"; 8 8 9 - export interface Event { 10 - start: Date; 11 - end: Date; 12 - summary?: string; 13 - location?: string; 14 - description?: string; 15 - timezone?: string; 16 - } 17 - 18 9 export interface TwitchEvent { 19 10 id: string; 20 11 start_time: string; ··· 30 21 31 22 export async function fetchCalendar( 32 23 icalUrl: string | undefined, 33 - ): Promise<Event[]> { 24 + ): Promise<ICalEvent[]> { 34 25 if (!icalUrl) { 35 26 throw new Error("Must provide an iCalendar URL."); 36 27 } ··· 71 62 } 72 63 73 64 // deno-lint-ignore no-explicit-any 74 - function parseICalEvents(icalEvents: Record<string, any>): Event[] { 65 + function parseICalEvents(icalEvents: Record<string, any>): ICalEvent[] { 75 66 return Object.values(icalEvents) 76 67 .filter((event) => event.type === "VEVENT") 77 68 .map((event) => { ··· 90 81 } 91 82 92 83 export function filterEvents( 93 - events: Event[], 84 + events: ICalEvent[], 94 85 weekStart: Date, 95 86 weekEnd: Date, 96 - ): Event[] { 87 + ): ICalEvent[] { 97 88 // Ensure weekStart and weekEnd are in UTC for comparison 98 89 const utcWeekStart = new Date(weekStart.toISOString()); 99 90 const utcWeekEnd = new Date(weekEnd.toISOString()); ··· 105 96 return weeklyEvents; 106 97 } 107 98 108 - export function filterTwitchEvents<T extends Event | TwitchEvent>( 99 + export function filterTwitchEvents<T extends ICalEvent | TwitchEvent>( 109 100 events: T[], 110 101 currentDate: Date = new Date(), 111 102 ): T[] { ··· 125 116 126 117 export async function processCalendarRequest( 127 118 url: URL, 128 - ): Promise<{ events: Event[]; startDate: Date; endDate: Date }> { 119 + ): Promise<{ events: ICalEvent[]; startDate: Date; endDate: Date }> { 129 120 const targetUrl = url.searchParams.get("url"); 130 121 131 122 const startDateParam = url.searchParams.get("startDate");
+8
utils/types.d.ts
··· 1 + interface ICalEvent { 2 + start: Date; 3 + end: Date; 4 + summary?: string; 5 + location?: string; 6 + description?: string; 7 + timezone?: string; 8 + }