feat: add new static badge generator under next-13 pages
8
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||
|
2
.github/workflows/nodejs.yml
vendored
@ -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
@ -5,3 +5,4 @@ node_modules
|
||||
.firebase
|
||||
.next
|
||||
.meta
|
||||
.env
|
||||
|
8
.hintrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"no-inline-styles": "off"
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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'>
|
||||
|
88
libs/create-badgen-handler-next.ts
Normal file
@ -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, '/')
|
||||
}
|
108
libs/serve-badge-next.ts
Normal file
@ -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 {}
|
||||
}
|
@ -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>
|
||||
|
@ -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
generated
@ -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",
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
|
@ -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' })
|
||||
}
|
29
pages/api/static.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@ -2,6 +2,7 @@
|
||||
"version": 2,
|
||||
"regions": ["all"],
|
||||
"routes": [
|
||||
{ "src": "/(?<name>[^/]+).*", "dest": "/api/$name.ts" },
|
||||
{ "src": "/docs/(.*)", "status": 301, "headers": { "Location": "/$1" } }
|
||||
],
|
||||
"env": {
|
||||
|