redirecter for ao3 that adds opengraph metadata
at main 9.5 kB view raw
1"use client" 2 3import { useEffect, useState } from "react" 4import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls" 5import themes from "@/lib/themes.js" 6import baseFonts from "@/lib/baseFonts.js" 7import titleFonts from "@/lib/titleFonts.js" 8import defaults from "@/lib/ogdefaults.js" 9import ao3CanonicalUrls from "@/lib/ao3Canonical.js" 10 11export default function Generator() { 12 const [url, setUrl] = useState('') 13 const [workData, setWorkData] = useState(null) 14 const [addr, setAddr] = useState('') 15 const [imgData, setImgData] = useState(null) 16 const [props, setProps] = useState(defaults) 17 const [domain, setDomain] = useState('') 18 19 const updateProp = (name, value) => { 20 const newProps = props 21 newProps[name] = value 22 setProps(newProps) 23 updateData() 24 } 25 26 const updateData = async () => { 27 if (url === '') return 28 const workMatch = /\/works\/(?<workId>[0-9]+)(?:\/chapters\/(?<chapterId>[0-9]+))?$/ 29 const seriesMatch = /\/series\/(?<seriesId>[0-9]+)$/ 30 const baseurl = /(?<domain>https:\/\/[a-z0-9\-\.]+)\// 31 const domainMatch = url.match(baseurl) 32 if (!domainMatch) return 33 setDomain(domainMatch.groups.domain) 34 const domainParam = domain && !ao3CanonicalUrls.includes(domain) ? `?archive=${domain}` : '' 35 if (workMatch.test(url)) { 36 const match = url.match(workMatch) 37 const resp = match.groups.chapterId ? await fetch(`/api/works/${match.groups.workId}/chapters/${match.groups.chapterId}${domainParam}`) : await fetch(`/api/works/${match.groups.workId}${domainParam}`) 38 if (!resp) return 39 const data = await resp.json() 40 setAddr(match.groups.chapterId ? `works/${match.groups.workId}/chapters/${match.groups.chapterId}` : `works/${match.groups.workId}`) 41 setWorkData(data) 42 } else if (seriesMatch.test(url)) { 43 const match = url.match(seriesMatch) 44 const resp = await fetch(`/api/series/${match.groups.seriesId}${domainParam}`) 45 if (!resp) return 46 const data = await resp.json() 47 setAddr(`series/${match.groups.seriesId}`) 48 setWorkData(data) 49 } 50 } 51 52 useEffect(() => { 53 updateData() 54 }, [url, props.theme, props.baseFont, props.titleFont]) 55 56 useEffect(() => { 57 const fn = async () => { 58 if (!addr) return; 59 if (workData.locked) { 60 const image = await fetch('/locked') 61 if (image.status !== 200) return; 62 const imageBlob = await image.blob() 63 const reader = new FileReader() 64 reader.onloadend = () => { 65 setImgData(reader.result) 66 } 67 return 68 } 69 const params = new URLSearchParams(props) 70 const image = await fetch(`/${addr}/preview?${params.toString()}&archive=${domain}`) 71 if (image.status !== 200) return; 72 const imageBlob = await image.blob() 73 const reader = new FileReader() 74 reader.onloadend = () => { 75 setImgData(reader.result) 76 } 77 reader.readAsDataURL(imageBlob) 78 } 79 fn() 80 }, [workData]) 81 return ( 82 <main> 83 <form id="generator"> 84 <div className="input-field"> 85 <label htmlFor="url">Work/Series URL</label> 86 <input type="text" name="url" id="url" onChange={e => setUrl(e.target.value)} /> 87 </div> 88 <details><summary>Style Settings</summary> 89 <div className="cols"> 90 <div className="col"> 91 <div className="input-field"> 92 <label htmlFor="theme">Theme</label> 93 <select name="theme" id="theme" defaultValue={props.theme} onChange={e => updateProp("theme", e.target.value)}> 94 {Object.entries(themes).sort().map((th, i) => { 95 return ( 96 <option key={i} value={th[0]}>{th[1].name}</option> 97 ) 98 })} 99 </select> 100 </div> 101 <div className="input-field"> 102 <label htmlFor="baseFont">Base Font</label> 103 <select name="baseFont" id="baseFont" defaultValue={props.baseFont} onChange={e => updateProp("baseFont", e.target.value)}> 104 {Object.entries(baseFonts).sort().map((bf, i) => { 105 106 return ( 107 <option key={i} value={bf[0]}>{bf[1].displayName}</option> 108 ) 109 })} 110 </select> 111 </div> 112 <div className="input-field"> 113 <label htmlFor="titleFont">Title Font</label> 114 <select name="titleFont" id="titleFont" defaultValue={props.titleFont} onChange={e => updateProp("titleFont", e.target.value)}> 115 {Object.entries({...titleFonts, ...baseFonts}).sort().map((tf, i) => { 116 return ( 117 <option key={i} value={tf[0]}>{tf[1].displayName}</option> 118 ) 119 })} 120 </select> 121 </div> 122 </div> 123 <div className="input-field"> 124 <label htmlFor="features">Features:</label> 125 <ul> 126 <li><label><input type="checkbox" name="features[]" value="category" defaultChecked={props.category} onChange={e => updateProp(e.target.value, e.target.checked)} /> Category</label></li> 127 <li><label><input type="checkbox" name="features[]" value="rating" defaultChecked={props.rating} onChange={e => updateProp(e.target.value, e.target.checked)} /> Rating</label></li> 128 <li><label><input type="checkbox" name="features[]" value="warnings" defaultChecked={props.warnings} onChange={e => updateProp(e.target.value, e.target.checked)} /> Archive Warnings</label></li> 129 <li><label><input type="checkbox" name="features[]" value="charTags" defaultChecked={props.charTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Character Tags</label></li> 130 <li><label><input type="checkbox" name="features[]" value="relTags" defaultChecked={props.relTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Relationship Tags</label></li> 131 <li><label><input type="checkbox" name="features[]" value="freeTags" defaultChecked={props.freeTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Free Tags</label></li> 132 <li><label><input type="checkbox" name="features[]" value="summary" defaultChecked={props.summary} onChange={e => updateProp(e.target.value, e.target.checked)} /> Summary</label></li> 133 <li><label><input type="checkbox" name="features[]" value="wordcount" defaultChecked={props.wordcount} onChange={e => updateProp(e.target.value, e.target.checked)} /> Wordcount</label></li> 134 <li><label><input type="checkbox" name="features[]" value="chapters" defaultChecked={props.chapters} onChange={e => updateProp(e.target.value, e.target.checked)} /> Chapters</label></li> 135 <li><label><input type="checkbox" name="features[]" value="postedAt" defaultChecked={props.postedAt} onChange={e => updateProp(e.target.value, e.target.checked)} /> Posted Date</label></li> 136 <li><label><input type="checkbox" name="features[]" value="updatedAt" defaultChecked={props.updatedAt} onChange={e => updateProp(e.target.value, e.target.checked)} /> Updated Date</label></li> 137 </ul> 138 </div> 139 <div className="col"> 140 <div className="input-field"> 141 <label htmlFor="displayOptions">Display Options</label> 142 <ul> 143 <li><label><input type="checkbox" name="uppercaseTitle" value="uppercaseTitle" defaultChecked={props.uppercaseTitle} onChange={e => updateProp(e.target.value, e.target.checked)} /> Uppercase Title?</label></li> 144 <li><label><input type="checkbox" name="uppercaseChapterName" value="uppercaseChapterName" defaultChecked={props.uppercaseChapterName} onChange={e => updateProp(e.target.value, e.target.checked)} /> Uppercase Chapter Name?</label></li> 145 </ul> 146 </div> 147 <div className="input-field"> 148 <label htmlFor="summaryOptions">Summary Type</label> 149 <ul> 150 <li><label><input type="radio" name="summaryType" value="basic" defaultChecked={props.summaryType === 'basic'} onChange={e => updateProp(e.target.name, e.target.value)} /> Story Summary</label></li> 151 <li><label><input type="radio" name="summaryType" defaultChecked={props.summaryType === 'chapter'} value="chapter" onChange={e => updateProp(e.target.name, e.target.value)} /> Chapter Summary (if available)</label></li> 152 <li><label><input type="radio" name="summaryType" defaultChecked={props.summaryType === 'custom'} value="custom" onChange={e => updateProp(e.target.name, e.target.value)} /> Custom Summary</label></li> 153 </ul> 154 {props.summaryType === 'custom' && ( 155 <div className="input-field"> 156 <label htmlFor="customSummary">Custom Summary</label> 157 <textarea name="customSummary" id="customSummary" onBlur={e => updateProp(e.target.name, e.target.value)}></textarea> 158 </div> 159 )} 160 </div> 161 </div> 162 </div> 163 </details> 164 </form> 165 {imgData && imgData !== '' && ( 166 <div id="output"> 167 <img id="output-img" src={imgData} /> 168 </div> 169 )} 170 </main> 171 ); 172}