+24
-31
src/app/generator/page.js
+24
-31
src/app/generator/page.js
···
5
5
import baseFonts from "@/lib/baseFonts.js"
6
6
import titleFonts from "@/lib/titleFonts.js"
7
7
import styles from "./page.module.css"
8
+
import defaults from "@/lib/ogdefaults.js"
8
9
9
10
export default function Generator() {
10
11
const [url, setUrl] = useState('')
11
12
const [workData, setWorkData] = useState(null)
12
13
const [addr, setAddr] = useState('')
13
14
const [imgData, setImgData] = useState(null)
14
-
const [props, setProps] = useState({
15
-
theme: 'ao3',
16
-
baseFont: 'bricolagegrotesque',
17
-
titleFont: 'stacksansnotch',
18
-
category: true,
19
-
rating: true,
20
-
warnings: false,
21
-
charTags: false,
22
-
relTags: false,
23
-
freeTags: false,
24
-
summary: true,
25
-
wordcount: true,
26
-
chapters: true,
27
-
postedAt: true,
28
-
updatedAt: false,
29
-
summaryType: 'basic',
30
-
customSummary: ''
31
-
})
15
+
const [props, setProps] = useState(defaults)
32
16
33
17
const updateProp = (name, value) => {
34
18
const newProps = props
···
132
116
<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>
133
117
</ul>
134
118
</div>
135
-
<div className="input-field">
136
-
<label htmlFor="summaryOptions">Summary Type</label>
137
-
<ul>
138
-
<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>
139
-
<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>
140
-
<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>
141
-
</ul>
142
-
{props.summaryType === 'custom' && (
143
-
<div className="input-field">
144
-
<label htmlFor="customSummary">Custom Summary</label>
145
-
<textarea name="customSummary" id="customSummary" onChange={e => updateProp(e.target.name, e.target.value)}></textarea>
146
-
</div>
147
-
)}
119
+
<div className="col">
120
+
<div className="input-field">
121
+
<label htmlFor="displayOptions">Display Options</label>
122
+
<ul>
123
+
<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>
124
+
<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>
125
+
</ul>
126
+
</div>
127
+
<div className="input-field">
128
+
<label htmlFor="summaryOptions">Summary Type</label>
129
+
<ul>
130
+
<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>
131
+
<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>
132
+
<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>
133
+
</ul>
134
+
{props.summaryType === 'custom' && (
135
+
<div className="input-field">
136
+
<label htmlFor="customSummary">Custom Summary</label>
137
+
<textarea name="customSummary" id="customSummary" onChange={e => updateProp(e.target.name, e.target.value)}></textarea>
138
+
</div>
139
+
)}
140
+
</div>
148
141
</div>
149
142
</div>
150
143
</details>
+1
-20
src/app/series/[seriesId]/opengraph-image.jsx
+1
-20
src/app/series/[seriesId]/opengraph-image.jsx
···
3
3
import OGImage from "@/lib/ogimage.js"
4
4
import baseFonts from "@/lib/baseFonts.js"
5
5
import titleFonts from "@/lib/titleFonts.js"
6
+
import defaults from "@/lib/ogdefaults.js"
6
7
7
8
export const size = {
8
9
width: 1600,
9
10
height: 900,
10
11
}
11
12
export const alt = 'fixAO3'
12
-
13
13
export const contentType = 'image/webp'
14
-
15
-
const defaults = new URLSearchParams({
16
-
theme: 'ao3',
17
-
baseFont: 'bricolagegrotesque',
18
-
titleFont: 'stacksansnotch',
19
-
category: true,
20
-
rating: true,
21
-
warnings: false,
22
-
charTags: false,
23
-
relTags: false,
24
-
freeTags: false,
25
-
summary: true,
26
-
wordcount: true,
27
-
chapters: true,
28
-
postedAt: true,
29
-
updatedAt: false,
30
-
summaryType: 'basic',
31
-
customSummary: ''
32
-
})
33
14
34
15
export default async function Image({params, searchParams}) {
35
16
const { seriesId } = await params
+3
-1
src/app/series/[seriesId]/preview/route.js
+3
-1
src/app/series/[seriesId]/preview/route.js
···
1
1
import { getSeries } from "@fujocoded/ao3.js"
2
+
import querystring from 'node:querystring'
2
3
import sanitizeData from "@/lib/sanitizeData.js"
3
4
import OGImage from "@/lib/ogimage.js"
4
5
import baseFonts from "@/lib/baseFonts.js"
···
13
14
14
15
export async function GET(req, ctx) {
15
16
const { seriesId } = await ctx.params
16
-
const props = await req.nextUrl.searchParams
17
+
const p = await req.nextUrl.searchParams
18
+
const props = querystring.parse(p.toString())
17
19
const addr = `series/${seriesId}`
18
20
const data = await getSeries({seriesId: seriesId})
19
21
const imageParams = await sanitizeData({type: 'series', data: data, props: props})
+1
-20
src/app/works/[workId]/chapters/[chapterId]/opengraph-image.jsx
+1
-20
src/app/works/[workId]/chapters/[chapterId]/opengraph-image.jsx
···
3
3
import OGImage from "@/lib/ogimage.js"
4
4
import baseFonts from "@/lib/baseFonts.js"
5
5
import titleFonts from "@/lib/titleFonts.js"
6
+
import defaults from "@/lib/ogdefaults.js"
6
7
7
8
export const size = {
8
9
width: 1600,
9
10
height: 900,
10
11
}
11
12
export const alt = 'fixAO3'
12
-
13
13
export const contentType = 'image/webp'
14
-
15
-
const defaults = new URLSearchParams({
16
-
theme: 'ao3',
17
-
baseFont: 'bricolagegrotesque',
18
-
titleFont: 'stacksansnotch',
19
-
category: true,
20
-
rating: true,
21
-
warnings: false,
22
-
charTags: false,
23
-
relTags: false,
24
-
freeTags: false,
25
-
summary: true,
26
-
wordcount: true,
27
-
chapters: true,
28
-
postedAt: true,
29
-
updatedAt: false,
30
-
summaryType: 'basic',
31
-
customSummary: ''
32
-
})
33
14
34
15
export default async function Image({params, searchParams}) {
35
16
const { workId, chapterId } = await params
+3
-1
src/app/works/[workId]/chapters/[chapterId]/preview/route.js
+3
-1
src/app/works/[workId]/chapters/[chapterId]/preview/route.js
···
1
1
import { getWork } from "@fujocoded/ao3.js"
2
+
import querystring from 'node:querystring'
2
3
import sanitizeData from "@/lib/sanitizeData.js"
3
4
import OGImage from "@/lib/ogimage.js"
4
5
import baseFonts from "@/lib/baseFonts.js"
···
13
14
14
15
export async function GET(req, ctx) {
15
16
const { workId, chapterId } = await ctx.params
16
-
const props = await req.nextUrl.searchParams
17
+
const p = await req.nextUrl.searchParams
18
+
const props = querystring.parse(p.toString())
17
19
const addr = `works/${workId}/chapters/${chapterId}`
18
20
const data = await getWork({workId: workId, chapterId: chapterId})
19
21
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
+1
-20
src/app/works/[workId]/opengraph-image.jsx
+1
-20
src/app/works/[workId]/opengraph-image.jsx
···
3
3
import OGImage from "@/lib/ogimage.js"
4
4
import baseFonts from "@/lib/baseFonts.js"
5
5
import titleFonts from "@/lib/titleFonts.js"
6
+
import defaults from "@/lib/ogdefaults.js"
6
7
7
8
export const size = {
8
9
width: 1600,
9
10
height: 900,
10
11
}
11
12
export const alt = 'fixAO3'
12
-
13
13
export const contentType = 'image/webp'
14
-
15
-
const defaults = new URLSearchParams({
16
-
theme: 'ao3',
17
-
baseFont: 'bricolagegrotesque',
18
-
titleFont: 'stacksansnotch',
19
-
category: true,
20
-
rating: true,
21
-
warnings: false,
22
-
charTags: false,
23
-
relTags: false,
24
-
freeTags: false,
25
-
summary: true,
26
-
wordcount: true,
27
-
chapters: true,
28
-
postedAt: true,
29
-
updatedAt: false,
30
-
summaryType: 'basic',
31
-
customSummary: ''
32
-
})
33
14
34
15
export default async function Image({params}) {
35
16
const { workId } = await params
+3
-1
src/app/works/[workId]/preview/route.js
+3
-1
src/app/works/[workId]/preview/route.js
···
1
1
import { getWork } from "@fujocoded/ao3.js"
2
+
import querystring from 'node:querystring'
2
3
import sanitizeData from "@/lib/sanitizeData.js"
3
4
import OGImage from "@/lib/ogimage.js"
4
5
import baseFonts from "@/lib/baseFonts.js"
···
13
14
14
15
export async function GET(req, ctx) {
15
16
const { workId } = await ctx.params
16
-
const props = await req.nextUrl.searchParams
17
+
const p = await req.nextUrl.searchParams
18
+
const props = querystring.parse(p.toString())
17
19
const addr = `works/${workId}`
18
20
const data = await getWork({workId: workId})
19
21
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
+22
src/lib/ogdefaults.js
+22
src/lib/ogdefaults.js
···
1
+
const defaults = {
2
+
theme: 'ao3',
3
+
baseFont: 'bricolagegrotesque',
4
+
titleFont: 'stacksansnotch',
5
+
category: true,
6
+
rating: true,
7
+
warnings: false,
8
+
charTags: false,
9
+
relTags: false,
10
+
freeTags: false,
11
+
summary: true,
12
+
wordcount: true,
13
+
chapters: true,
14
+
postedAt: true,
15
+
updatedAt: false,
16
+
uppercaseTitle: false,
17
+
uppercaseChapterName: false,
18
+
summaryType: 'basic',
19
+
customSummary: ''
20
+
}
21
+
22
+
export default defaults
+25
-23
src/lib/ogimage.js
+25
-23
src/lib/ogimage.js
···
55
55
{image.topLine}
56
56
</div>
57
57
58
-
{image.props.get('rating') === 'true' && image.rating === 'E' && (<Explicit fg={theme.background} bg={theme.accent} width={28} height={28} />)}
59
-
{image.props.get('rating') === 'true' && image.rating === 'M' && (<Mature fg={theme.background} bg={theme.accent} width={28} height={28} />)}
60
-
{image.props.get('rating') === 'true' && image.rating === 'T' && (<Teen fg={theme.background} bg={theme.accent} width={28} height={28} />)}
61
-
{image.props.get('rating') === 'true' && image.rating === 'G' && (<General fg={theme.background} bg={theme.accent} width={28} height={28} />)}
62
-
{image.props.get('rating') === 'true' && image.rating === 'NR' && (<NotRated fg={theme.background} bg={theme.accent} width={28} height={28} />)}
58
+
{image.props.rating && image.rating === 'E' && (<Explicit fg={theme.background} bg={theme.accent} width={28} height={28} />)}
59
+
{image.props.rating && image.rating === 'M' && (<Mature fg={theme.background} bg={theme.accent} width={28} height={28} />)}
60
+
{image.props.rating && image.rating === 'T' && (<Teen fg={theme.background} bg={theme.accent} width={28} height={28} />)}
61
+
{image.props.rating && image.rating === 'G' && (<General fg={theme.background} bg={theme.accent} width={28} height={28} />)}
62
+
{image.props.rating && image.rating === 'NR' && (<NotRated fg={theme.background} bg={theme.accent} width={28} height={28} />)}
63
63
64
-
{image.props.get('warnings') === 'true' && image.warning === 'NW' && (<NoWarnings fg={theme.background} bg={theme.accent2} width={28} height={28} />)}
65
-
{image.props.get('warnings') === 'true' && image.warning === 'CNTW' && (<ChoseNotToWarn fg={theme.background} bg={theme.accent2} width={28} height={28} />)}
66
-
{image.props.get('warnings') === 'true' && image.warning === 'W' && (<Warnings fg={theme.background} bg={theme.accent2} width={28} height={28} />)}
64
+
{image.props.warnings && image.warning === 'NW' && (<NoWarnings fg={theme.background} bg={theme.accent2} width={28} height={28} />)}
65
+
{image.props.warnings && image.warning === 'CNTW' && (<ChoseNotToWarn fg={theme.background} bg={theme.accent2} width={28} height={28} />)}
66
+
{image.props.warnings && image.warning === 'W' && (<Warnings fg={theme.background} bg={theme.accent2} width={28} height={28} />)}
67
67
68
-
{image.props.get('category') === 'true' && image.category === 'F' && (<Yuri fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
69
-
{image.props.get('category') === 'true' && image.category === 'M' && (<Yaoi fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
70
-
{image.props.get('category') === 'true' && image.category === 'FM' && (<Het fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
71
-
{image.props.get('category') === 'true' && image.category === 'G' && (<Gen fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
72
-
{image.props.get('category') === 'true' && image.category === 'MX' && (<MultiShip fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
73
-
{image.props.get('category') === 'true' && image.category === 'O' && (<OtherShip fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
68
+
{image.props.category && image.category === 'F' && (<Yuri fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
69
+
{image.props.category && image.category === 'M' && (<Yaoi fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
70
+
{image.props.category && image.category === 'FM' && (<Het fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
71
+
{image.props.category && image.category === 'G' && (<Gen fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
72
+
{image.props.category && image.category === 'MX' && (<MultiShip fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
73
+
{image.props.category && image.category === 'O' && (<OtherShip fg={theme.background} bg={theme.accent3} width={28} height={28} />)}
74
74
</div>
75
75
<div
76
76
style={{
···
78
78
justifyContent: "center",
79
79
fontFamily: titleFont,
80
80
fontWeight: "bold",
81
-
color: theme.color
81
+
color: theme.color,
82
+
textTransform: (image.props.uppercaseTitle ? 'uppercase' : 'none')
82
83
}}
83
84
>
84
85
{image.titleLine}
···
94
95
>
95
96
{`by ${image.authorLine}`}
96
97
</div>
97
-
<div
98
+
{image.chapterLine !== '' && (<div
98
99
style={{
99
100
fontStyle: "italic",
100
101
fontSize: 36,
101
102
fontFamily: titleFont,
102
103
display: "flex",
103
104
justifyContent: "center",
104
-
color: theme.color
105
+
color: theme.color,
106
+
textTransform: (image.props.uppercaseChapterName ? 'uppercase' : 'none')
105
107
}}
106
108
>
107
109
{image.chapterLine}
108
-
</div>
110
+
</div>)}
109
111
</div>
110
112
<div
111
113
style={{
···
118
120
alignItems: "flex-end"
119
121
}}
120
122
>
121
-
{image.props.get("charTags") === 'true' && (<div
123
+
{image.props.charTags && (<div
122
124
style={{
123
125
display: "flex",
124
126
flexWrap: "wrap",
···
141
143
</span>
142
144
))}
143
145
</div>)}
144
-
{image.props.get("relTags") === 'true' && (<div
146
+
{image.props.relTags && (<div
145
147
style={{
146
148
display: "flex",
147
149
flexWrap: "wrap",
···
164
166
</span>
165
167
))}
166
168
</div>)}
167
-
{image.props.get("freeTags") === 'true' && (<div
169
+
{image.props.freeTags && (<div
168
170
style={{
169
171
display: "flex",
170
172
flexWrap: "wrap",
···
187
189
</span>
188
190
))}
189
191
</div>)}
190
-
{image.props.get("summary") === 'true' && (<div
192
+
{image.props.summary && (<div
191
193
style={{
192
194
display: "flex",
193
195
flexDirection: "column",
···
216
218
color: theme.accent2
217
219
}}
218
220
>
219
-
{image.props.get("wordcount") === 'true' && `${image.words} words • `}{(image.props.get("chapters") === 'true' && image.chapterCount !== null) && `${image.chapterCount} chapters • `}{image.props.get("postedAt") === 'true' && `posted on ${image.postedAt} • `}{image.props.get("updatedAt") === 'true' && `updated on ${image.updatedAt} • `}{process.env.ARCHIVE}/{addr}
221
+
{image.props.wordcount && `${image.words} words • `}{(image.props.chapters && image.chapterCount !== null) && `${image.chapterCount} chapters • `}{image.props.postedAt && `posted on ${image.postedAt} • `}{image.props.updatedAt && `updated on ${image.updatedAt} • `}{process.env.ARCHIVE}/{addr}
220
222
</div>
221
223
</div>
222
224
</div>
+25
-6
src/lib/sanitizeData.js
+25
-6
src/lib/sanitizeData.js
···
56
56
return "MX"
57
57
}
58
58
59
+
const sanitizeProps = (props) => {
60
+
let propsParsed = {}
61
+
Object.keys(props).forEach((pr) => {
62
+
if (props[pr] === 'true') {
63
+
propsParsed[pr] = true
64
+
return
65
+
} else if (props[pr] === 'false') {
66
+
propsParsed[pr] = false
67
+
return
68
+
} else if (typeof parseInt(props[pr]) === 'Number') {
69
+
propsParsed[pr] = parseInt(props[pr])
70
+
return
71
+
}
72
+
propsParsed[pr] = props[pr]
73
+
})
74
+
return propsParsed
75
+
}
76
+
59
77
export default async function sanitizeData ({ type, data, props}) {
60
-
const baseFont = props.has('baseFont') ? props.get('baseFont') : process.env.DEFAULT_BASE_FONT
78
+
const propsParsed = sanitizeProps(props)
79
+
const baseFont = propsParsed.baseFont ? propsParsed.baseFont : process.env.DEFAULT_BASE_FONT
61
80
const baseFontData = baseFonts[baseFont]
62
-
const titleFont = props.has('titleFont') ? props.get('titleFont') : process.env.DEFAULT_TITLE_FONT
81
+
const titleFont = propsParsed.titleFont ? propsParsed.titleFont : process.env.DEFAULT_TITLE_FONT
63
82
const titleFontData = titleFonts[titleFont]
64
-
const themeData = props.has('theme') ? themes[props.get('theme')] : themes[process.env.DEFAULT_THEME]
83
+
const themeData = propsParsed.theme ? themes[propsParsed.theme] : themes[process.env.DEFAULT_THEME]
65
84
const parentWork = type === 'work' && data.chapterInfo ? await getWork({workId: data.id}) : null
66
85
const bfs = await Promise.all(baseFontData.defs.map(async (bf) => {
67
86
return {
···
98
117
authorsFormatted.slice(-1)[0]
99
118
: authorsFormatted[0]
100
119
const summaryContent = type === 'work'
101
-
? (props.get('summaryType') === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.get('summaryType') === 'custom' && props.has('customSummary') ? props.get('customSummary') : (data.summary ? data.summary : (parentWork ? parentWork.summary : ''))))
102
-
: (props.get('summaryType') === 'custom' && props.has('customSummary') ? props.get('customSummary') : data.notes)
120
+
? (propsParsed.summaryType === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (propsParsed.summaryType === 'custom' && propsParsed.customSummary !== '' ? propsParsed.customSummary : (data.summary ? data.summary : (parentWork ? parentWork.summary : ''))))
121
+
: (propsParsed.summaryType === 'custom' && propsParsed.customSummary !== '' ? propsParsed.customSummary : data.notes)
103
122
const formatter = new Intl.NumberFormat('en-US')
104
123
const words = formatter.format(data.words)
105
124
const summaryDOM = new DOM(summaryContent, {decodeEntities: true});
···
153
172
updatedAt: data.updatedAt,
154
173
baseFont: baseFont,
155
174
titleFont: titleFont,
156
-
props: props,
175
+
props: propsParsed,
157
176
opts: {
158
177
fonts: bfs.concat(tfs)
159
178
}