feat: autofill portfolio metadata

This commit is contained in:
DIYgod 2023-08-05 16:03:16 +01:00
parent d0b8f22ff6
commit 137c6eaf7f
No known key found for this signature in database
5 changed files with 268 additions and 113 deletions

View File

@ -98,6 +98,7 @@
"nanoid": "4.0.2",
"next": "13.4.12",
"node-id3": "0.2.6",
"open-graph-scraper": "^6.2.2",
"pangu": "4.0.7",
"path-to-regexp": "6.2.1",
"pinyin-pro": "3.16.2",

View File

@ -209,6 +209,9 @@ dependencies:
node-id3:
specifier: 0.2.6
version: 0.2.6
open-graph-scraper:
specifier: ^6.2.2
version: 6.2.2
pangu:
specifier: 4.0.7
version: 4.0.7
@ -4751,6 +4754,10 @@ packages:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
dev: false
/chardet@1.6.0:
resolution: {integrity: sha512-+QOTw3otC4+FxdjK9RopGpNOglADbr4WPFi0SonkO99JbpkTPbMxmdm4NenhF5Zs+4gPXLI1+y2uazws5TMe8w==}
dev: false
/charenc@0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
dev: false
@ -4759,6 +4766,30 @@ packages:
resolution: {integrity: sha512-VyklBkB6dOKnCIh63zdVr7QKVMN9/npwUqNAXxWrz8HabVZH/n/d+lyNm1O/vbXFJlT/Hytb5ouYKYGkoeZirQ==}
dev: false
/cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
dependencies:
boolbase: 1.0.0
css-select: 5.1.0
css-what: 6.1.0
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
dev: false
/cheerio@1.0.0-rc.12:
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
engines: {node: '>= 6'}
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
domutils: 3.1.0
htmlparser2: 8.0.2
parse5: 7.1.2
parse5-htmlparser2-tree-adapter: 7.0.0
dev: false
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
@ -5184,10 +5215,25 @@ packages:
- encoding
dev: true
/css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
domutils: 3.1.0
nth-check: 2.1.1
dev: false
/css-selector-parser@1.4.1:
resolution: {integrity: sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==}
dev: false
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
dev: false
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@ -5722,6 +5768,18 @@ packages:
csstype: 3.1.2
dev: false
/dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
/domexception@4.0.0:
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
engines: {node: '>=12'}
@ -5729,10 +5787,25 @@ packages:
webidl-conversions: 7.0.0
dev: false
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/dompurify@3.0.5:
resolution: {integrity: sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==}
dev: false
/domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: false
/dot-prop@5.3.0:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
engines: {node: '>=8'}
@ -7097,6 +7170,15 @@ packages:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
dev: false
/htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
dev: false
/http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
@ -9153,6 +9235,16 @@ packages:
mimic-fn: 4.0.0
dev: true
/open-graph-scraper@6.2.2:
resolution: {integrity: sha512-cQO0c0HF9ZMhSoIEOKMyxbSYwKn6qWBDEdQeCvZnAVwKCxSWj2DV8AwC1J4JCiwZbn/C4grGCJXpvmlAyTXrBg==}
engines: {node: '>=18.0.0'}
dependencies:
chardet: 1.6.0
cheerio: 1.0.0-rc.12
undici: 5.23.0
validator: 13.11.0
dev: false
/open@9.1.0:
resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==}
engines: {node: '>=14.16'}
@ -9325,6 +9417,13 @@ packages:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
dev: false
/parse5-htmlparser2-tree-adapter@7.0.0:
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
dependencies:
domhandler: 5.0.3
parse5: 7.1.2
dev: false
/parse5@6.0.1:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
dev: false
@ -11351,6 +11450,13 @@ packages:
which-boxed-primitive: 1.0.2
dev: true
/undici@5.23.0:
resolution: {integrity: sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg==}
engines: {node: '>=14.0'}
dependencies:
busboy: 1.6.0
dev: false
/unified@10.1.2:
resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==}
dependencies:
@ -11643,6 +11749,11 @@ packages:
sade: 1.8.1
dev: false
/validator@13.11.0:
resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==}
engines: {node: '>= 0.10'}
dev: false
/valtio@1.10.6(react@18.2.0):
resolution: {integrity: sha512-SxN1bHUmdhW6V8qsQTpCgJEwp7uHbntuH0S9cdLQtiohuevwBksbpXjwj5uDMA7bLwg1WKyq9sEpZrx3TIMrkA==}
engines: {node: '>=12.20.0'}

View File

@ -0,0 +1,13 @@
import ogs from "open-graph-scraper"
import { NextServerResponse, getQuery } from "~/lib/server-helper"
export async function GET(req: Request) {
const query = getQuery(req)
const result = await ogs({
url: query.url,
})
const res = new NextServerResponse()
return res.status(200).json(result)
}

View File

@ -110,6 +110,7 @@ export default function PortfolioEditor() {
const updateValue = useCallback(
<K extends keyof Values>(key: K, value: Values[K]) => {
console.log("updateValue", key, value)
if (visibility !== PageVisibilityEnum.Draft) {
setVisibility(PageVisibilityEnum.Modified)
}
@ -129,6 +130,7 @@ export default function PortfolioEditor() {
])
}
useEditorState.setState(newValues)
console.log("newValues", newValues)
},
[type, queryClient, subdomain, visibility],
)
@ -322,125 +324,151 @@ export default function PortfolioEditor() {
)
}
const EditorExtraProperties = memo(
({
updateValue,
}: {
updateValue: <K extends keyof Values>(key: K, value: Values[K]) => void
}) => {
const values = useEditorState()
const { t } = useTranslation("dashboard")
const EditorExtraProperties = memo(() => {
const values = useEditorState()
const { t } = useTranslation("dashboard")
const updateValue = useEditorState.setState
return (
<div className="w-full space-y-5">
<div>
<Input
label={t("External Url") || ""}
isBlock
name="externalUrl"
id="externalUrl"
value={values.externalUrl}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue("externalUrl", e.target.value)
}}
/>
</div>
<div>
<FieldLabel label={t("Cover Image")} />
<ImageUploader
id="icon"
className="aspect-video rounded-lg w-[480px]"
value={values.cover as any}
hasClose={true}
withMimeType={true}
uploadEnd={(key) => {
const { address, mime_type } = key as {
address?: string
mime_type?: string
}
updateValue("cover", {
const [filling, setFilling] = useState(false)
const autofill = async () => {
setFilling(true)
const { result } = await (
await fetch(`/api/open-graph?url=${values.externalUrl}`)
).json()
updateValue({
cover: {
address: result?.ogImage?.[0]?.url,
mime_type: result?.ogImage?.[0]?.type || "image/png",
},
title: result?.ogTitle,
excerpt: result?.ogDescription,
publishedAt: result?.articlePublishedTime || result?.publishedTime,
})
setFilling(false)
}
return (
<div className="w-full space-y-5">
<div>
<Input
label={t("External Url") || ""}
isBlock
name="externalUrl"
id="externalUrl"
value={values.externalUrl}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue({
externalUrl: e.target.value,
})
}}
/>
</div>
<Button onClick={autofill} isLoading={filling}>
{t("Autofill")}
</Button>
<div>
<FieldLabel label={t("Cover Image")} />
<ImageUploader
id="icon"
className="aspect-video rounded-lg w-[480px]"
value={values.cover as any}
hasClose={true}
withMimeType={true}
uploadEnd={(key) => {
const { address, mime_type } = key as {
address?: string
mime_type?: string
}
updateValue({
cover: {
address,
mime_type,
})
}}
accept="image/*"
/>
</div>
<div>
<Input
label={t("Title") || ""}
isBlock
name="title"
id="title"
value={values.title}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue("title", e.target.value)
}}
/>
</div>
<div>
<Input
label={t("Excerpt") || ""}
isBlock
name="excerpt"
id="excerpt"
value={values.excerpt}
multiline
rows={4}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
updateValue("excerpt", e.target.value)
}}
/>
</div>
<div>
<label className="form-label" htmlFor="publishAt">
{t("Publish at")}
</label>
<DateInput
className="[&_input]:text-black/90"
allowDeselect
clearable
valueFormat="YYYY-MM-DD, h:mm a"
name="publishAt"
id="publishAt"
value={
values.publishedAt ? new Date(values.publishedAt) : undefined
}
onChange={(value: Date | null) => {
if (value) {
updateValue("publishedAt", value.toISOString())
} else {
updateValue("publishedAt", "")
}
}}
styles={{
input: {
borderRadius: "0.5rem",
borderColor: "var(--border-color)",
height: "2.5rem",
"&:focus-within": {
borderColor: "var(--theme-color)",
},
},
}}
/>
<div className="text-xs text-gray-400 mt-1">
{t(
`This portfolio will be accessible from this time. Leave blank to use the current time.`,
)}
</div>
{values.publishedAt > new Date().toISOString() && (
<div className="text-xs mt-1 text-orange-500">
{t(
"The post is currently not public as its publication date has been scheduled for a future time.",
)}
</div>
})
}}
accept="image/*"
/>
</div>
<div>
<Input
label={t("Title") || ""}
isBlock
name="title"
id="title"
value={values.title}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
updateValue({
title: e.target.value,
})
}}
/>
</div>
<div>
<Input
label={t("Excerpt") || ""}
isBlock
name="excerpt"
id="excerpt"
value={values.excerpt}
multiline
rows={4}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
updateValue({
excerpt: e.target.value,
})
}}
/>
</div>
<div>
<label className="form-label" htmlFor="publishAt">
{t("Publish at")}
</label>
<DateInput
className="[&_input]:text-black/90"
allowDeselect
clearable
valueFormat="YYYY-MM-DD, h:mm a"
name="publishAt"
id="publishAt"
value={values.publishedAt ? new Date(values.publishedAt) : undefined}
onChange={(value: Date | null) => {
if (value) {
updateValue({
publishedAt: value.toISOString(),
})
} else {
updateValue({
publishedAt: "",
})
}
}}
styles={{
input: {
borderRadius: "0.5rem",
borderColor: "var(--border-color)",
height: "2.5rem",
"&:focus-within": {
borderColor: "var(--theme-color)",
},
},
}}
/>
<div className="text-xs text-gray-400 mt-1">
{t(
`This portfolio will be accessible from this time. Leave blank to use the current time.`,
)}
</div>
{values.publishedAt > new Date().toISOString() && (
<div className="text-xs mt-1 text-orange-500">
{t(
"The post is currently not public as its publication date has been scheduled for a future time.",
)}
</div>
)}
</div>
)
},
)
</div>
)
})
EditorExtraProperties.displayName = "EditorExtraProperties"

View File

@ -223,5 +223,7 @@
"Pin": "置顶",
"Unpin": "取消置顶",
"Interaction": "互动",
"External Url": "外部链接"
"External Url": "外部链接",
"Autofill": "自动填充",
"Title": "标题"
}