refactor(website): convert website to TypeScript (#5328)

* Initial work

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Minor changes

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Fix error

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* This looks better

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Address suggestions

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Better style

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Better style

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Better context typing

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Update edit URL

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>

* Minor refactor

Signed-off-by: Josh-Cena <sidachen2003@gmail.com>
This commit is contained in:
Joshua Chen 2021-08-11 17:38:33 +08:00 committed by GitHub
parent 9afc900780
commit 6c21061e34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 193 additions and 111 deletions

View file

@ -63,6 +63,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/docusaurus": "^1.0.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"raw-loader": "^4.0.2" "raw-loader": "^4.0.2"
} }

View file

@ -5,11 +5,17 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react'; import React, {ReactNode} from 'react';
import styles from './styles.module.css'; import styles from './styles.module.css';
function BrowserWindow({children, minHeight, url}) { interface Props {
children: ReactNode;
minHeight: number;
url: string;
}
function BrowserWindow({children, minHeight, url}: Props) {
return ( return (
<div className={styles.browserWindow} style={{minHeight}}> <div className={styles.browserWindow} style={{minHeight}}>
<div className={styles.browserWindowHeader}> <div className={styles.browserWindowHeader}>

View file

@ -6,17 +6,23 @@
*/ */
import React, {useState} from 'react'; import React, {useState} from 'react';
import Color from 'color'; import Color from 'color';
import CodeBlock from '@theme/CodeBlock'; import CodeBlock from '@theme/CodeBlock';
import styles from './styles.module.css'; import styles from './styles.module.css';
const COLOR_SHADES = { const COLOR_SHADES: Record<
string,
{
adjustment: number;
adjustmentInput: string;
displayOrder: number;
codeOrder: number;
}
> = {
'--ifm-color-primary': { '--ifm-color-primary': {
adjustment: 0, adjustment: 0,
adjustmentInput: 0, adjustmentInput: '0',
displayOrder: 3, displayOrder: 3,
codeOrder: 0, codeOrder: 0,
}, },
@ -60,7 +66,7 @@ const COLOR_SHADES = {
const DEFAULT_PRIMARY_COLOR = '3578e5'; const DEFAULT_PRIMARY_COLOR = '3578e5';
function ColorGenerator({children, minHeight, url}) { function ColorGenerator() {
const [baseColor, setBaseColor] = useState(DEFAULT_PRIMARY_COLOR); const [baseColor, setBaseColor] = useState(DEFAULT_PRIMARY_COLOR);
const [shades, setShades] = useState(COLOR_SHADES); const [shades, setShades] = useState(COLOR_SHADES);
const color = Color('#' + baseColor); const color = Color('#' + baseColor);

View file

@ -45,7 +45,14 @@ const Playgrounds = [
}, },
]; ];
function PlaygroundCard({name, image, url, description}) { interface Props {
name: string;
image: any;
url: string;
description: JSX.Element;
}
function PlaygroundCard({name, image, url, description}: Props) {
return ( return (
<div className="col col--6 margin-bottom--lg"> <div className="col col--6 margin-bottom--lg">
<div className={clsx('card')}> <div className={clsx('card')}>

View file

@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react'; import React, {ReactNode} from 'react';
import Translate from '@docusaurus/Translate'; import Translate from '@docusaurus/Translate';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
function WebsiteLink({to, children}) { function WebsiteLink({to, children}: {to: string; children?: ReactNode}) {
return ( return (
<Link to={to}> <Link to={to}>
{children || ( {children || (
@ -19,7 +19,21 @@ function WebsiteLink({to, children}) {
); );
} }
function TeamProfileCard({className, name, children, githubUrl, twitterUrl}) { interface ProfileProps {
className?: string;
name: string;
children: ReactNode;
githubUrl?: string;
twitterUrl?: string;
}
function TeamProfileCard({
className,
name,
children,
githubUrl,
twitterUrl,
}: ProfileProps) {
return ( return (
<div className={className}> <div className={className}>
<div className="card card--full-height"> <div className="card card--full-height">
@ -55,7 +69,7 @@ function TeamProfileCard({className, name, children, githubUrl, twitterUrl}) {
); );
} }
function TeamProfileCardCol(props) { function TeamProfileCardCol(props: ProfileProps) {
return ( return (
<TeamProfileCard {...props} className={'col col--6 margin-bottom--lg'} /> <TeamProfileCard {...props} className={'col col--6 margin-bottom--lg'} />
); );

View file

@ -10,10 +10,10 @@ import React, {memo} from 'react';
import styles from './styles.module.css'; import styles from './styles.module.css';
import clsx from 'clsx'; import clsx from 'clsx';
import Image from '@theme/IdealImage'; import Image from '@theme/IdealImage';
import {Tags, TagList} from '../../../data/users'; import {Tags, TagList, TagType, User, Tag} from '../../../data/users';
import {sortBy} from '../../../utils/jsUtils'; import {sortBy} from '../../../utils/jsUtils';
function TagIcon({label, description, icon}) { function TagIcon({label, description, icon}: Tag) {
return ( return (
<span <span
className={styles.tagIcon} className={styles.tagIcon}
@ -24,7 +24,7 @@ function TagIcon({label, description, icon}) {
); );
} }
function ShowcaseCardTagIcons({tags}) { function ShowcaseCardTagIcons({tags}: {tags: TagType[]}) {
const tagObjects = tags const tagObjects = tags
.map((tag) => ({tag, ...Tags[tag]})) .map((tag) => ({tag, ...Tags[tag]}))
.filter((tagObject) => !!tagObject.icon); .filter((tagObject) => !!tagObject.icon);
@ -34,12 +34,16 @@ function ShowcaseCardTagIcons({tags}) {
TagList.indexOf(tagObject.tag), TagList.indexOf(tagObject.tag),
); );
return tagObjectsSorted.map((tagObject, index) => ( return (
<TagIcon key={index} {...tagObject} /> <>
)); {tagObjectsSorted.map((tagObject, index) => (
<TagIcon key={index} {...tagObject} />
))}
</>
);
} }
const ShowcaseCard = memo(function ({user}) { const ShowcaseCard = memo(function ({user}: {user: User}) {
return ( return (
<div key={user.title} className="col col--4 margin-bottom--lg"> <div key={user.title} className="col col--4 margin-bottom--lg">
<div className={clsx('card', styles.showcaseCard)}> <div className={clsx('card', styles.showcaseCard)}>

View file

@ -1,36 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
function ShowcaseCheckbox({
className,
name,
label,
onChange,
checked,
...props
}) {
const id = `showcase_checkbox_id_${name};`;
return (
<div className={clsx(props.className, styles.checkboxContainer)} {...props}>
<input
type="checkbox"
id={id}
name={name}
onChange={onChange}
checked={checked}
/>
<label htmlFor={id}>{label}</label>
</div>
);
}
export default ShowcaseCheckbox;

View file

@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {ComponentProps, ReactNode} from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
interface Props extends ComponentProps<'input'> {
label: ReactNode;
}
function ShowcaseCheckbox({title, className, label, ...props}: Props) {
const id = `showcase_checkbox_id_${props.name};`;
return (
<div className={clsx(className, styles.checkboxContainer)} title={title}>
<input type="checkbox" id={id} {...props} />
<label htmlFor={id}>{label}</label>
</div>
);
}
export default ShowcaseCheckbox;

View file

@ -5,17 +5,21 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react'; import React, {ComponentProps} from 'react';
import styles from './styles.module.css'; import styles from './styles.module.css';
function ShowcaseSelect({tag, label, onChange, value, children}) { interface Props extends ComponentProps<'select'> {
const id = `showcase_select_id_${tag};`; label: string;
}
function ShowcaseSelect({label, ...props}: Props) {
const id = `showcase_select_id_${props.name};`;
return ( return (
<div className={styles.selectContainer}> <div className={styles.selectContainer}>
<label htmlFor={id}>{label}</label> <label htmlFor={id}>{label}</label>
<select id={id} name={tag} onChange={onChange} value={value}> <select id={id} {...props}>
{children} {props.children}
</select> </select>
</div> </div>
); );

View file

@ -39,10 +39,38 @@ import {difference, sortBy} from '../utils/jsUtils';
* *
*/ */
export type Tag = {
label: string;
description: string;
icon: JSX.Element;
};
export type TagType =
| 'favorite'
| 'opensource'
| 'product'
| 'design'
| 'i18n'
| 'versioning'
| 'multiInstance'
| 'large'
| 'facebook'
| 'personal'
| 'rtl';
export type User = {
title: string;
description: string;
preview: any;
website: string;
source: string;
tags: TagType[];
};
// LIST OF AVAILABLE TAGS // LIST OF AVAILABLE TAGS
// Available tags to assign to your site // Available tags to assign to your site
// Please choose widely, we'll remove unappropriate tags // Please choose widely, we'll remove unappropriate tags
export const Tags = { export const Tags: Record<TagType, Tag> = {
// DO NOT USE THIS TAG: we choose sites to add to favorites // DO NOT USE THIS TAG: we choose sites to add to favorites
favorite: { favorite: {
label: 'Favorite', label: 'Favorite',
@ -123,7 +151,7 @@ export const Tags = {
// Add your site to this list // Add your site to this list
// prettier-ignore // prettier-ignore
const Users = [ const Users: User[] = [
{ {
title: 'Aide Jeune', title: 'Aide Jeune',
description: 'French Discord server that helps young people who have been bullied or feel bad about themselves', description: 'French Discord server that helps young people who have been bullied or feel bad about themselves',
@ -1276,7 +1304,7 @@ const Users = [
} }
]; ];
export const TagList = Object.keys(Tags); export const TagList = Object.keys(Tags) as TagType[];
function sortUsers() { function sortUsers() {
let result = Users; let result = Users;
// Sort by site name // Sort by site name
@ -1289,7 +1317,7 @@ function sortUsers() {
export const SortedUsers = sortUsers(); export const SortedUsers = sortUsers();
// Fail-fast on common errors // Fail-fast on common errors
function ensureUserValid(user) { function ensureUserValid(user: User) {
function checkFields() { function checkFields() {
const keys = Object.keys(user); const keys = Object.keys(user);
const validKeys = [ const validKeys = [
@ -1346,7 +1374,11 @@ function ensureUserValid(user) {
} }
function checkTags() { function checkTags() {
if (!user.tags || !(user.tags instanceof Array) || user.tags.includes('')) { if (
!user.tags ||
!(user.tags instanceof Array) ||
(user.tags as string[]).includes('')
) {
throw new Error(`Bad showcase tags=[${JSON.stringify(user.tags)}]`); throw new Error(`Bad showcase tags=[${JSON.stringify(user.tags)}]`);
} }
const unknownTags = difference(user.tags, TagList); const unknownTags = difference(user.tags, TagList);

View file

@ -5,10 +5,10 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react'; import React, {useState} from 'react';
export default function MyComponent() { export default function MyComponent() {
const [bool, setBool] = React.useState(false); const [bool, setBool] = useState(false);
return ( return (
<div> <div>
<p>MyComponent rendered !</p> <p>MyComponent rendered !</p>

View file

@ -17,8 +17,8 @@ const BOARD_TOKEN = '054e0e53-d951-b14c-7e74-9eb8f9ed2f91';
function Feedback() { function Feedback() {
useEffect(() => { useEffect(() => {
canny(); canny();
window.Canny && (window as any).Canny &&
window.Canny('render', { (window as any).Canny('render', {
boardToken: BOARD_TOKEN, boardToken: BOARD_TOKEN,
basePath: '/feedback', basePath: '/feedback',
}); });

View file

@ -79,10 +79,14 @@ const QUOTES = [
]; ];
function Home() { function Home() {
const context = useDocusaurusContext(); const {
const {siteConfig: {customFields = {}, tagline} = {}} = context; siteConfig: {
customFields: {description},
tagline,
},
} = useDocusaurusContext();
return ( return (
<Layout title={tagline} description={customFields.description}> <Layout title={tagline} description={description as string}>
<main> <main>
<div className={styles.hero}> <div className={styles.hero}>
<div className={styles.heroInner}> <div className={styles.heroInner}>

View file

@ -16,14 +16,20 @@ import clsx from 'clsx';
import {useHistory, useLocation} from '@docusaurus/router'; import {useHistory, useLocation} from '@docusaurus/router';
import {toggleListItem} from '../../utils/jsUtils'; import {toggleListItem} from '../../utils/jsUtils';
import {SortedUsers, Tags, TagList} from '../../data/users'; import {SortedUsers, Tags, TagList, User, TagType} from '../../data/users';
type Operator = 'OR' | 'AND';
const TITLE = 'Docusaurus Site Showcase'; const TITLE = 'Docusaurus Site Showcase';
const DESCRIPTION = 'List of websites people are building with Docusaurus'; const DESCRIPTION = 'List of websites people are building with Docusaurus';
const EDIT_URL = const EDIT_URL =
'https://github.com/facebook/docusaurus/edit/master/website/src/data/users.js'; 'https://github.com/facebook/docusaurus/edit/master/website/src/data/users.tsx';
function filterUsers(users, selectedTags, operator) { function filterUsers(
users: User[],
selectedTags: TagType[],
operator: Operator,
) {
if (selectedTags.length === 0) { if (selectedTags.length === 0) {
return users; return users;
} }
@ -39,7 +45,11 @@ function filterUsers(users, selectedTags, operator) {
}); });
} }
function useFilteredUsers(users, selectedTags, operator) { function useFilteredUsers(
users: User[],
selectedTags: TagType[],
operator: Operator,
) {
return useMemo(() => filterUsers(users, selectedTags, operator), [ return useMemo(() => filterUsers(users, selectedTags, operator), [
users, users,
selectedTags, selectedTags,
@ -49,11 +59,11 @@ function useFilteredUsers(users, selectedTags, operator) {
const TagQueryStringKey = 'tags'; const TagQueryStringKey = 'tags';
function readSearchTags(search) { function readSearchTags(search: string) {
return new URLSearchParams(search).getAll(TagQueryStringKey); return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[];
} }
function replaceSearchTags(search, newTags) { function replaceSearchTags(search: string, newTags: TagType[]) {
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
searchParams.delete(TagQueryStringKey); searchParams.delete(TagQueryStringKey);
newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag)); newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag));
@ -66,7 +76,7 @@ function useSelectedTags() {
const {push} = useHistory(); const {push} = useHistory();
// On SSR / first mount (hydration) no tag is selected // On SSR / first mount (hydration) no tag is selected
const [selectedTags, setSelectedTags] = useState([]); const [selectedTags, setSelectedTags] = useState<TagType[]>([]);
// Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch) // Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch)
useEffect(() => { useEffect(() => {
@ -76,7 +86,7 @@ function useSelectedTags() {
// Update the QS value // Update the QS value
const toggleTag = useCallback( const toggleTag = useCallback(
(tag) => { (tag: TagType) => {
const tags = readSearchTags(location.search); const tags = readSearchTags(location.search);
const newTags = toggleListItem(tags, tag); const newTags = toggleListItem(tags, tag);
const newSearch = replaceSearchTags(location.search, newTags); const newSearch = replaceSearchTags(location.search, newTags);
@ -106,7 +116,19 @@ function ShowcaseHeader() {
); );
} }
function ShowcaseFilters({selectedTags, toggleTag, operator, setOperator}) { interface Props {
selectedTags: TagType[];
toggleTag: (tag: TagType) => void;
operator: Operator;
setOperator: (op: Operator) => void;
}
function ShowcaseFilters({
selectedTags,
toggleTag,
operator,
setOperator,
}: Props) {
return ( return (
<div className="margin-top--l margin-bottom--md container"> <div className="margin-top--l margin-bottom--md container">
<div className="row"> <div className="row">
@ -138,7 +160,7 @@ function ShowcaseFilters({selectedTags, toggleTag, operator, setOperator}) {
name="operator" name="operator"
label="Filter: " label="Filter: "
value={operator} value={operator}
onChange={(e) => setOperator(e.target.value)}> onChange={(e) => setOperator(e.target.value as Operator)}>
<option value="OR">OR</option> <option value="OR">OR</option>
<option value="AND">AND</option> <option value="AND">AND</option>
</ShowcaseSelect> </ShowcaseSelect>
@ -148,7 +170,7 @@ function ShowcaseFilters({selectedTags, toggleTag, operator, setOperator}) {
); );
} }
function ShowcaseCards({filteredUsers}) { function ShowcaseCards({filteredUsers}: {filteredUsers: User[]}) {
return ( return (
<section className="container margin-top--lg"> <section className="container margin-top--lg">
<h2> <h2>
@ -176,7 +198,7 @@ function ShowcaseCards({filteredUsers}) {
function Showcase() { function Showcase() {
const {selectedTags, toggleTag} = useSelectedTags(); const {selectedTags, toggleTag} = useSelectedTags();
const [operator, setOperator] = useState('OR'); const [operator, setOperator] = useState<Operator>('OR');
const filteredUsers = useFilteredUsers(SortedUsers, selectedTags, operator); const filteredUsers = useFilteredUsers(SortedUsers, selectedTags, operator);
return ( return (
<Layout title={TITLE} description={DESCRIPTION}> <Layout title={TITLE} description={DESCRIPTION}>

View file

@ -17,14 +17,16 @@ import VersionsArchived from '@site/versionsArchived.json';
const VersionsArchivedList = Object.entries(VersionsArchived); const VersionsArchivedList = Object.entries(VersionsArchived);
function Version() { function Version() {
const {siteConfig} = useDocusaurusContext(); const {
siteConfig: {organizationName, projectName},
} = useDocusaurusContext();
const versions = useVersions(); const versions = useVersions();
const latestVersion = useLatestVersion(); const latestVersion = useLatestVersion();
const currentVersion = versions.find((version) => version.name === 'current'); const currentVersion = versions.find((version) => version.name === 'current');
const pastVersions = versions.filter( const pastVersions = versions.filter(
(version) => version !== latestVersion && version.name !== 'current', (version) => version !== latestVersion && version.name !== 'current',
); );
const repoUrl = `https://github.com/${siteConfig.organizationName}/${siteConfig.projectName}`; const repoUrl = `https://github.com/${organizationName}/${projectName}`;
return ( return (
<Layout <Layout
@ -77,7 +79,7 @@ function Version() {
</div> </div>
)} )}
{(pastVersions.length > 0 || VersionsArchived.length > 0) && ( {(pastVersions.length > 0 || VersionsArchivedList.length > 0) && (
<div className="margin-bottom--lg"> <div className="margin-bottom--lg">
<h3 id="archive">Past versions (Not maintained anymore)</h3> <h3 id="archive">Past versions (Not maintained anymore)</h3>
<p> <p>

View file

@ -1,6 +1,6 @@
import React from 'react'; import React, {ComponentProps} from 'react';
export const ButtonExample = (props) => ( export const ButtonExample = (props: ComponentProps<'button'>) => (
<button <button
{...props} {...props}
style={{ style={{

View file

@ -1,10 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="@docusaurus/module-type-aliases" />
/// <reference types="@docusaurus/theme-classic" />

View file

@ -1,12 +1,12 @@
// Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_difference // Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_difference
export function difference(...arrays) { export function difference<T>(...arrays: T[][]) {
return arrays.reduce((a, b) => a.filter((c) => !b.includes(c))); return arrays.reduce((a, b) => a.filter((c) => !b.includes(c)));
} }
// Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby // Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby
export function sortBy(array, getter) { export function sortBy<T>(array: T[], getter: (item: T) => unknown) {
function compareBy(getter) { function compareBy(getter: (item: T) => unknown) {
return (a, b) => return (a: T, b: T) =>
getter(a) > getter(b) ? 1 : getter(b) > getter(a) ? -1 : 0; getter(a) > getter(b) ? 1 : getter(b) > getter(a) ? -1 : 0;
} }
@ -15,7 +15,7 @@ export function sortBy(array, getter) {
return sortedArray; return sortedArray;
} }
export function toggleListItem(list, item) { export function toggleListItem<T>(list: T[], item: T) {
const itemIndex = list.indexOf(item); const itemIndex = list.indexOf(item);
if (itemIndex === -1) { if (itemIndex === -1) {
return list.concat(item); return list.concat(item);

View file

@ -1,12 +1,11 @@
{ {
// This file is not used in compilation. It is here just for a nice editor experience. // This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "paths": {
"esModuleInterop": true, "@site/*": ["./*"]
"jsx": "react", },
"lib": ["DOM"], "resolveJsonModule": true
"noEmit": true,
"noImplicitAny": false
}, },
"include": ["src/"] "include": ["src/"]
} }