Skip to content

Commit

Permalink
mockup rendering of link checking result
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielmfern committed Dec 4, 2024
1 parent 737d686 commit a34d385
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 55 deletions.
50 changes: 19 additions & 31 deletions packages/react-email/src/actions/email-validation/check-links.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use server';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import http from 'node:http';
import { promises as fs } from 'node:fs';
import traverse from '@babel/traverse';
import { Passero_One } from 'next/font/google';
import { getEmailPathFromSlug } from '../get-email-path-from-slug';
import { getLineAndColumnFromIndex } from './get-line-and-column-from-index';
import { parseCode } from './parse-code';

Expand All @@ -23,21 +26,22 @@ const doesURLSucceedResponse = async (url: URL) => {
return succeedingURLs.get(url)!;
};

interface LinkCheck {
type: 'syntax' | 'security' | 'response-code';
status: 'failed' | 'passed';
}

interface LinkCheckingResult {
export interface LinkCheckingResult {
link: string;

line: number;
column: number;

checks: LinkCheck[];
checks: {
syntax: 'failed' | 'passed';
security?: 'failed' | 'passed';
responseCode?: 'failed' | 'passed';
};
}

export const checkLinks = async (code: string) => {
export const checkLinks = async (emailSlug: string) => {
const emailPath = await getEmailPathFromSlug(emailSlug);
const code = await fs.readFile(emailPath, 'utf8');
const ast = parseCode(code);

const linkCheckingResults: LinkCheckingResult[] = [];
Expand All @@ -49,11 +53,10 @@ export const checkLinks = async (code: string) => {
}[] = [];

traverse(ast, {
// <a href="...">
JSXOpeningElement(nodePath) {
if (
nodePath.node.name.type !== 'JSXIdentifier' ||
nodePath.node.name.name !== 'a'
(nodePath.node.name.name !== 'a' && nodePath.node.name.name !== 'Link')
)
return;

Expand Down Expand Up @@ -95,34 +98,19 @@ export const checkLinks = async (code: string) => {
continue;
}

if (link.startsWith('/')) {
continue;
}

const checks: LinkCheck[] = [];
const checks: LinkCheckingResult['checks'] = {
syntax: 'passed',
};

try {
const url = new URL(link);
checks.push({
type: 'syntax',
status: 'passed',
});

const hasSucceeded = await doesURLSucceedResponse(url);
checks.push({
type: 'response-code',
status: hasSucceeded ? 'passed' : 'failed',
});
checks.responseCode = hasSucceeded ? 'passed' : 'failed';

checks.push({
type: 'security',
status: link.startsWith('https://') ? 'passed' : 'failed',
});
checks.security = link.startsWith('https://') ? 'passed' : 'failed';
} catch (exception) {
checks.push({
type: 'syntax',
status: 'failed',
});
checks.syntax = 'failed';
}

linkCheckingResults.push({
Expand Down
79 changes: 79 additions & 0 deletions packages/react-email/src/components/sidebar/link-checker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import clsx from 'clsx';
import {
checkLinks,
type LinkCheckingResult,
} from '../../actions/email-validation/check-links';
import { Button } from '../button';

const checkingResultsCache = new Map<string, LinkCheckingResult[]>();

interface LinkCheckerProps {
currentEmailOpenSlug: string;
}

export const LinkChecker = ({ currentEmailOpenSlug }: LinkCheckerProps) => {
const [results, setResults] = React.useState<
LinkCheckingResult[] | undefined
>(checkingResultsCache.get(currentEmailOpenSlug));

const handleRun = () => {
checkLinks(currentEmailOpenSlug)
.then((newResults) => {
checkingResultsCache.set(currentEmailOpenSlug, newResults);
setResults(newResults);
})
.catch((exception) => {
throw exception;
});
};

console.log(results);

return (
<div className="flex flex-col text-center p-2 gap-3 w-[calc(100vw-36px)] h-full lg:w-full lg:min-w-[231px] lg:max-w-[231px]">
<h2 className="text-xl">Link Checker</h2>
{results ? (
<>
<ol className="list-none p-0">
{results.map((result) => (
<LinkCheckingResultView {...result} key={result.link} />
))}
</ol>
<Button className="w-fit mt-auto mr-auto" onClick={handleRun}>
Re-run
</Button>
</>
) : (
<>
<span className="text-gray-300 text-sm">
Check if all links are valid and going to the right pages
</span>
<Button className="w-fit mx-auto" onClick={handleRun}>
Run
</Button>
</>
)}
</div>
);
};

const LinkCheckingResultView = (props: LinkCheckingResult) => {
let status: 'success' | 'error' | 'warning' = 'success';
if (props.checks.security === 'failed') {
status = 'warning';
}
if (props.checks.syntax === 'failed') {
status = 'error';
}
if (props.checks.responseCode === 'failed') {
status = 'error';
}
return (
<li className="group" data-status={status}>
<div className="data-[status=error]:text-red-400 data-[status=warning]:text-yellow-300 data-[status=success]:text-green-400">
{props.link}
</div>
</li>
);
};
55 changes: 31 additions & 24 deletions packages/react-email/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Logo } from '../logo';
import { IconEmail } from '../icons/icon-email';
import { IconLink } from '../icons/icon-link';
import { SidebarDirectoryChildren } from './sidebar-directory-children';
import { LinkChecker } from './link-checker';

interface SidebarProps {
className?: string;
Expand All @@ -21,25 +22,28 @@ interface SidebarTabTriggerProps {
className?: string;
children?: React.ReactNode;

disabled?: boolean;
tabValue: SidebarTab;
activeTabValue: SidebarTab;
}

type SidebarTab = 'email-templates' | 'link-checker';
type SidebarTab = 'file-tree' | 'link-checker';

const SidebarTabTrigger = ({
children,
className,
tabValue,
disabled,
activeTabValue,
}: SidebarTabTriggerProps) => {
return (
<Tabs.Trigger
className={clsx(
'w-[40px] h-[40px] flex items-center justify-center bg-transparent data-[active=true]:bg-slate-6 text-white',
'w-[40px] h-[40px] disabled:bg-slate-4 disabled:text-slate-10 flex items-center justify-center bg-transparent data-[active=true]:bg-slate-6 text-white',
className,
)}
data-active={tabValue === activeTabValue}
disabled={disabled}
value={tabValue}
>
{children}
Expand All @@ -53,7 +57,7 @@ export const Sidebar = ({
style,
}: SidebarProps) => {
const [activeTabValue, setActiveTabValue] =
React.useState<SidebarTab>('email-templates');
React.useState<SidebarTab>('file-tree');
const { emailsDirectoryMetadata } = useEmails();

return (
Expand All @@ -69,17 +73,18 @@ export const Sidebar = ({
className={cn('border-r flex flex-col border-slate-6', className)}
style={{ ...style }}
>
<div className="flex border-t border-slate-6 h-[calc(100vh_-_70px)]">
<div className="flex border-t border-slate-6 h-screen">
<Tabs.List className="flex flex-col w-[40px] h-full border-r border-slate-6">
<SidebarTabTrigger
activeTabValue={activeTabValue}
tabValue="email-templates"
tabValue="file-tree"
>
<IconEmail height="24" width="24" />
</SidebarTabTrigger>
<SidebarTabTrigger
activeTabValue={activeTabValue}
className="relative"
disabled={currentEmailOpenSlug === undefined}
tabValue="link-checker"
>
<IconLink height="24" width="24" />
Expand All @@ -89,25 +94,27 @@ export const Sidebar = ({
<div className="p-4 h-[70px] flex-shrink items-center hidden lg:flex">
<Logo />
</div>
{/*activeTabValue === 'link-checker' && emailValidationWarnings ? (
<WarningsView warnings={emailValidationWarnings} />
) : null*/}
{activeTabValue === 'email-templates' ? (
<div className="flex flex-col w-[calc(100vw-36px)] h-full lg:w-full lg:min-w-[231px] lg:max-w-[231px]">
<nav className="p-4 flex-grow lg:pt-0 pl-0 w-full flex flex-col overflow-y-auto">
<Collapsible.Root>
<React.Suspense>
<SidebarDirectoryChildren
currentEmailOpenSlug={currentEmailOpenSlug}
emailsDirectoryMetadata={emailsDirectoryMetadata}
isRoot
open
/>
</React.Suspense>
</Collapsible.Root>
</nav>
</div>
) : null}
<div className="h-[calc(100vh-70px)]">
{activeTabValue === 'link-checker' && currentEmailOpenSlug ? (
<LinkChecker currentEmailOpenSlug={currentEmailOpenSlug} />
) : null}
{activeTabValue === 'file-tree' ? (
<div className="flex flex-col w-[calc(100vw-36px)] h-full lg:w-full lg:min-w-[231px] lg:max-w-[231px]">
<nav className="p-4 flex-grow lg:pt-0 pl-0 w-full flex flex-col overflow-y-auto">
<Collapsible.Root>
<React.Suspense>
<SidebarDirectoryChildren
currentEmailOpenSlug={currentEmailOpenSlug}
emailsDirectoryMetadata={emailsDirectoryMetadata}
isRoot
open
/>
</React.Suspense>
</Collapsible.Root>
</nav>
</div>
) : null}
</div>
</div>
</div>
</aside>
Expand Down

0 comments on commit a34d385

Please sign in to comment.