Skip to content

Commit

Permalink
Merge pull request #118 from Aryan51203/main
Browse files Browse the repository at this point in the history
Improvements to loading and CORS
  • Loading branch information
Divide-By-0 authored Oct 11, 2024
2 parents ace8c78 + 95bbed7 commit 8fbfa28
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 104 deletions.
62 changes: 34 additions & 28 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
/** @type {import('next').NextConfig} */

const cspValue = [
"default-src 'self'",
"style-src 'self' 'unsafe-inline'",
"frame-ancestors 'none'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"img-src 'self' https://authjs.dev/ data:", // https://authjs.dev/ for images during the login flow
"connect-src 'self' https://*.alchemy.com", // https://*.alchemy.com used by Witness
]
"default-src 'self'",
"style-src 'self' 'unsafe-inline'",
"frame-ancestors 'none'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"img-src 'self' https://authjs.dev/ data:", // https://authjs.dev/ for images during the login flow
"connect-src 'self' https://*.alchemy.com", // https://*.alchemy.com used by Witness
];

const nextConfig = {
async headers() {
return [{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspValue.join('; '),
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
],
}]
},
}
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*",
},
{
key: "Content-Security-Policy",
value: cspValue.join("; "),
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
],
},
];
},
};

module.exports = nextConfig
module.exports = nextConfig;
5 changes: 3 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { NextAuthProvider } from './session-provider'
import Link from 'next/link'

const inter = Inter({ subsets: ['latin'] })

Expand Down Expand Up @@ -36,7 +37,7 @@ export default function RootLayout({
borderBottom: '1px solid #aaa',
display: 'flex',
}}>
<a href='/'
<Link href='/'
className='defaultcolor'
style={{ display: 'flex', fontWeight: 600 }}
>
Expand All @@ -45,7 +46,7 @@ export default function RootLayout({
alt="Proof of Email logotype"
style={{ width: '2.5rem', paddingRight: '0.5rem' }} />
DKIM Archive
</a>
</Link>
<DevModeNotice />
</header>
<main style={{ margin: '0.5rem', alignItems: 'center', display: 'flex', flexDirection: 'column' }}>
Expand Down
16 changes: 16 additions & 0 deletions src/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Skeleton } from "@mui/material";

export default function Loading() {
const skeletonCount = 4; // Number of Skeleton components to render
const skeletons = Array.from({ length: skeletonCount }, (_, index) => (
<Skeleton
key={index}
sx={{ bgcolor: "grey.350" }}
variant="rounded"
width={"42rem"}
height={"20rem"}
style={{ margin: "1rem" }}
/>
));
return <div style={{ marginTop: "4rem" }}>{skeletons}</div>;
}
73 changes: 40 additions & 33 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
import { DomainSearchResults } from '@/components/DomainSearchResults';
import { SearchInput } from '@/components/SearchInput';
import { findKeysPaginated } from './actions';
import { parseDkimTagList } from '@/lib/utils';
"use client";

function dkimValueHasPrivateKey(dkimValue: string): boolean {
return !!parseDkimTagList(dkimValue).p;
}

export default async function Home({ searchParams }: {
searchParams: { [key: string]: string | string[] | undefined }
}) {
const domainQuery = searchParams?.domain?.toString();
let records = domainQuery ? await findKeysPaginated(domainQuery, null) : []
records = records.filter((record) => dkimValueHasPrivateKey(record.value));

return (
<div style={{ display: 'flex', minHeight: '100vh', flexDirection: 'column', alignItems: 'center' }}>
<h2 style={{ padding: '2rem' }}>
<a href='/' className='defaultcolor'>DKIM Archive</a>
</h2>
<SearchInput domainQuery={domainQuery} />
<DomainSearchResults initialRecords={records} domainQuery={domainQuery} />
import DomainSearchResults from "@/components/DomainSearchResults";
import { SearchInput } from "@/components/SearchInput";
import Link from "next/link";
import { useState } from "react";

<div style={{ textAlign: 'center', marginTop: '5rem', fontSize: '0.8rem' }}>
<hr style={{ width: '50%', margin: '1rem auto', borderTop: '1px solid black' }} />
<div><a href="about">About</a> this site</div>
<div>Visit the project on <a href="https://github.com/zkemail/archive.prove.email">GitHub</a></div>
<div>Visit <a href="https://prove.email/">Proof of Email</a></div>
<div><a href="contribute">Contribute</a> to the archive</div>
<div>Explore the <a href="api-explorer">API</a></div>
<div>Read the <a href="privacy-policy">Privacy policy</a></div>
</div>
</div>
)
export default function Home({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) {
const domainQuery = searchParams?.domain?.toString();
const [isLoading, setIsLoading] = useState(true);
return (
<div style={{ display: "flex", minHeight: "100vh", flexDirection: "column", alignItems: "center" }}>
<h2 style={{ padding: "2rem" }}>
<Link href="/" className="defaultcolor">
DKIM Archive
</Link>
</h2>
<SearchInput domainQuery={domainQuery} setIsLoading={setIsLoading} />
<DomainSearchResults domainQuery={domainQuery} isLoading={isLoading} setIsLoading={setIsLoading} />
<div style={{ textAlign: "center", marginTop: "5rem", fontSize: "0.8rem" }}>
<hr style={{ width: "50%", margin: "1rem auto", borderTop: "1px solid black" }} />
<div>
<a href="about">About</a> this site
</div>
<div>
Visit the project on <a href="https://github.com/zkemail/archive.prove.email">GitHub</a>
</div>
<div>
Visit <a href="https://prove.email/">Proof of Email</a>
</div>
<div>
<a href="contribute">Contribute</a> to the archive
</div>
<div>
Explore the <a href="api-explorer">API</a>
</div>
<div>
Read the <a href="privacy-policy">Privacy policy</a>
</div>
</div>
</div>
);
}
78 changes: 39 additions & 39 deletions src/components/DomainSearchResults.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,59 @@
"use client";
import { RecordWithSelector } from "@/lib/db";
import { SelectorResult } from "./SelectorResult";
import React, { useEffect } from "react";

import { findKeysPaginated } from "@/app/actions";
import { useInView } from "react-intersection-observer";
import Loading from "@/app/loading";
import { RecordWithSelector } from "@/lib/db";
import { parseDkimTagList } from "@/lib/utils";
import { useEffect, useState } from "react";
import { DomainSearchResultsDisplay } from "./DomainSearchResultsDisplay";

interface DomainSearchResultProps {
interface DomainSearchResultsProps {
domainQuery: string | undefined;
initialRecords: RecordWithSelector[];
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
}

function dkimValueHasPrivateKey(dkimValue: string): boolean {
return !!parseDkimTagList(dkimValue).p;
}

export const DomainSearchResults: React.FC<DomainSearchResultProps> = ({ domainQuery, initialRecords }) => {
const [cursor, setCursor] = React.useState<number | null>(initialRecords[initialRecords.length - 1]?.id);
const [records, setRecords] = React.useState(initialRecords);
const { ref: inViewElement, inView } = useInView();
async function DomainResultsLoader(domainQuery: string | undefined) {
let records = null;
records = domainQuery ? await findKeysPaginated(domainQuery, null) : [];
records = records.filter((record) => dkimValueHasPrivateKey(record.value));
return records;
}

useEffect(() => {
setCursor(initialRecords[initialRecords.length - 1]?.id);
setRecords(initialRecords);
}, [domainQuery]);
function DomainSearchResults({ domainQuery, isLoading, setIsLoading }: DomainSearchResultsProps) {
// const [isLoading, setIsLoading] = useState(true);
const [records, setRecords] = useState<RecordWithSelector[]>([]);
const [cursor, setCursor] = useState<number | null>(null);

useEffect(() => {
if (inView) {
loadMore();
setIsLoading(true);
async function loadRecords() {
const newRecords = await DomainResultsLoader(domainQuery);
setRecords(newRecords);
setCursor(newRecords[newRecords.length - 1]?.id);
setIsLoading(false);
}
}, [inView]);

loadRecords();
}, [domainQuery]);

async function loadMore() {
if (!cursor) {
return;
}

let newRecords = domainQuery ? await findKeysPaginated(domainQuery, cursor) : [];

if (!newRecords.length) {
// If no new records are found, stop further loading
setCursor(null);
return;
}

let lastCursor = newRecords[newRecords.length - 1]?.id;

if (lastCursor === cursor) {
setCursor(null);
return;
Expand All @@ -57,24 +70,11 @@ export const DomainSearchResults: React.FC<DomainSearchResultProps> = ({ domainQ
setRecords(uniqueRecords);
}

if (!domainQuery) {
return <div>Enter a search term</div>;
}
if (!records?.length) {
return <div>No records found for "{domainQuery}"</div>;
}
return (
<div>
<p>
Search results for <b>{domainQuery}</b>
</p>
<div>
{records.map((record) => (
<SelectorResult key={record.id} record={record} />
))}
</div>
{!cursor && <p>No more records</p>}
<div ref={inViewElement} />
</div>
return isLoading ? (
<Loading />
) : (
<DomainSearchResultsDisplay records={records} domainQuery={domainQuery} loadMore={loadMore} cursor={cursor} />
);
};
}

export default DomainSearchResults;
48 changes: 48 additions & 0 deletions src/components/DomainSearchResultsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";
import { RecordWithSelector } from "@/lib/db";
import React, { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { SelectorResult } from "./SelectorResult";

interface DomainSearchResultProps {
domainQuery: string | undefined;
records: RecordWithSelector[];
loadMore: () => void;
cursor: number | null;
}

export const DomainSearchResultsDisplay: React.FC<DomainSearchResultProps> = ({
domainQuery,
records,
loadMore,
cursor,
}) => {
const { ref: inViewElement, inView } = useInView();

useEffect(() => {
if (inView) {
loadMore();
}
}, [inView]);

if (!domainQuery) {
return <div>Enter a search term</div>;
}
if (!records?.length) {
return <div>No records found for "{domainQuery}"</div>;
}
return (
<div>
<p>
Search results for <b>{domainQuery}</b>
</p>
<div>
{records.map((record) => (
<SelectorResult key={record.id} record={record} />
))}
</div>
{!cursor && <p>No more records</p>}
<div ref={inViewElement} />
</div>
);
};
6 changes: 4 additions & 2 deletions src/components/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import debounce from "lodash/debounce"; // Importing lodash's debounce

interface SearchFormProps {
domainQuery: string | undefined;
setIsLoading: (isLoading: boolean) => void;
}

export const SearchInput: React.FC<SearchFormProps> = ({ domainQuery }) => {
export const SearchInput: React.FC<SearchFormProps> = ({ domainQuery, setIsLoading }) => {
const router = useRouter();
const [searchResults, setSearchResults] = useState<AutocompleteResults>([]);
const [inputValue, setInputValue] = useState<string>(domainQuery || "");
Expand All @@ -20,7 +21,7 @@ export const SearchInput: React.FC<SearchFormProps> = ({ domainQuery }) => {
const results = await autocomplete(value);
setSearchResults(results);
}
}, 500),
}, 200),
[]
);

Expand All @@ -37,6 +38,7 @@ export const SearchInput: React.FC<SearchFormProps> = ({ domainQuery }) => {

const onChange = (_event: React.SyntheticEvent, value: string | null) => {
if (value) {
setIsLoading(true);
router.push(`/?domain=${value}`);
}
};
Expand Down

0 comments on commit 8fbfa28

Please sign in to comment.