@dbosoft/nextjs-uicore 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/package.json +6 -4
- package/src/head/index.tsx +119 -119
- package/src/subnav/helpers/useStuckRef.ts +1 -1
- package/src/subnav/index.tsx +2 -3
- package/src/subnav/partials/CtaLinks/github-stars-link/formatStarCount/index.ts +17 -17
- package/src/subnav/partials/CtaLinks/github-stars-link/index.tsx +96 -96
- package/src/subnav/partials/CtaLinks/github-stars-link/parseGithubUrl/index.ts +25 -25
- package/src/subnav/partials/CtaLinks/index.tsx +46 -46
- package/src/subnav/partials/MenuItemsDefault/index.tsx +52 -52
- package/src/subnav/partials/MenuItemsOverflow/index.tsx +51 -51
- package/src/subnav/partials/TitleLink/index.tsx +1 -1
- package/src/subnav/partials/nav-item-text/index.tsx +29 -29
- package/src/tabs/Tabs.tsx +50 -50
- package/src/tabs/TabsClient.tsx +75 -75
- package/src/tabs/index.ts +3 -3
- package/src/tabs/server.ts +2 -2
- package/src/themeselector/index.tsx +3 -3
- package/src/translations.ts +25 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @dbosoft/nextjs-uicore
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 42189cd: Rework design system
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [42189cd]
|
|
12
|
+
- @dbosoft/react-uicore@1.4.0
|
|
13
|
+
|
|
14
|
+
## 1.5.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- react 19.x bug fixes
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
- @dbosoft/react-uicore@1.3.1
|
|
21
|
+
|
|
3
22
|
## 1.5.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbosoft/nextjs-uicore",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "DEPRECATED: Use @dbosoft/react-uicore/tabs for pure React tabs and @dbosoft/nextjs-site-core/ui/tabs for Next.js integration. All other components (head, subnav, themeselector) are replaced by @dbosoft/nextjs-site-core.",
|
|
4
|
+
"deprecated": true,
|
|
4
5
|
"author": "dbosoft",
|
|
5
|
-
"version": "1.
|
|
6
|
+
"version": "1.6.0",
|
|
6
7
|
"sideEffects": false,
|
|
7
8
|
"license": "MIT",
|
|
8
9
|
"exports": {
|
|
@@ -34,10 +35,11 @@
|
|
|
34
35
|
"@heroicons/react": ">=2.1.0",
|
|
35
36
|
"clsx": ">=2.1.0",
|
|
36
37
|
"next-themes": ">=0.4.3",
|
|
37
|
-
"@dbosoft/react-uicore": "1.
|
|
38
|
+
"@dbosoft/react-uicore": "1.4.0"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
|
-
"
|
|
41
|
+
"check-types": "tsc --noEmit",
|
|
42
|
+
"lint": "echo 'DEPRECATED: skipping lint for nextjs-uicore'",
|
|
41
43
|
"clean": "rimraf .turbo && rimraf node_modules && rimraf dist"
|
|
42
44
|
}
|
|
43
45
|
}
|
package/src/head/index.tsx
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
import Head from 'next/head'
|
|
2
|
-
|
|
3
|
-
export default function DBOHead(props: DBOHeadProps): React.ReactElement {
|
|
4
|
-
return (
|
|
5
|
-
<Head>
|
|
6
|
-
{whenString(props.title, <title>{props.title}</title>)}
|
|
7
|
-
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
|
|
8
|
-
<meta property="og:locale" content="en_US" key="og:locale" />
|
|
9
|
-
<meta property="og:type" content="website" key="og:type" />
|
|
10
|
-
<meta
|
|
11
|
-
property="article:publisher"
|
|
12
|
-
content="https://www.facebook.com/dbosoft/"
|
|
13
|
-
key="article:publisher"
|
|
14
|
-
/>
|
|
15
|
-
<meta name="twitter:site" content="@dbosoft" key="twitter:site" />
|
|
16
|
-
<meta
|
|
17
|
-
name="twitter:card"
|
|
18
|
-
content={props.twitterCard || 'summary_large_image'}
|
|
19
|
-
key="twitter:card"
|
|
20
|
-
/>
|
|
21
|
-
<meta name="theme-color" content="#000" key="themeColor" />
|
|
22
|
-
{whenString(
|
|
23
|
-
props.description,
|
|
24
|
-
<meta
|
|
25
|
-
name="description"
|
|
26
|
-
property="og:description"
|
|
27
|
-
content={props.description}
|
|
28
|
-
key="description"
|
|
29
|
-
/>
|
|
30
|
-
)}
|
|
31
|
-
{whenString(
|
|
32
|
-
props.siteName,
|
|
33
|
-
<meta
|
|
34
|
-
property="og:site_name"
|
|
35
|
-
content={props.siteName}
|
|
36
|
-
key="og:site_name"
|
|
37
|
-
/>
|
|
38
|
-
)}
|
|
39
|
-
{whenString(
|
|
40
|
-
props.pageName,
|
|
41
|
-
<meta property="og:title" content={props.pageName} key="og:title" />
|
|
42
|
-
)}
|
|
43
|
-
{whenString(
|
|
44
|
-
props.image,
|
|
45
|
-
<meta property="og:image" content={props.image} key="og:image" />
|
|
46
|
-
)}
|
|
47
|
-
{whenString(
|
|
48
|
-
props.canonicalUrl,
|
|
49
|
-
<link rel="canonical" key="canonical" href={props.canonicalUrl} />
|
|
50
|
-
)}
|
|
51
|
-
{whenArray(props.preload, ({ href, ...linkProps }: {href:string}) => (
|
|
52
|
-
<link href={href} {...linkProps} rel="preload" key={href} />
|
|
53
|
-
))}
|
|
54
|
-
{whenArray(props.icon, ({ href, ...linkProps }: {href:string}) => (
|
|
55
|
-
<link href={href} {...linkProps} rel="icon" key={href} />
|
|
56
|
-
))}
|
|
57
|
-
{whenArray(props.stylesheet, ({ href, ...linkProps }: {href:string}) => (
|
|
58
|
-
<link href={href} {...linkProps} rel="stylesheet" key={href} />
|
|
59
|
-
))}
|
|
60
|
-
{props.children}
|
|
61
|
-
</Head>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Return a map of the value using the callback if it is an Array. */
|
|
66
|
-
const whenArray = (value : any, mapFn: $TSFixMe) =>
|
|
67
|
-
Array.isArray(value) ? value.map(mapFn) : null
|
|
68
|
-
|
|
69
|
-
/** Return the value if it is a String */
|
|
70
|
-
const whenString = (value : any, returnValue: any) =>
|
|
71
|
-
typeof value === 'string' ? returnValue : null
|
|
72
|
-
|
|
73
|
-
// -----
|
|
74
|
-
// Types
|
|
75
|
-
// -----
|
|
76
|
-
|
|
77
|
-
interface DBOHeadProps {
|
|
78
|
-
canonicalUrl?: string
|
|
79
|
-
children?: React.ReactNode
|
|
80
|
-
description?: string
|
|
81
|
-
icon?: {
|
|
82
|
-
[key: string]: unknown
|
|
83
|
-
href: string
|
|
84
|
-
sizes?: string
|
|
85
|
-
type?: string
|
|
86
|
-
}[]
|
|
87
|
-
image?: string
|
|
88
|
-
pageName?: string
|
|
89
|
-
preload?: {
|
|
90
|
-
[key: string]: unknown
|
|
91
|
-
as: asProp
|
|
92
|
-
href: string
|
|
93
|
-
type?: string
|
|
94
|
-
}[]
|
|
95
|
-
siteName?: string
|
|
96
|
-
stylesheet?: {
|
|
97
|
-
[key: string]: unknown
|
|
98
|
-
href: string
|
|
99
|
-
media?: string
|
|
100
|
-
}[]
|
|
101
|
-
title?: string
|
|
102
|
-
twitterCard?: TwitterCardProp
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
type TwitterCardProp = 'summary' | 'summary_large_image'
|
|
106
|
-
|
|
107
|
-
type asProp =
|
|
108
|
-
| 'audio'
|
|
109
|
-
| 'document'
|
|
110
|
-
| 'embed'
|
|
111
|
-
| 'fetch'
|
|
112
|
-
| 'font'
|
|
113
|
-
| 'image'
|
|
114
|
-
| 'object'
|
|
115
|
-
| 'script'
|
|
116
|
-
| 'style'
|
|
117
|
-
| 'track'
|
|
118
|
-
| 'video'
|
|
119
|
-
| 'worker'
|
|
1
|
+
import Head from 'next/head'
|
|
2
|
+
|
|
3
|
+
export default function DBOHead(props: DBOHeadProps): React.ReactElement {
|
|
4
|
+
return (
|
|
5
|
+
<Head>
|
|
6
|
+
{whenString(props.title, <title>{props.title}</title>)}
|
|
7
|
+
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
|
|
8
|
+
<meta property="og:locale" content="en_US" key="og:locale" />
|
|
9
|
+
<meta property="og:type" content="website" key="og:type" />
|
|
10
|
+
<meta
|
|
11
|
+
property="article:publisher"
|
|
12
|
+
content="https://www.facebook.com/dbosoft/"
|
|
13
|
+
key="article:publisher"
|
|
14
|
+
/>
|
|
15
|
+
<meta name="twitter:site" content="@dbosoft" key="twitter:site" />
|
|
16
|
+
<meta
|
|
17
|
+
name="twitter:card"
|
|
18
|
+
content={props.twitterCard || 'summary_large_image'}
|
|
19
|
+
key="twitter:card"
|
|
20
|
+
/>
|
|
21
|
+
<meta name="theme-color" content="#000" key="themeColor" />
|
|
22
|
+
{whenString(
|
|
23
|
+
props.description,
|
|
24
|
+
<meta
|
|
25
|
+
name="description"
|
|
26
|
+
property="og:description"
|
|
27
|
+
content={props.description}
|
|
28
|
+
key="description"
|
|
29
|
+
/>
|
|
30
|
+
)}
|
|
31
|
+
{whenString(
|
|
32
|
+
props.siteName,
|
|
33
|
+
<meta
|
|
34
|
+
property="og:site_name"
|
|
35
|
+
content={props.siteName}
|
|
36
|
+
key="og:site_name"
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
{whenString(
|
|
40
|
+
props.pageName,
|
|
41
|
+
<meta property="og:title" content={props.pageName} key="og:title" />
|
|
42
|
+
)}
|
|
43
|
+
{whenString(
|
|
44
|
+
props.image,
|
|
45
|
+
<meta property="og:image" content={props.image} key="og:image" />
|
|
46
|
+
)}
|
|
47
|
+
{whenString(
|
|
48
|
+
props.canonicalUrl,
|
|
49
|
+
<link rel="canonical" key="canonical" href={props.canonicalUrl} />
|
|
50
|
+
)}
|
|
51
|
+
{whenArray(props.preload, ({ href, ...linkProps }: {href:string}) => (
|
|
52
|
+
<link href={href} {...linkProps} rel="preload" key={href} />
|
|
53
|
+
))}
|
|
54
|
+
{whenArray(props.icon, ({ href, ...linkProps }: {href:string}) => (
|
|
55
|
+
<link href={href} {...linkProps} rel="icon" key={href} />
|
|
56
|
+
))}
|
|
57
|
+
{whenArray(props.stylesheet, ({ href, ...linkProps }: {href:string}) => (
|
|
58
|
+
<link href={href} {...linkProps} rel="stylesheet" key={href} />
|
|
59
|
+
))}
|
|
60
|
+
{props.children}
|
|
61
|
+
</Head>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Return a map of the value using the callback if it is an Array. */
|
|
66
|
+
const whenArray = (value : any, mapFn: $TSFixMe) =>
|
|
67
|
+
Array.isArray(value) ? value.map(mapFn) : null
|
|
68
|
+
|
|
69
|
+
/** Return the value if it is a String */
|
|
70
|
+
const whenString = (value : any, returnValue: any) =>
|
|
71
|
+
typeof value === 'string' ? returnValue : null
|
|
72
|
+
|
|
73
|
+
// -----
|
|
74
|
+
// Types
|
|
75
|
+
// -----
|
|
76
|
+
|
|
77
|
+
interface DBOHeadProps {
|
|
78
|
+
canonicalUrl?: string
|
|
79
|
+
children?: React.ReactNode
|
|
80
|
+
description?: string
|
|
81
|
+
icon?: {
|
|
82
|
+
[key: string]: unknown
|
|
83
|
+
href: string
|
|
84
|
+
sizes?: string
|
|
85
|
+
type?: string
|
|
86
|
+
}[]
|
|
87
|
+
image?: string
|
|
88
|
+
pageName?: string
|
|
89
|
+
preload?: {
|
|
90
|
+
[key: string]: unknown
|
|
91
|
+
as: asProp
|
|
92
|
+
href: string
|
|
93
|
+
type?: string
|
|
94
|
+
}[]
|
|
95
|
+
siteName?: string
|
|
96
|
+
stylesheet?: {
|
|
97
|
+
[key: string]: unknown
|
|
98
|
+
href: string
|
|
99
|
+
media?: string
|
|
100
|
+
}[]
|
|
101
|
+
title?: string
|
|
102
|
+
twitterCard?: TwitterCardProp
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type TwitterCardProp = 'summary' | 'summary_large_image'
|
|
106
|
+
|
|
107
|
+
type asProp =
|
|
108
|
+
| 'audio'
|
|
109
|
+
| 'document'
|
|
110
|
+
| 'embed'
|
|
111
|
+
| 'fetch'
|
|
112
|
+
| 'font'
|
|
113
|
+
| 'image'
|
|
114
|
+
| 'object'
|
|
115
|
+
| 'script'
|
|
116
|
+
| 'style'
|
|
117
|
+
| 'track'
|
|
118
|
+
| 'video'
|
|
119
|
+
| 'worker'
|
package/src/subnav/index.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import useStuckRef from './helpers/useStuckRef'
|
|
|
8
8
|
import TitleLink from './partials/TitleLink'
|
|
9
9
|
import style from './style.module.scss'
|
|
10
10
|
import clsx from 'clsx'
|
|
11
|
-
import { ThemeSelector } from '../themeselector'
|
|
12
11
|
|
|
13
12
|
export type MenuItem = 'divider' | ILinkMenuItem;
|
|
14
13
|
|
|
@@ -48,7 +47,7 @@ const defaultSubNavLabels: SubNavLabels = {
|
|
|
48
47
|
|
|
49
48
|
interface ISubNavProps {
|
|
50
49
|
titleLink?: ITitleLink,
|
|
51
|
-
titleContent?: JSX.Element,
|
|
50
|
+
titleContent?: React.JSX.Element,
|
|
52
51
|
ctaLinks: ICtaItem[],
|
|
53
52
|
hideGithubStars: boolean
|
|
54
53
|
menuItems: MenuItem[],
|
|
@@ -103,7 +102,7 @@ function Subnav({
|
|
|
103
102
|
|
|
104
103
|
<div className="-mr-2 flex items-center sm:hidden ">
|
|
105
104
|
{/* Mobile menu button */}
|
|
106
|
-
<DisclosureButton className="bg-white inline-flex items-center justify-center p-2 rounded-
|
|
105
|
+
<DisclosureButton className="bg-white inline-flex items-center justify-center p-2 rounded-control text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-secondary">
|
|
107
106
|
<span className="sr-only">{labels.openMainMenu}</span>
|
|
108
107
|
{open ? (
|
|
109
108
|
<XMarkIcon aria-hidden="true" className="block h-6 w-6" />
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Given:
|
|
3
|
-
* starCount (int)
|
|
4
|
-
* Return:
|
|
5
|
-
* Formatted string, to match GitHub's typical display of star counts,
|
|
6
|
-
* that is, expressed as thousands of stars
|
|
7
|
-
* Or returns false for falsy starCount values
|
|
8
|
-
*/
|
|
9
|
-
function formatStarCount(starCount: number) {
|
|
10
|
-
if (!starCount || starCount <= 0) return false
|
|
11
|
-
if (starCount < 1000) return `${starCount}`
|
|
12
|
-
const thousands = Math.floor(starCount / 100.0) / 10.0
|
|
13
|
-
if (starCount < 100000) return `${thousands.toFixed(1)}k`
|
|
14
|
-
return `${Math.floor(thousands)}k`
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export default formatStarCount
|
|
1
|
+
/*
|
|
2
|
+
* Given:
|
|
3
|
+
* starCount (int)
|
|
4
|
+
* Return:
|
|
5
|
+
* Formatted string, to match GitHub's typical display of star counts,
|
|
6
|
+
* that is, expressed as thousands of stars
|
|
7
|
+
* Or returns false for falsy starCount values
|
|
8
|
+
*/
|
|
9
|
+
function formatStarCount(starCount: number) {
|
|
10
|
+
if (!starCount || starCount <= 0) return false
|
|
11
|
+
if (starCount < 1000) return `${starCount}`
|
|
12
|
+
const thousands = Math.floor(starCount / 100.0) / 10.0
|
|
13
|
+
if (starCount < 100000) return `${thousands.toFixed(1)}k`
|
|
14
|
+
return `${Math.floor(thousands)}k`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default formatStarCount
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useState } from 'react'
|
|
4
|
-
import type { IconObject } from '@dbosoft/react-uicore/linkbutton';
|
|
5
|
-
import LinkButton from '@dbosoft/react-uicore/linkbutton';
|
|
6
|
-
import { StarIcon } from '@heroicons/react/16/solid'
|
|
7
|
-
import GithubIcon from '../icons/github.svg'
|
|
8
|
-
import formatStarCount from './formatStarCount'
|
|
9
|
-
import parseGithubUrl from './parseGithubUrl'
|
|
10
|
-
import Link from 'next/link';
|
|
11
|
-
|
|
12
|
-
export interface GithubStarsLinkLabels {
|
|
13
|
-
github: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const defaultGithubStarsLinkLabels: GithubStarsLinkLabels = {
|
|
17
|
-
github: 'Github',
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function GithubStarsButton({ url, hideGithubStars, labels: labelsProp }: { url: string, hideGithubStars: boolean, labels?: Partial<GithubStarsLinkLabels> }) {
|
|
21
|
-
const labels = { ...defaultGithubStarsLinkLabels, ...labelsProp }
|
|
22
|
-
const [starCount, setStarCount] = useState(-1)
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (hideGithubStars) { setStarCount(0); return; }
|
|
26
|
-
const parseResult = parseGithubUrl(url);
|
|
27
|
-
const { org, repo } = parseResult !== false ? parseResult : { org: undefined, repo: undefined };
|
|
28
|
-
|
|
29
|
-
if (!org || !repo) { setStarCount(0); return; }
|
|
30
|
-
const githubApiUrl = `https://api.github.com/repos/${org}/${repo}`
|
|
31
|
-
fetch(githubApiUrl)
|
|
32
|
-
.then((response) => {
|
|
33
|
-
response.json().then((data) => {
|
|
34
|
-
// Github's rate limit for unauthenticated requests is 60 per hour
|
|
35
|
-
// When the limit is hit, data.stargazers_count is undefined,
|
|
36
|
-
// and setStarCount falls back to not showing the star count
|
|
37
|
-
setStarCount(data.stargazers_count)
|
|
38
|
-
// Warn if this limit is hit, to avoid otherwise confusing behavior
|
|
39
|
-
// We're still using the response to provide a documentation link
|
|
40
|
-
if (!data.stargazers_count) {
|
|
41
|
-
const { headers } = response
|
|
42
|
-
if (headers.get('x-ratelimit-remaining') === '0') {
|
|
43
|
-
const resetAtSeconds = parseInt(headers.get('x-ratelimit-reset') || '')
|
|
44
|
-
const resetDate = new Date(resetAtSeconds * 1000)
|
|
45
|
-
const rateLimit = headers.get('x-ratelimit-limit')
|
|
46
|
-
console.warn(
|
|
47
|
-
`⭐ Stargazers count could not be fetched. Rate limit exceeded for unauthenticated GitHub API. Limit will be reset to ${rateLimit} at ${resetDate}. See ${data.documentation_url} for more details.`
|
|
48
|
-
)
|
|
49
|
-
} else {
|
|
50
|
-
console.warn(
|
|
51
|
-
`Request for stargazers was successful, but the returned value was undefined or falsy. This might be because the repo has no stars, or it might be a different issue.`
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
.catch((err) => {
|
|
58
|
-
setStarCount(0)
|
|
59
|
-
console.warn(JSON.stringify(err, null, 2))
|
|
60
|
-
})
|
|
61
|
-
}, [hideGithubStars, url])
|
|
62
|
-
|
|
63
|
-
const isLoadingStarCount = starCount === -1
|
|
64
|
-
const isFailedStarCount = formatStarCount(starCount) === false
|
|
65
|
-
|
|
66
|
-
const showStarCount = !hideGithubStars && (isLoadingStarCount || !isFailedStarCount);
|
|
67
|
-
|
|
68
|
-
const icon: IconObject = {
|
|
69
|
-
position: 'right',
|
|
70
|
-
svg: GithubIcon,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<LinkButton icon={!showStarCount ? icon : undefined} label={labels.github} text={!showStarCount ? labels.github : ""}
|
|
75
|
-
variant='neutral'
|
|
76
|
-
url={url} external
|
|
77
|
-
Link={Link}
|
|
78
|
-
>
|
|
79
|
-
{showStarCount ? <div className="inline-flex">
|
|
80
|
-
<GithubIcon className="w-5 mr-2" />
|
|
81
|
-
<div className='-my-2 mx-2 h-9 block bg-gray-300' style={{ width: "1px" }} />
|
|
82
|
-
|
|
83
|
-
<span className="inline-flex gap-1 items-center">
|
|
84
|
-
<StarIcon className='w-5 fill-yellow-500' />
|
|
85
|
-
<span className="g-type-body-small-strong" data-testid="github-stars">
|
|
86
|
-
{formatStarCount(starCount) || <span>—</span>}
|
|
87
|
-
</span>
|
|
88
|
-
</span>
|
|
89
|
-
|
|
90
|
-
</div> : null}
|
|
91
|
-
</LinkButton>
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export default GithubStarsButton
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
import type { IconObject } from '@dbosoft/react-uicore/linkbutton';
|
|
5
|
+
import LinkButton from '@dbosoft/react-uicore/linkbutton';
|
|
6
|
+
import { StarIcon } from '@heroicons/react/16/solid'
|
|
7
|
+
import GithubIcon from '../icons/github.svg'
|
|
8
|
+
import formatStarCount from './formatStarCount'
|
|
9
|
+
import parseGithubUrl from './parseGithubUrl'
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
|
|
12
|
+
export interface GithubStarsLinkLabels {
|
|
13
|
+
github: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const defaultGithubStarsLinkLabels: GithubStarsLinkLabels = {
|
|
17
|
+
github: 'Github',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function GithubStarsButton({ url, hideGithubStars, labels: labelsProp }: { url: string, hideGithubStars: boolean, labels?: Partial<GithubStarsLinkLabels> }) {
|
|
21
|
+
const labels = { ...defaultGithubStarsLinkLabels, ...labelsProp }
|
|
22
|
+
const [starCount, setStarCount] = useState(-1)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (hideGithubStars) { setStarCount(0); return; }
|
|
26
|
+
const parseResult = parseGithubUrl(url);
|
|
27
|
+
const { org, repo } = parseResult !== false ? parseResult : { org: undefined, repo: undefined };
|
|
28
|
+
|
|
29
|
+
if (!org || !repo) { setStarCount(0); return; }
|
|
30
|
+
const githubApiUrl = `https://api.github.com/repos/${org}/${repo}`
|
|
31
|
+
fetch(githubApiUrl)
|
|
32
|
+
.then((response) => {
|
|
33
|
+
response.json().then((data) => {
|
|
34
|
+
// Github's rate limit for unauthenticated requests is 60 per hour
|
|
35
|
+
// When the limit is hit, data.stargazers_count is undefined,
|
|
36
|
+
// and setStarCount falls back to not showing the star count
|
|
37
|
+
setStarCount(data.stargazers_count)
|
|
38
|
+
// Warn if this limit is hit, to avoid otherwise confusing behavior
|
|
39
|
+
// We're still using the response to provide a documentation link
|
|
40
|
+
if (!data.stargazers_count) {
|
|
41
|
+
const { headers } = response
|
|
42
|
+
if (headers.get('x-ratelimit-remaining') === '0') {
|
|
43
|
+
const resetAtSeconds = parseInt(headers.get('x-ratelimit-reset') || '')
|
|
44
|
+
const resetDate = new Date(resetAtSeconds * 1000)
|
|
45
|
+
const rateLimit = headers.get('x-ratelimit-limit')
|
|
46
|
+
console.warn(
|
|
47
|
+
`⭐ Stargazers count could not be fetched. Rate limit exceeded for unauthenticated GitHub API. Limit will be reset to ${rateLimit} at ${resetDate}. See ${data.documentation_url} for more details.`
|
|
48
|
+
)
|
|
49
|
+
} else {
|
|
50
|
+
console.warn(
|
|
51
|
+
`Request for stargazers was successful, but the returned value was undefined or falsy. This might be because the repo has no stars, or it might be a different issue.`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
setStarCount(0)
|
|
59
|
+
console.warn(JSON.stringify(err, null, 2))
|
|
60
|
+
})
|
|
61
|
+
}, [hideGithubStars, url])
|
|
62
|
+
|
|
63
|
+
const isLoadingStarCount = starCount === -1
|
|
64
|
+
const isFailedStarCount = formatStarCount(starCount) === false
|
|
65
|
+
|
|
66
|
+
const showStarCount = !hideGithubStars && (isLoadingStarCount || !isFailedStarCount);
|
|
67
|
+
|
|
68
|
+
const icon: IconObject = {
|
|
69
|
+
position: 'right',
|
|
70
|
+
svg: GithubIcon,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<LinkButton icon={!showStarCount ? icon : undefined} label={labels.github} text={!showStarCount ? labels.github : ""}
|
|
75
|
+
variant='neutral'
|
|
76
|
+
url={url} external
|
|
77
|
+
Link={Link}
|
|
78
|
+
>
|
|
79
|
+
{showStarCount ? <div className="inline-flex">
|
|
80
|
+
<GithubIcon className="w-5 mr-2" />
|
|
81
|
+
<div className='-my-2 mx-2 h-9 block bg-gray-300' style={{ width: "1px" }} />
|
|
82
|
+
|
|
83
|
+
<span className="inline-flex gap-1 items-center">
|
|
84
|
+
<StarIcon className='w-5 fill-yellow-500' />
|
|
85
|
+
<span className="g-type-body-small-strong" data-testid="github-stars">
|
|
86
|
+
{formatStarCount(starCount) || <span>—</span>}
|
|
87
|
+
</span>
|
|
88
|
+
</span>
|
|
89
|
+
|
|
90
|
+
</div> : null}
|
|
91
|
+
</LinkButton>
|
|
92
|
+
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default GithubStarsButton
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Given:
|
|
3
|
-
* urlString (string) valid URL string
|
|
4
|
-
* Return:
|
|
5
|
-
* { org, repo }, where the url is ~= www.github.com/{org}/{repo}
|
|
6
|
-
* or false otherwise (eg if not a valid URL, or if not a GitHub url)
|
|
7
|
-
*/
|
|
8
|
-
function parseGithubUrl(urlString: string) {
|
|
9
|
-
try {
|
|
10
|
-
const urlObj = new URL(urlString)
|
|
11
|
-
if (urlObj.hostname !== 'www.github.com') return false
|
|
12
|
-
const parts = urlObj.pathname.split('/').filter(Boolean)
|
|
13
|
-
const org = parts[0]
|
|
14
|
-
const repo = parts[1]
|
|
15
|
-
return { org, repo }
|
|
16
|
-
} catch (err) {
|
|
17
|
-
console.warn(
|
|
18
|
-
'Warning! An invalid URL has probably been supplied to the GitHub ctaLink in <Subnav />. The corresponding error:'
|
|
19
|
-
)
|
|
20
|
-
console.warn(err)
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default parseGithubUrl
|
|
1
|
+
/*
|
|
2
|
+
* Given:
|
|
3
|
+
* urlString (string) valid URL string
|
|
4
|
+
* Return:
|
|
5
|
+
* { org, repo }, where the url is ~= www.github.com/{org}/{repo}
|
|
6
|
+
* or false otherwise (eg if not a valid URL, or if not a GitHub url)
|
|
7
|
+
*/
|
|
8
|
+
function parseGithubUrl(urlString: string) {
|
|
9
|
+
try {
|
|
10
|
+
const urlObj = new URL(urlString)
|
|
11
|
+
if (urlObj.hostname !== 'www.github.com') return false
|
|
12
|
+
const parts = urlObj.pathname.split('/').filter(Boolean)
|
|
13
|
+
const org = parts[0]
|
|
14
|
+
const repo = parts[1]
|
|
15
|
+
return { org, repo }
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.warn(
|
|
18
|
+
'Warning! An invalid URL has probably been supplied to the GitHub ctaLink in <Subnav />. The corresponding error:'
|
|
19
|
+
)
|
|
20
|
+
console.warn(err)
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default parseGithubUrl
|