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": {
"@tsconfig/docusaurus": "^1.0.3",
"cross-env": "^7.0.3",
"raw-loader": "^4.0.2"
}

View file

@ -5,11 +5,17 @@
* 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';
function BrowserWindow({children, minHeight, url}) {
interface Props {
children: ReactNode;
minHeight: number;
url: string;
}
function BrowserWindow({children, minHeight, url}: Props) {
return (
<div className={styles.browserWindow} style={{minHeight}}>
<div className={styles.browserWindowHeader}>

View file

@ -6,17 +6,23 @@
*/
import React, {useState} from 'react';
import Color from 'color';
import CodeBlock from '@theme/CodeBlock';
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': {
adjustment: 0,
adjustmentInput: 0,
adjustmentInput: '0',
displayOrder: 3,
codeOrder: 0,
},
@ -60,7 +66,7 @@ const COLOR_SHADES = {
const DEFAULT_PRIMARY_COLOR = '3578e5';
function ColorGenerator({children, minHeight, url}) {
function ColorGenerator() {
const [baseColor, setBaseColor] = useState(DEFAULT_PRIMARY_COLOR);
const [shades, setShades] = useState(COLOR_SHADES);
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 (
<div className="col col--6 margin-bottom--lg">
<div className={clsx('card')}>

View file

@ -5,11 +5,11 @@
* 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 Link from '@docusaurus/Link';
function WebsiteLink({to, children}) {
function WebsiteLink({to, children}: {to: string; children?: ReactNode}) {
return (
<Link to={to}>
{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 (
<div className={className}>
<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 (
<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 clsx from 'clsx';
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';
function TagIcon({label, description, icon}) {
function TagIcon({label, description, icon}: Tag) {
return (
<span
className={styles.tagIcon}
@ -24,7 +24,7 @@ function TagIcon({label, description, icon}) {
);
}
function ShowcaseCardTagIcons({tags}) {
function ShowcaseCardTagIcons({tags}: {tags: TagType[]}) {
const tagObjects = tags
.map((tag) => ({tag, ...Tags[tag]}))
.filter((tagObject) => !!tagObject.icon);
@ -34,12 +34,16 @@ function ShowcaseCardTagIcons({tags}) {
TagList.indexOf(tagObject.tag),
);
return tagObjectsSorted.map((tagObject, index) => (
<TagIcon key={index} {...tagObject} />
));
return (
<>
{tagObjectsSorted.map((tagObject, index) => (
<TagIcon key={index} {...tagObject} />
))}
</>
);
}
const ShowcaseCard = memo(function ({user}) {
const ShowcaseCard = memo(function ({user}: {user: User}) {
return (
<div key={user.title} className="col col--4 margin-bottom--lg">
<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.
*/
import React from 'react';
import React, {ComponentProps} from 'react';
import styles from './styles.module.css';
function ShowcaseSelect({tag, label, onChange, value, children}) {
const id = `showcase_select_id_${tag};`;
interface Props extends ComponentProps<'select'> {
label: string;
}
function ShowcaseSelect({label, ...props}: Props) {
const id = `showcase_select_id_${props.name};`;
return (
<div className={styles.selectContainer}>
<label htmlFor={id}>{label}</label>
<select id={id} name={tag} onChange={onChange} value={value}>
{children}
<select id={id} {...props}>
{props.children}
</select>
</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
// Available tags to assign to your site
// 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
favorite: {
label: 'Favorite',
@ -123,7 +151,7 @@ export const Tags = {
// Add your site to this list
// prettier-ignore
const Users = [
const Users: User[] = [
{
title: 'Aide Jeune',
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() {
let result = Users;
// Sort by site name
@ -1289,7 +1317,7 @@ function sortUsers() {
export const SortedUsers = sortUsers();
// Fail-fast on common errors
function ensureUserValid(user) {
function ensureUserValid(user: User) {
function checkFields() {
const keys = Object.keys(user);
const validKeys = [
@ -1346,7 +1374,11 @@ function ensureUserValid(user) {
}
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)}]`);
}
const unknownTags = difference(user.tags, TagList);

View file

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

View file

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

View file

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

View file

@ -16,14 +16,20 @@ import clsx from 'clsx';
import {useHistory, useLocation} from '@docusaurus/router';
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 DESCRIPTION = 'List of websites people are building with Docusaurus';
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) {
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), [
users,
selectedTags,
@ -49,11 +59,11 @@ function useFilteredUsers(users, selectedTags, operator) {
const TagQueryStringKey = 'tags';
function readSearchTags(search) {
return new URLSearchParams(search).getAll(TagQueryStringKey);
function readSearchTags(search: string) {
return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[];
}
function replaceSearchTags(search, newTags) {
function replaceSearchTags(search: string, newTags: TagType[]) {
const searchParams = new URLSearchParams(search);
searchParams.delete(TagQueryStringKey);
newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag));
@ -66,7 +76,7 @@ function useSelectedTags() {
const {push} = useHistory();
// 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)
useEffect(() => {
@ -76,7 +86,7 @@ function useSelectedTags() {
// Update the QS value
const toggleTag = useCallback(
(tag) => {
(tag: TagType) => {
const tags = readSearchTags(location.search);
const newTags = toggleListItem(tags, tag);
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 (
<div className="margin-top--l margin-bottom--md container">
<div className="row">
@ -138,7 +160,7 @@ function ShowcaseFilters({selectedTags, toggleTag, operator, setOperator}) {
name="operator"
label="Filter: "
value={operator}
onChange={(e) => setOperator(e.target.value)}>
onChange={(e) => setOperator(e.target.value as Operator)}>
<option value="OR">OR</option>
<option value="AND">AND</option>
</ShowcaseSelect>
@ -148,7 +170,7 @@ function ShowcaseFilters({selectedTags, toggleTag, operator, setOperator}) {
);
}
function ShowcaseCards({filteredUsers}) {
function ShowcaseCards({filteredUsers}: {filteredUsers: User[]}) {
return (
<section className="container margin-top--lg">
<h2>
@ -176,7 +198,7 @@ function ShowcaseCards({filteredUsers}) {
function Showcase() {
const {selectedTags, toggleTag} = useSelectedTags();
const [operator, setOperator] = useState('OR');
const [operator, setOperator] = useState<Operator>('OR');
const filteredUsers = useFilteredUsers(SortedUsers, selectedTags, operator);
return (
<Layout title={TITLE} description={DESCRIPTION}>

View file

@ -17,14 +17,16 @@ import VersionsArchived from '@site/versionsArchived.json';
const VersionsArchivedList = Object.entries(VersionsArchived);
function Version() {
const {siteConfig} = useDocusaurusContext();
const {
siteConfig: {organizationName, projectName},
} = useDocusaurusContext();
const versions = useVersions();
const latestVersion = useLatestVersion();
const currentVersion = versions.find((version) => version.name === 'current');
const pastVersions = versions.filter(
(version) => version !== latestVersion && version.name !== 'current',
);
const repoUrl = `https://github.com/${siteConfig.organizationName}/${siteConfig.projectName}`;
const repoUrl = `https://github.com/${organizationName}/${projectName}`;
return (
<Layout
@ -77,7 +79,7 @@ function Version() {
</div>
)}
{(pastVersions.length > 0 || VersionsArchived.length > 0) && (
{(pastVersions.length > 0 || VersionsArchivedList.length > 0) && (
<div className="margin-bottom--lg">
<h3 id="archive">Past versions (Not maintained anymore)</h3>
<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
{...props}
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
export function difference(...arrays) {
export function difference<T>(...arrays: T[][]) {
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
export function sortBy(array, getter) {
function compareBy(getter) {
return (a, b) =>
export function sortBy<T>(array: T[], getter: (item: T) => unknown) {
function compareBy(getter: (item: T) => unknown) {
return (a: T, b: T) =>
getter(a) > getter(b) ? 1 : getter(b) > getter(a) ? -1 : 0;
}
@ -15,7 +15,7 @@ export function sortBy(array, getter) {
return sortedArray;
}
export function toggleListItem(list, item) {
export function toggleListItem<T>(list: T[], item: T) {
const itemIndex = list.indexOf(item);
if (itemIndex === -1) {
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.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"jsx": "react",
"lib": ["DOM"],
"noEmit": true,
"noImplicitAny": false
"paths": {
"@site/*": ["./*"]
},
"resolveJsonModule": true
},
"include": ["src/"]
}