redirecter for ao3 that adds opengraph metadata
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}