Skip to content

Commit c227e68

Browse files
authored
Merge pull request #4254 from cpinitiative/add_file
support adding arbitrary file path + refactor modals
2 parents b547369 + 6d1977a commit c227e68

25 files changed

+507
-460
lines changed

.storybook/preview.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ export const parameters = {
4444
};
4545

4646
export const decorators = [
47-
renderStory => (
47+
Story => (
4848
<div className="grid storybook-container font-sans">
4949
<div className="h-full">
5050
<div className="p-4 sm:p-6 lg:p-8 max-w-4xl mx-auto">
5151
<p className="text-gray-800 text-2xl font-bold">Light Mode</p>
5252
<div className="h-4" />
5353
<DarkModeContext.Provider value={false}>
54-
{renderStory()}
54+
<Story />
5555
</DarkModeContext.Provider>
5656
</div>
5757
</div>
@@ -60,7 +60,7 @@ export const decorators = [
6060
<p className="text-gray-100 text-2xl font-bold">Dark Mode</p>
6161
<div className="h-4" />
6262
<DarkModeContext.Provider value={true}>
63-
{renderStory()}
63+
<Story />
6464
</DarkModeContext.Provider>
6565
</div>
6666
</div>

src/components/Editor/parsers/ac.ts src/api/(parsers)/ac.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ export default function parseAc(url: string, html: string) {
77
uniqueId,
88
name,
99
source: 'AC',
10-
solutionMetadata: {
11-
kind: 'CHANGE THIS',
12-
},
10+
solutionMetadata: { kind: 'none' },
1311
};
1412
}

src/components/Editor/parsers/cf.ts src/api/(parsers)/cf.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// example gym url: https://codeforces.com/gym/102951/problem/A
44
export default function parseCf(url: string, html: string) {
55
const urlSplit = url.split('/');
6-
const contestId = urlSplit.at(-2 - +url.includes('gym'));
6+
const contestId = urlSplit.at(
7+
-2 - +url.includes('gym') - +url.includes('contest')
8+
);
79
const problemId = urlSplit.at(-1);
810
const titleRegex = /<div class="title">.*?\. (.*?)<\/div>/;
911
return {

src/components/Editor/parsers/cses.ts src/api/(parsers)/cses.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ export default function parseCses(url: string, html: string) {
77
uniqueId: `cses-${problemId}`, // e.g. cses-1083
88
name: html.match(titleRegex)?.[1] ?? 'Unknown',
99
source: 'CSES',
10-
solutionMetadata: {
11-
kind: 'CHANGE THIS',
12-
},
10+
solutionMetadata: { kind: 'none' },
1311
};
1412
}

src/components/Editor/parsers/parse.ts src/api/(parsers)/parse.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import axios from 'axios';
12
import parseAc from './ac';
23
import parseCf from './cf';
34
import parseCses from './cses';
@@ -8,11 +9,17 @@ export const parsers = {
89
'cses.fi': parseCses,
910
'atcoder.jp': parseAc,
1011
};
11-
export default function parse(url: string, html: string) {
12+
13+
export default async function parse(url: string) {
14+
const html = (await axios.get(url)).data;
1215
for (const [domain, parser] of Object.entries(parsers)) {
1316
if (url.includes(domain)) {
1417
return parser(url, html);
1518
}
1619
}
17-
throw new Error('No parser found for this URL');
20+
throw new Error(`No parser found for this url.
21+
Available parsers:
22+
${Object.keys(parsers)
23+
.map(key => ` - ${key}`)
24+
.join('\n')}`);
1825
}
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import axios from 'axios';
21
import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby';
2+
import parse from './(parsers)/parse';
33
interface RequestBody {
44
url: string;
55
}
66
export default async function handler(
77
request: GatsbyFunctionRequest<RequestBody>,
88
response: GatsbyFunctionResponse
99
) {
10-
// const res = await axios.get(
11-
// 'https://codeforces.com/problemset/problem/1917/D'
12-
// );
1310
console.log(request.body.url, 'url');
14-
const res = await axios.get(request.body.url);
15-
response.json({ data: res.data });
11+
response.json({ data: await parse(request.body.url) });
1612
}

src/atoms/editor.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@ export const openOrCreateExistingFileAtom = atom(
119119
export const createNewInternalSolutionFileAtom = atom(
120120
null,
121121
async (get, set, file: AlgoliaEditorSolutionFile) => {
122-
console.log(file);
123122
const module = file.problemModules[0]?.path.split('/')[1];
124-
console.log(module);
125-
const division = !module ? 'orphaned' : module.split('_')[1].toLowerCase();
123+
const division =
124+
file.division ||
125+
(!module ? 'orphaned' : module.split('_')[1].toLowerCase());
126126
const newFile: EditorFile = {
127127
path: `solutions/${division}/${file.id}.mdx`,
128128
markdown: `---
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Dialog } from '@headlessui/react';
2+
import { useSetAtom } from 'jotai';
3+
import React, { useState } from 'react';
4+
import { createNewInternalSolutionFileAtom } from '../../atoms/editor';
5+
import { AlgoliaEditorSolutionFile } from '../../models/algoliaEditorFile';
6+
import Modal from '../Modal';
7+
import Select from '../Select';
8+
const divisions = [
9+
'General',
10+
'Bronze',
11+
'Silver',
12+
'Gold',
13+
'Platinum',
14+
'Advanced',
15+
] as const; // hack to allow typeof divisions[number] by marking array as readonly
16+
export default function AddFileModal(props) {
17+
const [division, setDivision] =
18+
useState<(typeof divisions)[number]>('General');
19+
const [fileStatus, setFileStatus] = useState<
20+
'Create File' | 'Creating File...'
21+
>('Create File');
22+
const [fileURL, setFileURL] = useState('');
23+
const createSol = useSetAtom(createNewInternalSolutionFileAtom);
24+
return (
25+
<Modal {...props}>
26+
<Dialog.Panel className="bg-white dark:bg-black w-full max-w-xl dark:text-white p-5 rounded-lg shadow-lg flex flex-col items-start">
27+
<h3 className="text-lg font-bold">Enter Problem URL</h3>
28+
<input
29+
type="url"
30+
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"
31+
placeholder="e.g. https://codeforces.com/contest/1920/problem/C"
32+
onChange={e => setFileURL(e.target.value)}
33+
/>
34+
<p className="mt-2">Problem Division</p>
35+
<div className="mt-2 relative w-full dark:bg-black rounded-md shadow-sm">
36+
<Select
37+
options={[
38+
'General',
39+
'Bronze',
40+
'Silver',
41+
'Gold',
42+
'Platinum',
43+
'Advanced',
44+
].map(div => ({
45+
label: div,
46+
value: div.toLowerCase(),
47+
}))}
48+
onChange={e => setDivision(e.value)}
49+
/>
50+
</div>
51+
<button
52+
className="btn mt-2"
53+
disabled={fileStatus === 'Creating File...'}
54+
onClick={async () => {
55+
try {
56+
setFileStatus('Creating File...');
57+
const info = (
58+
await fetch('/api/fetch-metadata', {
59+
method: 'POST',
60+
headers: {
61+
'Content-Type': 'application/json',
62+
},
63+
body: JSON.stringify({ url: fileURL }),
64+
}).then(res => res.json())
65+
).data;
66+
props.onClose();
67+
createSol({
68+
id: info.uniqueId,
69+
title: info.name,
70+
source: info.source,
71+
division,
72+
problemModules: [],
73+
} as AlgoliaEditorSolutionFile);
74+
setFileStatus('Create File');
75+
} catch (e) {
76+
setFileStatus('Create File');
77+
props.onClose();
78+
alert(e);
79+
}
80+
}}
81+
>
82+
{fileStatus}
83+
</button>
84+
</Dialog.Panel>
85+
</Modal>
86+
);
87+
}
+57-93
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
1-
import { Dialog, Transition } from '@headlessui/react';
1+
import { Dialog } from '@headlessui/react';
22
import prettier from 'prettier';
33
import babelParser from 'prettier/parser-babel';
4-
import React, { useRef, useState } from 'react';
4+
import React, { useState } from 'react';
5+
import Modal from '../Modal';
56
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-
}
177
async function addProblem(
188
url: string,
199
setMetadata: (metadata: string) => void,
20-
setStatus: (status: string) => void
10+
setStatus: (status: 'Get Metadata' | 'Fetching metadata...') => void
2111
) {
2212
try {
2313
setStatus('Fetching metadata...');
24-
const html = await getHtml(url);
25-
const parsed = parse(url, html);
14+
const parsed = (
15+
await fetch('/api/fetch-metadata', {
16+
method: 'POST',
17+
headers: {
18+
'Content-Type': 'application/json',
19+
},
20+
body: JSON.stringify({ url }),
21+
}).then(res => res.json())
22+
).data;
2623
const metadata = {
2724
uniqueId: parsed.uniqueId,
2825
name: parsed.name,
@@ -33,96 +30,63 @@ async function addProblem(
3330
tags: ['Add Tags'],
3431
solutionMetadata: parsed.solutionMetadata,
3532
};
36-
console.log(metadata);
3733
setMetadata(
38-
await prettier.format(JSON.stringify(metadata), {
34+
await prettier.format(JSON.stringify(metadata, null, 2), {
3935
parser: 'json',
4036
plugins: [babelParser],
4137
})
4238
);
4339
setStatus('Get Metadata');
4440
} 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-
);
41+
setMetadata(e.toString());
5242
setStatus('Get Metadata');
5343
}
5444
}
55-
export default function AddProblemModal({ isOpen, onClose }) {
56-
const linkRef = useRef<HTMLInputElement>(null);
45+
export default function AddProblemModal(props: {
46+
isOpen: boolean;
47+
onClose: () => void;
48+
}) {
49+
const [link, setLink] = useState('');
5750
const [metadata, setMetadata] = useState('// metadata will appear here');
58-
const [status, setStatus] = useState('Get Metadata');
51+
const [status, setStatus] = useState<'Get Metadata' | 'Fetching metadata...'>(
52+
'Get Metadata'
53+
);
5954
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>
55+
<Modal {...props}>
56+
<Dialog.Panel className="w-full max-w-2xl transform overflow-hidden rounded-2xl bg-black text-white p-6 text-left align-middle shadow-xl transition-all">
57+
<Dialog.Title as="h3" className="text-lg font-medium leading-6">
58+
Add Problem
59+
</Dialog.Title>
60+
<div className="mt-2 relative rounded-md shadow-sm">
61+
<input
62+
type="text"
63+
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"
64+
placeholder="Enter Problem URL"
65+
onChange={e => setLink(e.target.value)}
66+
/>
67+
</div>
9868

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>
69+
<div className="mt-4">
70+
<button
71+
className="btn"
72+
disabled={status === 'Fetching metadata...'}
73+
onClick={() => addProblem(link, setMetadata, setStatus)}
74+
>
75+
{status}
76+
</button>
77+
</div>
78+
<div className="mt-4 relative">
79+
<pre className="bg-gray-900 p-4 rounded-md text-white text-sm whitespace-pre-wrap">
80+
{metadata}
81+
</pre>
82+
<CopyButton
83+
className="btn absolute top-2 right-2"
84+
onClick={() => {
85+
navigator.clipboard.writeText(metadata);
86+
}}
87+
/>
12488
</div>
125-
</Dialog>
126-
</Transition>
89+
</Dialog.Panel>
90+
</Modal>
12791
);
12892
}

0 commit comments

Comments
 (0)