Skip to content

Commit

Permalink
v2.0.0 Revised UI
Browse files Browse the repository at this point in the history
  • Loading branch information
LiamMartens committed May 14, 2021
1 parent cdd4720 commit c58bb06
Show file tree
Hide file tree
Showing 26 changed files with 790 additions and 520 deletions.
88 changes: 87 additions & 1 deletion .d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,90 @@ declare module 'config:seo-tools' {
meta_description_required?: boolean;
};
export default config;
}
}
declare module 'yoastseo' {
export class Paper {
constructor(html: string, options: {
keyword?: string;
synonyms?: string;
description?: string;
title?: string;
titleWidth?: number;
url?: string;
locale?: string;
permalink?: string;
});
}
export class Researcher {
constructor(paper: Paper);
getResearch(type: 'keyphraseLength'): number;
getResearch(type: 'metaDescriptionKeyword'): number;
getResearch(type: 'linkStatistics'): false | {
total: number;
totalNaKeyword: number;
keyword: {
totalKeyword: number;
matchedAnchors: any[];
};
internalTotal: number;
internalDofollow: number;
internalNofollow: number;
externalTotal: number;
externalDofollow: number;
externalNofollow: number;
otherTotal: number;
otherDofollow: number;
otherNofollow: number;
};
getResearch(type: 'findKeywordInFirstParagraph'): boolean;
getResearch(type: 'getKeywordDensity'): number;
getResearch(type: 'wordCountInText'): number;
getResearch(type: 'keywordCount'): false | {
count: number;
length: number;
markings: any[];
matches: string[];
};
getResearch(type: 'findKeywordInPageTitle'): false | {
allWordsFound: boolean;
exactMatchFound: boolean;
exactMatchKeyphrase: boolean;
position: number;
};
getResearch(type: 'altTagCount'): false | {
noAlt: number;
withAlt: number;
withAltKeyword: number;
withAltNonKeyword: number;
};
getResearch(type: 'pageTitleLength'): number;
getResearch(type: 'keywordCountInUrl'): false | {
keyphraseLength: number;
percentWordMatches: number;
};
getResearch(type: 'countSentencesFromText'): string[];
getResearch(type: 'passiveVoice'): false | {
passives: string[];
total: number;
};
getResearch(type: 'getSentenceBeginnings'): false | {
word: string;
count: number;
sentences: string[];
}[];
getResearch(type: 'sentences'): string[];
getResearch(type: 'calculateFleschReading'): number;
getResearch(type: 'getParagraphLength'): false | {
wordCount: number;
text: string;
}[];
getResearch(type: 'findTransitionWords'): false | {
sentenceResults: {
sentence: string;
transitionWords: string[];
}[];
totalSentences: number;
transitionWordSentences: number;
};
}
}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Changelog
## v2.0.0
* Revised SEO tools UI with `@sanity/ui`

## v1.1.2
* Include `config.dist.json` in npm package so Sanity can automatically create it.

Expand Down
13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sanity-plugin-seo-tools",
"version": "1.1.2",
"version": "2.0.0",
"license": "GPL-3.0",
"author": {
"name": "Liam Martens",
Expand Down Expand Up @@ -52,6 +52,8 @@
"@rollup/plugin-node-resolve": "^11.1.1",
"@sanity/check": "^2.0.9",
"@sanity/form-builder": "^2.3.3",
"@sanity/react-hooks": "^2.10.0",
"@sanity/ui": "^0.33.24",
"@types/react": "^17.0.1",
"node-sass": "^5.0.0",
"npm-run-all": "^4.1.5",
Expand All @@ -61,19 +63,22 @@
"rollup-plugin-cleaner": "^1.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.0",
"typescript": "^4.1.3"
"typescript": "^4.2.4"
},
"peerDependencies": {
"@sanity/form-builder": "^0.0.0",
"react": "^16.0.0"
"@sanity/react-hooks": "^2.0.0",
"@sanity/ui": "^0.0.0",
"react": "^16.0.0||^17.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.13",
"axios": "^0.21.1",
"classnames": "^2.2.6",
"classnames": "^2.3.1",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"regenerator-runtime": "^0.13.7",
"use-debounce": "^6.0.1",
"yoastseo": "^1.90.0"
}
}
19 changes: 19 additions & 0 deletions src/input/components/BoxSpinner/BoxSpinner.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.overlay {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;

&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: .6;
background-color: var(--card-bg-color);
}
}
23 changes: 23 additions & 0 deletions src/input/components/BoxSpinner/BoxSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import styles from './BoxSpinner.scss';
import classNames from 'classnames';
import { Box, Flex, Spinner } from '@sanity/ui';

type Props = {
overlay?: boolean;
}

export const BoxSpinner: React.FunctionComponent<Props> = ({ overlay }) => {
return (
<Box
padding={[4, 2]}
className={classNames({
[styles.overlay]: !!overlay,
})}
>
<Flex align='center' justify="center" style={{ height: '100%', width: '100%' }}>
<Spinner />
</Flex>
</Box>
);
}
1 change: 1 addition & 0 deletions src/input/components/BoxSpinner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BoxSpinner';
18 changes: 18 additions & 0 deletions src/input/components/InsightResult/InsightResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { Card, Flex, Text } from '@sanity/ui';

type Props = {
valid?: boolean;
warning?: boolean;
children?: React.ReactNode | ((valid: boolean) => React.ReactNode);
}

export const InsightResult = ({ valid, warning, children }: Props) => {
return (
<Card border padding={[3]} tone={!valid ? 'critical' : (warning ? 'caution' : undefined)}>
<Flex justify="flex-start" align="center">
<Text>{typeof children === 'function' ? children(!!valid) : children}</Text>
</Flex>
</Card>
)
}
1 change: 1 addition & 0 deletions src/input/components/InsightResult/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './InsightResult';
12 changes: 12 additions & 0 deletions src/input/components/SEOInput/SEOInput.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
:root {
--seo-tools--color--dark: #000;
--seo-tools--color--light: #fff;
--seo-tools--color--gray: #f1f1f1;
--seo-tools--color-accent: #156dff;
}

.seotools {
border-bottom: .1rem solid var(--seo-tools--color--gray);
padding-bottom: 2rem;
margin-bottom: 4rem;
}
149 changes: 149 additions & 0 deletions src/input/components/SEOInput/SEOInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React from 'react';
import styles from './SEOInput.scss';
import PatchEvent from '@sanity/form-builder/lib/PatchEvent';
import {
studioTheme,
Card,
Container,
Heading,
ThemeProvider,
Stack,
TabList,
Tab,
} from '@sanity/ui';
import { BoxSpinner } from '../BoxSpinner';
import { Tabs } from '../../constants';
import { getRawContent } from '../../utils/getRawContent';
import { useDebouncedCallback } from 'use-debounce';
import { SEOInputSeoTab } from './SEOInputSeoTab';
import { getYoastInsightsForContent } from '../../utils';
import { SEOInputReadabilityTab } from './SEOInputReadabilityTab';
import { IField, ISEOOptions, IType } from '../../../types';
import { FormBuilderInput } from 'part:@sanity/form-builder';
import { setIfMissing } from 'part:@sanity/form-builder/patch-event';

type Props = {
document: any & { _type: string; };
type: IType<ISEOOptions>;
value?: {
focus_keyword?: string;
focus_synonyms?: string[];
};
onChange: (...args: any[]) => void;
onBlur: (...args: any[]) => void;
onFocus: (...args: any[]) => void;
}

const SEOInputComponent: React.FunctionComponent<Props> = ({ type, value, document }) => {
const YoastSEO = React.useRef(require('yoastseo') as typeof import('yoastseo'));
const [initialAudit, setInitialAudit] = React.useState(true);
const [auditOngoing, setAuditOngoing] = React.useState(false);
const [selectedTab, setSelectedTab] = React.useState(Tabs.SEO);
const [yoastInsights, setYoastInsights] = React.useState<ReturnType<typeof getYoastInsightsForContent> | null>(null);

const performYoastCheck = useDebouncedCallback(async () => {
setAuditOngoing(true);
try {
const { options } = type;
const { slug, url, title, description, langCulture, rawContent } = await getRawContent(document, {
baseUrl: options.baseUrl,
fetchRemote: options.fetchRemote,
contentSelector: options.contentSelector,
slug: options.slug,
content: options.content,
title: options.title,
description: options.description,
locale: options.locale,
});
const insights = getYoastInsightsForContent(YoastSEO.current, rawContent, {
keyword: value?.focus_keyword || '',
synonyms: value?.focus_synonyms || [],
title,
description,
langCulture,
url: slug, permalink: url,
});
setYoastInsights(insights);
} catch (err) {
console.error(err);
} finally {
setInitialAudit(false);
setAuditOngoing(false);
}
}, 2000);

React.useEffect(() => {
performYoastCheck();
});

return (
<Container>
<Card style={{ position: 'relative' }} padding={4} shadow={2}>
<Stack space={4}>
<Heading as="h3" size={1}>SEO Tools</Heading>
<Container>
{initialAudit ? (
<BoxSpinner />
) : (
<>
{auditOngoing && (
<BoxSpinner overlay />
)}
<TabList space={1}>
<Tab id={Tabs.SEO} aria-controls={Tabs.SEO} label="SEO" selected={selectedTab === Tabs.SEO} onClick={() => setSelectedTab(Tabs.SEO)} />
<Tab id={Tabs.READABILITY} aria-controls={Tabs.READABILITY} label="Readability" selected={selectedTab === Tabs.READABILITY} onClick={() => setSelectedTab(Tabs.READABILITY)} />
</TabList>
<SEOInputSeoTab value={value} hidden={selectedTab !== Tabs.SEO} insights={yoastInsights ?? undefined} />
<SEOInputReadabilityTab value={value} hidden={selectedTab !== Tabs.READABILITY} insights={yoastInsights ?? undefined} />
</>
)}
</Container>
</Stack>
</Card>
</Container>
);
}

export class SEOInput extends React.Component<Props> {
public focus() {
// @README irrelevant
}

private handleFieldChange = (field: IField, fieldPatchEvent: PatchEvent) => {
const { type, onChange } = this.props
onChange(fieldPatchEvent.prefixAll(field.name).prepend(setIfMissing({_type: type.name})))
}

public render() {
const { type, value, onFocus, onBlur, onChange } = this.props;
const { fields } = type;

return (
<ThemeProvider theme={studioTheme}>
<Stack space={4}>
<SEOInputComponent {...this.props} />
<Container>
<Card style={{ position: 'relative' }} padding={4} shadow={2}>
<Stack space={3}>
{fields.map(field => (
<div key={field.name}>
<FormBuilderInput
key={field.name}
type={field.type}
level={type.level}
path={[field.name]}
value={value ? value[field.name as keyof typeof value] : undefined}
onChange={(patchEvent: any) => this.handleFieldChange(field, patchEvent)}
onBlur={onBlur}
onFocus={onFocus}
/>
</div>
))}
</Stack>
</Card>
</Container>
</Stack>
</ThemeProvider>
);
}
}
Loading

0 comments on commit c58bb06

Please sign in to comment.