feat: add new static badge generator under next-13 pages

pull/579/head
Amio 2023-01-14 22:34:10 +08:00
rodzic f0ebc06449
commit e6f11b322a
28 zmienionych plików z 748 dodań i 1249 usunięć

Wyświetl plik

@ -38,12 +38,12 @@ jobs:
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
- run: git checkout HEAD^1
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -54,7 +54,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

Wyświetl plik

@ -14,7 +14,7 @@ jobs:
node-version: '16'
- name: npm install, build, and test
run: |
npm ci
npm install
npm run lint
npm run build --if-present
env:

1
.gitignore vendored
Wyświetl plik

@ -5,3 +5,4 @@ node_modules
.firebase
.next
.meta
.env

8
.hintrc 100644
Wyświetl plik

@ -0,0 +1,8 @@
{
"extends": [
"development"
],
"hints": {
"no-inline-styles": "off"
}
}

2
.nvmrc
Wyświetl plik

@ -1 +1 @@
14
18

Wyświetl plik

@ -10,7 +10,7 @@ export default function BadgenTitle ({ host }) {
<div className='title-block'>
<div className='title'>
<h1>
<Image className='badgen-icon' alt='badgen logo' src='/static/badgen-logo.svg' width='42' height='42' />
<Image className='badgen-icon' alt='badgen logo' src='/statics/badgen-logo.svg' width='42' height='42' />
<span className='badgen-name'>Badgen</span>
<StyleSwitch host={host} />
</h1>

Wyświetl plik

@ -1,4 +1,3 @@
// import debounceRender from 'react-debounce-render'
import BadgenTitle from './badgen-title'
const BadgePreview = ({ host, badgeURL, focus }) => {
@ -11,7 +10,7 @@ const BadgePreview = ({ host, badgeURL, focus }) => {
<div className={'preview ' + (showPreview ? 'show' : 'none')}>
<PreviewBadge host={host} url={badgeURL} />
</div>
<style>{`
<style jsx>{`
.header-preview {
height: calc(50vh - 100px);
width: 100%;
@ -54,22 +53,19 @@ const BadgePreview = ({ host, badgeURL, focus }) => {
)
}
/* const PreviewBadge = debounceRender(({ host, url }) => {
return <img style={{ height: '30px' }} src={genBadgeSrc(host, url)} />
}, 300) */
const PreviewBadge = ({ host, url }) => {
// eslint-disable-next-line @next/next/no-img-element
return <img alt={url} style={{ height: '30px' }} src={genBadgeSrc(host, url)} />
}
const genBadgeSrc = (host, url) => {
const genBadgeSrc = (host: string, url: string) => {
if (!url) {
return host + 'badge/%20/%20'
return host + 'static/%20/%20'
}
if (url.split('/').length > 2) {
return host + url
} else {
return host + 'badge/%20/%20'
return host + 'static/%20/%20'
}
}

Wyświetl plik

@ -8,7 +8,7 @@ export default function Footer () {
<div className='footer-content'>
<div>
<h3>
<img alt='badgen logo' src='/static/badgen-logo-w.svg' />
<img alt='badgen logo' src='/statics/badgen-logo-w.svg' />
Badgen Service
</h3>
<div className='sitemap'>

Wyświetl plik

@ -0,0 +1,88 @@
import http from 'http'
import { measure } from 'measurement-protocol'
import matchRoute from 'my-way'
import { serveBadgeNext } from './serve-badge-next'
import serveDoc from './serve-doc'
import sentry from './sentry'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { BadgenParams } from './types'
export type PathArgs = NonNullable<ReturnType<typeof matchRoute>>
export interface BadgenServeConfig {
title: string;
help?: string;
examples: { [url: string]: string };
handlers: { [pattern: string]: (pathArgs: PathArgs) => Promise<BadgenParams> };
}
export function createBadgenHandler (badgenServerConfig: BadgenServeConfig) {
const { handlers } = badgenServerConfig
async function nextHandler (req: NextApiRequest, res: NextApiResponse) {
let { pathname } = new URL(req.url || '/', `http://${req.headers.host}`)
if (pathname === '/favicon.ico') {
return res.end()
}
// Match badge handlers
let matchedArgs: PathArgs | null = null
const matchedScheme = Object.keys(handlers).find(scheme => {
return matchedArgs = matchRoute(scheme, decodeURI(pathname))
})
// Invoke badge handler
if (matchedArgs !== null && matchedScheme !== undefined) {
return await handlers[matchedScheme](matchedArgs).then(params => {
return serveBadgeNext(req, res, { params })
}).catch(e => {
return onBadgeHandlerError(e, req, res)
})
}
if (matchRoute('/:name', pathname)) {
return serveDoc(badgenServerConfig)(req, res)
// return res.send('TODO: serve doc page')
}
return res.status(404).end()
}
return nextHandler
}
function onBadgeHandlerError (err: Error, req: NextApiRequest, res: NextApiResponse) {
// Send user friendly response
res.status(500).setHeader('error-message', err.message)
return serveBadgeNext(req, res, {
code: 200,
params: {
subject: 'error',
status: '',
color: 'red'
}
})
}
const { TRACKING_GA, NOW_REGION } = process.env
const tracker = TRACKING_GA && measure(TRACKING_GA).setCustomDimensions([NOW_REGION || 'unknown'])
async function measurementLogInvocation (host: string, urlPath: string) {
tracker && tracker.pageview({ host, path: urlPath}).send()
}
async function measurementLogError (category: string, action: string, label?: string, value?: number) {
tracker && tracker.event(category, action, label, value).send()
}
function getBadgeStyle (req: http.IncomingMessage): string | undefined {
const host = req.headers['x-forwarded-host']?.toString() ?? req.headers.host ?? ''
return host.startsWith('flat') ? 'flat' : undefined
}
function simpleDecode (str: string): string {
return String(str).replace(/%2F/g, '/')
}

Wyświetl plik

@ -0,0 +1,108 @@
import { badgen } from 'badgen'
import icons from 'badgen-icons'
import originalUrl from 'original-url'
import { BadgenParams } from './types'
import type { NextApiRequest, NextApiResponse } from 'next'
type ServeBadgeOptions = {
code?: number
sMaxAge?: number,
params: BadgenParams
}
export function serveBadgeNext (req: NextApiRequest, res: NextApiResponse, options: ServeBadgeOptions) {
const { code = 200, sMaxAge = 3600, params } = options
const { subject, status, color } = params
const query = req.query
const { list, scale, cache } = req.query
const iconMeta = resolveIcon(query.icon, query.iconWidth)
const badgeParams = {
labelColor: resolveColor(query.labelColor, 'black'),
subject: formatSVGText(typeof query.label === 'string' ? query.label : subject),
status: formatSVGText(transformStatus(status, { list })),
color: resolveColor(query.color || color, 'blue'),
style: resolveBadgeStyle(req),
icon: iconMeta.src,
iconWidth: iconMeta.width,
scale: parseFloat(String(scale)) || 1,
}
const badgeSVGString = badgen(badgeParams)
// Minimum s-maxage is set to 300s(5m)
const cacheMaxAge = cache ? Math.max(parseInt(String(cache)), 300) : sMaxAge
res.setHeader('Cache-Control', `public, max-age=86400, s-maxage=${cacheMaxAge}, stale-while-revalidate=86400`)
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
res.statusCode = code
res.send(badgeSVGString)
}
function resolveBadgeStyle (req: NextApiRequest, style?: string | string[]): 'flat' | 'classic' {
if (style === 'flat') {
return 'flat'
}
if (process.env.BADGE_STYLE === 'flat') {
return 'flat'
}
if (originalUrl(req).hostname.includes('flat')) {
return 'flat'
}
return 'classic'
}
function formatSVGText (text: string): string {
return text
.replace(/%2F/g, '/') // simple decode
}
function transformStatus (status: any, { list }): string {
status = String(status)
if (list !== undefined) {
if (list === '1' || list === '') list = '|' // compatible
status = status.replace(/,/g, ` ${list} `)
}
return status
}
function resolveColor (color: string | string[] | undefined, defaultColor: string): string {
if (color !== undefined) {
return String(color)
}
return defaultColor
}
type ResolvedIcon = {
src?: string
width?: number
}
function resolveIcon (icon?: string | string[], width?: string | string[]): ResolvedIcon {
const iconArg = String(icon) || ''
const widthNum = parseInt(String(width)) || 10
const builtinIcon = icons[icon]
if (builtinIcon) {
return {
src: builtinIcon.base64,
width: widthNum || builtinIcon.width
}
}
if (iconArg.startsWith('data:image/')) {
return { src: iconArg, width: widthNum }
}
return {}
}

Wyświetl plik

@ -75,7 +75,7 @@ const helpFooter = `
<footer>
<div class='footer-content'>
<div>
<h3><img src='/static/badgen-logo-w.svg' />Badgen Service</h3>
<h3><img src='/statics/badgen-logo-w.svg' />Badgen Service</h3>
<div class='sitemap'>
<a href='https://badgen.net'>Classic</a>
<em>/</em>

Wyświetl plik

@ -25,7 +25,14 @@ const nextConfig = {
const badgeRedirects = liveBadgeRedirects.concat(staticBadgeRedirects)
return badgeRedirects
// return badgeRedirects
return [
{ source: '/static/:path*', destination: '/api/static' },
{ source: '/static', destination: '/api/static' },
{ source: '/badge/:path*', destination: '/api/static' },
{ source: '/badge', destination: '/api/static' }
]
},
}

1523
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -47,7 +47,6 @@
"yaml": "^2.1.0"
},
"devDependencies": {
"@babel/core": "^7.13.15",
"@next/font": "^13.1.1",
"@types/fs-extra": "^9.0.11",
"@types/lodash.debounce": "^4.0.6",

Wyświetl plik

@ -1,49 +0,0 @@
/* eslint-disable @next/next/no-css-tags */
import React from 'react'
import App from 'next/app'
import { Html, Head } from 'next/document'
import Script from 'next/script'
declare global {
interface Window {
dataLayer: Array<any>;
}
}
export default class MyApp extends App {
componentDidMount () {
window.dataLayer = window.dataLayer || []
function gtag (...args) { window.dataLayer.push(args) }
gtag('js', new Date())
gtag('config', 'UA-4646421-14')
}
render () {
const { Component, pageProps } = this.props
return (
<Html>
<Head>
<link rel='icon' type='image/png' href='/static/favicon.png' />
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
<link
rel='stylesheet'
href='https://fonts.googleapis.com/css?family=Merriweather:700,300&display=optional'
/>
<link rel='stylesheet' href='/static/index.css' />
<Script
src="https://www.googletagmanager.com/gtag/js?id=UA-4646421-14"
strategy="afterInteractive"
/>
</Head>
<Component {...pageProps} />
<style>{`
html, body { margin: 0; height: 100%; scroll-behavior: smooth }
#__next { height: 100% }
a { text-decoration: none }
`}
</style>
</Html>
)
}
}

Wyświetl plik

@ -1,62 +0,0 @@
import React from 'react'
import Preview from '../components/builder-preview'
import Bar from '../components/builder-bar'
import Hints from '../components/builder-hints'
import Helper from '../components/builder-helper'
import Footer from '../components/footer'
export default class BuilderPage extends React.Component {
state = {
host: undefined,
badgeURL: '',
placeholder: '',
focus: false
}
handleBlur = () => this.setState({ focus: false })
handleFocus = () => this.setState({ focus: true })
handleChange = badgeURL => this.setState({ badgeURL })
handleSelect = exampleURL => this.setState({ badgeURL: exampleURL })
componentDidMount () {
const forceHost = new URL(window.location.href).searchParams.get('host')
this.setState({
host: (forceHost || window.location.origin) + '/',
badgeURL: window.location.hash.replace(/^#/, ''),
placeholder: 'badge/:subject/:status/:color?icon=github'
})
}
render () {
const { host, placeholder, badgeURL, focus } = this.state
return (
<div className='home'>
<div className='hero'>
<Preview host={host} badgeURL={badgeURL} focus={focus} />
<Bar
host={host}
badgeURL={badgeURL}
placeholder={placeholder}
onChange={this.handleChange}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
/>
<Hints focus={focus} badgeURL={badgeURL} />
{badgeURL && <Helper host={host} badgeURL={badgeURL} onSelect={this.handleSelect} />}
</div>
<Footer />
<style jsx>{`
.hero {
min-height: 100vh;
position: relative;
}
`}
</style>
</div>
)
}
}

Wyświetl plik

@ -1,71 +0,0 @@
import { useState, useEffect } from 'react'
import BadgeExamples from '../components/badge-examples'
import BadgenTitle from '../components/badgen-title'
// import TopBar from '../components/top-bar'
import Intro from '../components/home-intro'
import Footer from '../components/footer'
import examples from '../public/.meta/badges.json'
const Index = () => {
const [tab, setTab] = useState('live')
const [host, setHost] = useState('')
const badges = examples[tab]
useEffect(() => {
const forceHost = new URL(window.location.href).searchParams.get('host')
setHost((forceHost || window.location.origin) + '/')
})
return <>
<BadgenTitle host={host} />
<div className='docs' style={{ width: '980px', margin: '0 auto' }}>
<Intro />
<h2 style={{ textAlign: 'center' }}>Badge Gallery</h2>
<div className='tab-row'>
<div className={`tab ${tab}`}>
<a onClick={() => setTab('live')} className='live'>Live Badges</a>
<a onClick={() => setTab('static')} className='static'>Static Badges</a>
</div>
</div>
<BadgeExamples data={badges} />
</div>
<Footer />
<style jsx>{`
.docs {
margin: 0 auto;
padding-bottom: 6em;
}
p {
text-align: center
}
.tab-row {
text-align: center;
}
.tab {
display: inline-block;
border: 1px solid #333;
margin-bottom: 2rem;
}
.tab a {
display: inline-block;
padding: 0 8px;
color: #333;
font: 14px/26px sans-serif;
text-transform: uppercase;
}
.tab a:hover {
cursor: pointer;
}
.live a.live,
.static a.static {
color: #EEE;
background-color: #333;
}
`}
</style>
</> // eslint-disable-line
}
export default Index

Wyświetl plik

@ -5,7 +5,7 @@ export default function Document() {
return (
<Html lang="en">
<Head>
<link rel='icon' type='image/png' href='/static/favicon.png' />
<link rel='icon' type='image/png' href='/statics/favicon.png' />
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Merriweather:700,300&display=swap' />
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Inter:700,300&display=swap' />
<Script

Wyświetl plik

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

Wyświetl plik

@ -0,0 +1,29 @@
import { createBadgenHandler } from '../../libs/create-badgen-handler-next'
import type { PathArgs } from '../../libs/create-badgen-handler-next'
export default createBadgenHandler({
title: 'Static Badge',
examples: {
'/static/Swift/4.2/orange': 'swift version',
'/static/license/MIT/blue': 'license MIT',
'/static/chat/on%20gitter/cyan': 'chat on gitter',
'/static/stars/★★★★☆': 'star rating',
'/static/become/a%20patron/F96854': 'patron',
'/static/code%20style/standard/f2a': 'code style: standard'
},
handlers: {
'/static/:label/:status': handler,
'/static/:label/:status/:color': handler,
'/badge/:label/:status': handler,
'/badge/:label/:status/:color': handler
}
})
async function handler ({ label, status, color }: PathArgs) {
return {
subject: label,
status,
color
}
}

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 445 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 445 B

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 445 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 445 B

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 457 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 457 B

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Wyświetl plik

@ -2,6 +2,7 @@
"version": 2,
"regions": ["all"],
"routes": [
{ "src": "/(?<name>[^/]+).*", "dest": "/api/$name.ts" },
{ "src": "/docs/(.*)", "status": 301, "headers": { "Location": "/$1" } }
],
"env": {