Skip to content

Commit 22a7670

Browse files
authored
Merge pull request #4251 from cpinitiative/auto_add
support getting metadata from url
2 parents 00ed3ba + de7a362 commit 22a7670

14 files changed

+3393
-91
lines changed

package.json

+37-39
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "A free collection of curated, high-quality resources to take you from Bronze to Platinum and beyond.",
55
"version": "0.1.0",
66
"author": "Nathan Wang <nathan.r.wang@gmail.com>",
7-
"dependencies": {
7+
"devDependencies": {
88
"@babel/core": "^7.18.5",
99
"@babel/plugin-syntax-typescript": "^7.17.12",
1010
"@babel/preset-env": "^7.18.2",
@@ -13,32 +13,54 @@
1313
"@dnd-kit/sortable": "^4.0.0",
1414
"@headlessui/react": "^1.6.4",
1515
"@heroicons/react": "^1.0.6",
16+
"@mdx-js/loader": "^1.6.22",
1617
"@monaco-editor/react": "^4.4.5",
1718
"@sentry/gatsby": "^7.2.0",
19+
"@storybook/addon-a11y": "^6.5.16",
20+
"@storybook/addon-actions": "^6.5.16",
21+
"@storybook/addon-essentials": "^6.5.16",
22+
"@storybook/addon-links": "^6.5.16",
23+
"@storybook/addon-postcss": "^2.0.0",
1824
"@storybook/builder-webpack5": "^6.5.16",
1925
"@storybook/manager-webpack5": "^6.5.16",
26+
"@storybook/react": "^6.5.16",
2027
"@tailwindcss/aspect-ratio": "^0.4.0",
2128
"@tailwindcss/forms": "^0.5.2",
2229
"@tailwindcss/typography": "^0.5.2",
2330
"@tailwindcss/ui": "^0.7.2",
31+
"@testing-library/jest-dom": "^5.14.1",
32+
"@testing-library/react": "^12.1.0",
2433
"@tippyjs/react": "^4.2.5",
34+
"@types/bad-words": "^3.0.1",
2535
"@types/reach__router": "^1.3.10",
36+
"@types/react": "^17.0.37",
2637
"@types/react-calendar-heatmap": "^1.6.6",
2738
"@types/seedrandom": "^3.0.8",
2839
"@types/styled-components": "^5.1.34",
40+
"@typescript-eslint/eslint-plugin": "^5.28.0",
41+
"@typescript-eslint/parser": "^5.28.0",
2942
"algoliasearch": "^4.10.5",
30-
"axios": "^1.6.2",
43+
"babel-jest": "^28.1.1",
44+
"babel-loader": "^8.2.5",
3145
"babel-plugin-remove-graphql-queries": "^4.16.0",
3246
"babel-plugin-styled-components": "^2.0.7",
47+
"babel-preset-gatsby": "^2.16.0",
48+
"babel-preset-react-app": "^10.0.1",
3349
"bad-words": "^3.0.4",
50+
"blob-polyfill": "^5.0.20210201",
3451
"broken-link-checker": "^0.7.8",
3552
"buffer": "^6.0.3",
53+
"bundlewatch": "^0.3.2",
54+
"chromatic": "^5.9.2",
3655
"classnames": "^2.3.1",
3756
"cssnano": "^5.0.8",
3857
"dayjs": "^1.11.3",
3958
"diff-match-patch": "^1.0.5",
4059
"dotenv": "^16.3.1",
60+
"eslint-plugin-jest": "^24.4.0",
4161
"firebase": "9.8.3",
62+
"firebase-admin": "^9.11.1",
63+
"firestore-export-import": "^0.17.0",
4264
"gatsby": "^4.25.7",
4365
"gatsby-image": "^3.11.0",
4466
"gatsby-plugin-algolia": "^0.24.0",
@@ -56,22 +78,29 @@
5678
"gatsby-plugin-sharp": "^4.16.1",
5779
"gatsby-plugin-sitemap": "^5.16.0",
5880
"gatsby-plugin-styled-components": "^5.16.0",
81+
"gatsby-plugin-webpack-bundle-analyser-v2": "^1.1.32",
5982
"gatsby-remark-autolink-headers": "^5.16.0",
6083
"gatsby-source-filesystem": "^4.16.0",
6184
"gatsby-transformer-sharp": "^4.16.0",
6285
"github-slugger": "^1.4.0",
6386
"gray-matter": "^4.0.3",
6487
"hast-util-to-string": "1.0.4",
6588
"hast-util-to-text": "2.0.0",
89+
"identity-obj-proxy": "^3.0.0",
6690
"import-fresh": "^3.3.0",
91+
"jest": "^27.1.1",
6792
"jotai": "^2.6.1",
6893
"katex": "^0.16.0",
94+
"lint-staged": "^11.1.2",
6995
"loader-utils": "^2.0.4",
7096
"loadjs": "^4.2.0",
7197
"octokit": "^3.1.2",
7298
"postcss": "^8.4.31",
73-
"postcss-import": "^14.1.0",
99+
"postcss-cli": "^8.3.1",
100+
"postcss-import": "^12.0.1",
74101
"postcss-nested": "^5.0.6",
102+
"prettier": "^2.7.1",
103+
"prettier-plugin-organize-imports": "^3.0.0",
75104
"prism-react-renderer": "^2.3.1",
76105
"prismjs": "^1.28.0",
77106
"process": "^0.11.10",
@@ -92,6 +121,7 @@
92121
"react-share": "^4.4.0",
93122
"react-simplemde-editor": "4.1.3",
94123
"react-split": "^2.0.14",
124+
"react-test-renderer": "^17.0.2",
95125
"react-youtube": "^7.13.1",
96126
"readline": "^1.3.0",
97127
"recharts": "^2.1.2",
@@ -108,7 +138,9 @@
108138
"remark-mdx-frontmatter": "1.1.1",
109139
"remark-slug": "6.0.0",
110140
"seedrandom": "^3.0.5",
141+
"serve": "^12.0.1",
111142
"storybook-addon-gatsby": "^0.0.5",
143+
"storybook-dark-mode": "^2.0.5",
112144
"styled-components": "^5.3.5",
113145
"tailwindcss": "^3.1.3",
114146
"tippy.js": "^6.3.1",
@@ -121,43 +153,9 @@
121153
"worker-loader": "^3.0.8",
122154
"xdm": "1.12.2"
123155
},
124-
"devDependencies": {
125-
"@mdx-js/loader": "^1.6.22",
126-
"@storybook/addon-a11y": "^6.5.16",
127-
"@storybook/addon-actions": "^6.5.16",
128-
"@storybook/addon-essentials": "^6.5.16",
129-
"@storybook/addon-links": "^6.5.16",
130-
"@storybook/addon-postcss": "^2.0.0",
131-
"@storybook/react": "^6.5.16",
132-
"@testing-library/jest-dom": "^5.14.1",
133-
"@testing-library/react": "^12.1.0",
134-
"@types/bad-words": "^3.0.1",
135-
"@types/react": "^17.0.37",
136-
"@typescript-eslint/eslint-plugin": "^5.28.0",
137-
"@typescript-eslint/parser": "^5.28.0",
138-
"babel-jest": "^28.1.1",
139-
"babel-loader": "^8.2.5",
140-
"babel-preset-gatsby": "^2.16.0",
141-
"babel-preset-react-app": "^10.0.1",
142-
"blob-polyfill": "^5.0.20210201",
143-
"bundlewatch": "^0.3.2",
144-
"chromatic": "^5.9.2",
145-
"eslint-plugin-jest": "^24.4.0",
146-
"firebase-admin": "^9.11.1",
147-
"firestore-export-import": "^0.17.0",
148-
"gatsby-plugin-webpack-bundle-analyser-v2": "^1.1.32",
149-
"identity-obj-proxy": "^3.0.0",
150-
"jest": "^27.1.1",
151-
"lint-staged": "^11.1.2",
152-
"postcss-cli": "^8.3.1",
153-
"prettier": "^2.7.1",
154-
"prettier-plugin-organize-imports": "^3.0.0",
155-
"react-test-renderer": "^17.0.2",
156-
"serve": "^12.0.1",
157-
"storybook-dark-mode": "^2.0.5"
158-
},
159156
"resolutions": {
160-
"@types/react": "^17.0.37"
157+
"@types/react": "^17.0.37",
158+
"postcss": "^8.4.31"
161159
},
162160
"license": "MIT",
163161
"scripts": {

src/api/fetch-html.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import axios from 'axios';
2+
import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby';
3+
interface RequestBody {
4+
url: string;
5+
}
6+
export default async function handler(
7+
request: GatsbyFunctionRequest<RequestBody>,
8+
response: GatsbyFunctionResponse
9+
) {
10+
// const res = await axios.get(
11+
// 'https://codeforces.com/problemset/problem/1917/D'
12+
// );
13+
console.log(request.body.url, 'url');
14+
const res = await axios.get(request.body.url);
15+
response.json({ data: res.data });
16+
}
+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Dialog, Transition } from '@headlessui/react';
2+
import prettier from 'prettier';
3+
import babelParser from 'prettier/parser-babel';
4+
import React, { useRef, useState } from 'react';
5+
import CopyButton from './CopyButton';
6+
import parse, { parsers } from './parsers/parse';
7+
async function getHtml(url: string): Promise<string> {
8+
const res = await fetch('/api/fetch-html', {
9+
method: 'POST',
10+
headers: {
11+
'Content-Type': 'application/json',
12+
},
13+
body: JSON.stringify({ url }),
14+
}).then(res => res.json());
15+
return res.data;
16+
}
17+
async function addProblem(
18+
url: string,
19+
setMetadata: (metadata: string) => void,
20+
setStatus: (status: string) => void
21+
) {
22+
try {
23+
setStatus('Fetching metadata...');
24+
const html = await getHtml(url);
25+
const parsed = parse(url, html);
26+
const metadata = {
27+
uniqueId: parsed.uniqueId,
28+
name: parsed.name,
29+
url,
30+
source: parsed.source,
31+
difficulty: 'N/A',
32+
isStarred: false,
33+
tags: ['Add Tags'],
34+
solutionMetadata: parsed.solutionMetadata,
35+
};
36+
console.log(metadata);
37+
setMetadata(
38+
await prettier.format(JSON.stringify(metadata), {
39+
parser: 'json',
40+
plugins: [babelParser],
41+
})
42+
);
43+
setStatus('Get Metadata');
44+
} catch (e) {
45+
setMetadata(
46+
`No parser found for this url.
47+
Available parsers:
48+
${Object.keys(parsers)
49+
.map(key => ` - ${key}`)
50+
.join('\n')}`
51+
);
52+
setStatus('Get Metadata');
53+
}
54+
}
55+
export default function AddProblemModal({ isOpen, onClose }) {
56+
const linkRef = useRef<HTMLInputElement>(null);
57+
const [metadata, setMetadata] = useState('// metadata will appear here');
58+
const [status, setStatus] = useState('Get Metadata');
59+
return (
60+
<Transition appear show={isOpen} as={React.Fragment}>
61+
<Dialog as="div" className="relative z-10" onClose={onClose}>
62+
<Transition.Child
63+
as={React.Fragment}
64+
enter="ease-out duration-300"
65+
enterFrom="opacity-0"
66+
enterTo="opacity-100"
67+
leave="ease-in duration-200"
68+
leaveFrom="opacity-100"
69+
leaveTo="opacity-0"
70+
>
71+
<div className="fixed inset-0 bg-black/25" />
72+
</Transition.Child>
73+
74+
<div className="fixed inset-0 overflow-y-auto">
75+
<div className="flex min-h-full items-center justify-center p-4 text-center">
76+
<Transition.Child
77+
as={React.Fragment}
78+
enter="ease-out duration-300"
79+
enterFrom="opacity-0 scale-95"
80+
enterTo="opacity-100 scale-100"
81+
leave="ease-in duration-200"
82+
leaveFrom="opacity-100 scale-100"
83+
leaveTo="opacity-0 scale-95"
84+
>
85+
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-black text-white p-6 text-left align-middle shadow-xl transition-all">
86+
<Dialog.Title as="h3" className="text-lg font-medium leading-6">
87+
Add Problem
88+
</Dialog.Title>
89+
<div className="mt-2 relative rounded-md shadow-sm">
90+
<input
91+
type="text"
92+
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-900 dark:border-gray-700"
93+
placeholder="Enter Problem URL"
94+
onChange={e => console.log(e.target.value)}
95+
ref={linkRef}
96+
/>
97+
</div>
98+
99+
<div className="mt-4">
100+
<button
101+
className="btn"
102+
onClick={() =>
103+
linkRef.current &&
104+
addProblem(linkRef.current.value, setMetadata, setStatus)
105+
}
106+
>
107+
{status}
108+
</button>
109+
</div>
110+
<div className="mt-4 relative">
111+
<pre className="bg-gray-900 p-4 rounded-md text-white text-xs whitespace-pre-wrap">
112+
{metadata}
113+
</pre>
114+
<CopyButton
115+
className="btn absolute top-2 right-2"
116+
onClick={() => {
117+
navigator.clipboard.writeText(metadata);
118+
}}
119+
/>
120+
</div>
121+
</Dialog.Panel>
122+
</Transition.Child>
123+
</div>
124+
</div>
125+
</Dialog>
126+
</Transition>
127+
);
128+
}

src/components/Editor/CopyButton.tsx

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// source: https://codesandbox.io/p/sandbox/copy-to-clipboard-animation-qt8pf
2+
import React from 'react';
3+
4+
export default function CopyButton({ className, onClick }) {
5+
const [copied, setCopied] = React.useState(false);
6+
7+
React.useEffect(() => {
8+
const timeout = setTimeout(() => {
9+
if (copied) setCopied(false);
10+
}, 1000);
11+
12+
return () => clearTimeout(timeout);
13+
}, [copied]);
14+
15+
return (
16+
<button
17+
onClick={() => {
18+
setCopied(true);
19+
onClick();
20+
}}
21+
className={`appearance-none p-2 border-0 outline-none cursor-pointer ${className}`}
22+
>
23+
<div className="relative h-4 w-4">
24+
<Clippy
25+
style={{
26+
strokeDasharray: 50,
27+
strokeDashoffset: copied ? -50 : 0,
28+
transition: 'all 300ms ease-in-out',
29+
}}
30+
/>
31+
<Check
32+
style={{
33+
strokeDasharray: 50,
34+
strokeDashoffset: copied ? 0 : -50,
35+
transition: 'all 300ms ease-in-out',
36+
}}
37+
/>
38+
</div>
39+
</button>
40+
);
41+
}
42+
43+
function Clippy(props) {
44+
return (
45+
<svg
46+
width="16"
47+
height="16"
48+
viewBox="0 0 16 16"
49+
fill="none"
50+
strokeLinecap="round"
51+
strokeLinejoin="round"
52+
className="stroke-gray-400 stroke-[1.5] absolute top-0 left-0"
53+
{...props}
54+
>
55+
<path d="M5.75 4.75H10.25V1.75H5.75V4.75Z" />
56+
<path d="M3.25 2.88379C2.9511 3.05669 2.75 3.37987 2.75 3.75001V13.25C2.75 13.8023 3.19772 14.25 3.75 14.25H12.25C12.8023 14.25 13.25 13.8023 13.25 13.25V3.75001C13.25 3.37987 13.0489 3.05669 12.75 2.88379" />
57+
</svg>
58+
);
59+
}
60+
61+
function Check(props) {
62+
return (
63+
<svg
64+
width="16"
65+
height="16"
66+
viewBox="0 0 16 16"
67+
fill="none"
68+
strokeLinecap="round"
69+
strokeLinejoin="round"
70+
{...props}
71+
className="stroke-green-500 stroke-[1.5] absolute top-0 left-0"
72+
>
73+
<path d="M13.25 4.75L6 12L2.75 8.75" />
74+
</svg>
75+
);
76+
}

0 commit comments

Comments
 (0)